| // 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 Kronecker product, built entry-wise and independently of both |
| // the operator under test and the KroneckerProduct module. |
| template <typename Scalar> |
| Matrix<Scalar, Dynamic, Dynamic> reference_kron(const Matrix<Scalar, Dynamic, Dynamic>& A, |
| const Matrix<Scalar, Dynamic, Dynamic>& B) { |
| Matrix<Scalar, Dynamic, Dynamic> K(A.rows() * B.rows(), A.cols() * B.cols()); |
| for (Index i = 0; i < K.rows(); ++i) |
| for (Index j = 0; j < K.cols(); ++j) K(i, j) = A(i / B.rows(), j / B.cols()) * B(i % B.rows(), j % B.cols()); |
| return K; |
| } |
| |
| template <typename Scalar> |
| void test_kron_product(Index m1, Index n1, Index m2, Index n2) { |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| Mat A = Mat::Random(m1, n1), B = Mat::Random(m2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| VERIFY_IS_EQUAL(K.rows(), m1 * m2); |
| VERIFY_IS_EQUAL(K.cols(), n1 * n2); |
| |
| // Dense assignment through the evaluator and coefficient access. |
| Mat Kd = K; |
| VERIFY_IS_APPROX(Kd, dense); |
| for (Index t = 0; t < 5; ++t) { |
| Index i = internal::random<Index>(0, K.rows() - 1), j = internal::random<Index>(0, K.cols() - 1); |
| VERIFY_IS_APPROX(K.coeff(i, j), dense(i, j)); |
| } |
| |
| // Fast matrix-vector and matrix-matrix products via the vec identity. |
| Vec x = Vec::Random(n1 * n2); |
| VERIFY_IS_APPROX((K * x).eval(), (dense * x).eval()); |
| Mat X = Mat::Random(n1 * n2, 3); |
| VERIFY_IS_APPROX((K * X).eval(), (dense * X).eval()); |
| |
| // A matrix product on the right must be materialized once before addProduct() |
| // takes column expressions from it. Keep the outer expression alive past the |
| // full expression that created the nested product as a lifetime check too. |
| Mat P = Mat::Random(n1 * n2, 4), Q = Mat::Random(4, 3); |
| auto nestedProduct = K * (P * Q); |
| Mat nestedResult = nestedProduct; |
| VERIFY_IS_APPROX(nestedResult, (dense * (P * Q)).eval()); |
| |
| // Accumulation form exercised by the iterative solvers. |
| Vec y = Vec::Random(m1 * m2); |
| Vec y0 = y; |
| y.noalias() += K * x; |
| VERIFY_IS_APPROX(y, (y0 + dense * x).eval()); |
| } |
| |
| template <typename Scalar> |
| void test_kron_transpose(Index m1, Index n1, Index m2, Index n2) { |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| Mat A = Mat::Random(m1, n1), B = Mat::Random(m2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| |
| Mat Td = K.transpose(); |
| VERIFY_IS_APPROX(Td, Mat(dense.transpose())); |
| Mat Ad = K.adjoint(); |
| VERIFY_IS_APPROX(Ad, Mat(dense.adjoint())); |
| Mat Cd = K.conjugate(); |
| VERIFY_IS_APPROX(Cd, Mat(dense.conjugate())); |
| |
| Vec y = Vec::Random(m1 * m2); |
| VERIFY_IS_APPROX((K.transpose() * y).eval(), (dense.transpose() * y).eval()); |
| VERIFY_IS_APPROX((K.adjoint() * y).eval(), (dense.adjoint() * y).eval()); |
| } |
| |
| // The products carry the default product tag, so plain assignment materializes a |
| // temporary exactly like a dense product would. That must resolve every aliasing |
| // form between the destination and the right-hand side: the same object, an |
| // expression referencing the destination, and overlapping views -- cases the old |
| // AliasFreeProduct same-object check could not see. |
| template <typename Scalar> |
| void test_kron_aliased_product(Index n1, Index n2) { |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| // Square factors so that the destination and the right-hand side can alias. |
| Mat A = Mat::Random(n1, n1), B = Mat::Random(n2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| |
| Vec x = Vec::Random(n1 * n2); |
| Vec y = x; |
| y = K * y; |
| VERIFY_IS_APPROX(y, (dense * x).eval()); |
| |
| y = x; |
| y += K * y; |
| VERIFY_IS_APPROX(y, (x + dense * x).eval()); |
| |
| y = x; |
| y -= K * y; |
| VERIFY_IS_APPROX(y, (x - dense * x).eval()); |
| |
| Mat X = Mat::Random(n1 * n2, 3); |
| Mat Y = X; |
| Y = K * Y; |
| VERIFY_IS_APPROX(Y, (dense * X).eval()); |
| |
| // Right-hand-side expression referencing the destination. |
| y = x; |
| y = K * (y + Vec::Ones(n1 * n2)); |
| VERIFY_IS_APPROX(y, (dense * (x + Vec::Ones(n1 * n2))).eval()); |
| |
| // Overlapping views: the destination shares all but one entry with the |
| // right-hand side. |
| Vec buf = Vec::Random(n1 * n2 + 1); |
| const Vec expected = dense * buf.tail(n1 * n2); |
| buf.head(n1 * n2) = K * buf.tail(n1 * n2); |
| VERIFY_IS_APPROX(buf.head(n1 * n2).eval(), expected); |
| } |
| |
| // Rectangular operators resize an aliased destination: x = K * x must evaluate |
| // into a temporary before x is resized from cols() to rows() entries. |
| template <typename Scalar> |
| void test_kron_aliased_resize(Index m1, Index n1, Index m2, Index n2) { |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| Mat A = Mat::Random(m1, n1), B = Mat::Random(m2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| |
| Vec x = Vec::Random(n1 * n2); |
| const Vec xref = x; |
| x = K * x; |
| VERIFY_IS_EQUAL(x.size(), m1 * m2); |
| VERIFY_IS_APPROX(x, (dense * xref).eval()); |
| |
| Mat X = Mat::Random(n1 * n2, 2); |
| const Mat Xref = X; |
| X = K * X; |
| VERIFY_IS_EQUAL(X.rows(), m1 * m2); |
| VERIFY_IS_APPROX(X, (dense * Xref).eval()); |
| } |
| |
| // transpose()/conjugate()/adjoint() and makeKroneckerOperator() 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_kron_delayed_product(Index m1, Index n1, Index m2, Index n2) { |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| STATIC_CHECK((!std::is_reference<typename internal::ref_selector<KroneckerOperator<Mat, Mat>>::type>::value)); |
| |
| Mat A = Mat::Random(m1, n1), B = Mat::Random(m2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| |
| Vec y = Vec::Random(m1 * m2); |
| auto expr = K.adjoint() * y; // the adjoint temporary dies with the full expression |
| Vec scribble = Vec::Random(2 * (m1 * m2 + n1 * n2)); // reuses the temporary's freed heap storage |
| Vec x = expr; |
| VERIFY_IS_APPROX(x, (dense.adjoint() * y).eval()); |
| VERIFY_IS_EQUAL(scribble.size(), 2 * (m1 * m2 + n1 * n2)); // keep the scribble alive across the evaluation |
| |
| Vec xin = Vec::Random(n1 * n2); |
| auto expr2 = makeKroneckerOperator(Mat(A), Mat(B)) * xin; // factory temporary |
| Vec scribble2 = Vec::Random(m1 * m2 + n1 * n2); |
| Vec y2 = expr2; |
| VERIFY_IS_APPROX(y2, (dense * xin).eval()); |
| VERIFY_IS_EQUAL(scribble2.size(), m1 * m2 + n1 * n2); |
| } |
| |
| // Mixed-scalar products: a real operator applied to a complex right-hand side |
| // (and a complex operator applied to a real one) promotes to the complex product |
| // scalar, so alpha, the vec-trick workspaces and the accumulation must run in the |
| // promoted type rather than the operator scalar. |
| template <typename RealScalar> |
| void test_kron_mixed_scalar(Index m1, Index n1, Index m2, Index n2) { |
| typedef std::complex<RealScalar> Complex; |
| typedef Matrix<RealScalar, Dynamic, 1> RVec; |
| typedef Matrix<RealScalar, Dynamic, Dynamic> RMat; |
| typedef Matrix<Complex, Dynamic, 1> CVec; |
| typedef Matrix<Complex, Dynamic, Dynamic> CMat; |
| |
| RMat A = RMat::Random(m1, n1), B = RMat::Random(m2, n2); |
| KroneckerOperator<RMat, RMat> K(A, B); |
| CMat dense = reference_kron<RealScalar>(A, B).template cast<Complex>(); |
| |
| CVec x = CVec::Random(n1 * n2); |
| CVec y = K * x; |
| VERIFY_IS_APPROX(y, (dense * x).eval()); |
| |
| CVec y0 = CVec::Random(m1 * m2); |
| y = y0; |
| y.noalias() += K * x; |
| VERIFY_IS_APPROX(y, (y0 + dense * x).eval()); |
| |
| CMat Ac = CMat::Random(m1, n1), Bc = CMat::Random(m2, n2); |
| KroneckerOperator<CMat, CMat> Kc(Ac, Bc); |
| CMat denseC = reference_kron<Complex>(Ac, Bc); |
| RVec xr = RVec::Random(n1 * n2); |
| CVec z = Kc * xr; |
| VERIFY_IS_APPROX(z, (denseC * xr).eval()); |
| } |
| |
| template <typename Scalar> |
| void test_kron_solve(Index n1, Index n2) { |
| typedef typename NumTraits<Scalar>::Real RealScalar; |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| // Diagonal boosts keep both factors (hence the product) well conditioned. |
| Mat A = Mat::Random(n1, n1) + RealScalar(2 * n1) * Mat::Identity(n1, n1); |
| Mat B = Mat::Random(n2, n2) + RealScalar(2 * n2) * Mat::Identity(n2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| |
| Vec b = Vec::Random(n1 * n2); |
| Vec x = K.solve(b); |
| VERIFY_IS_APPROX((dense * x).eval(), b); |
| VERIFY_IS_APPROX(x, dense.partialPivLu().solve(b).eval()); |
| |
| Mat Bm = Mat::Random(n1 * n2, 3); |
| Mat Xm = K.solve(Bm); |
| VERIFY_IS_APPROX(Xm, dense.partialPivLu().solve(Bm).eval()); |
| } |
| |
| template <typename Scalar> |
| void test_kron_least_squares(Index m1, Index n1, Index m2, Index n2) { |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| Mat A = Mat::Random(m1, n1), B = Mat::Random(m2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| |
| // The minimum-norm least-squares solution is unique, so the factored |
| // pseudo-inverse must match the dense complete orthogonal decomposition. |
| Vec b = Vec::Random(m1 * m2); |
| Vec x = K.leastSquaresSolve(b); |
| VERIFY_IS_APPROX(x, dense.completeOrthogonalDecomposition().solve(b).eval()); |
| } |
| |
| template <typename Scalar> |
| void test_kron_least_squares_rank_deficient(Index m1, Index n1, Index m2, Index n2) { |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| // A rank-one left factor and a rank-two right factor make the product rank |
| // deficient; the minimum-norm least-squares solution stays unique and |
| // comparable against the dense complete orthogonal decomposition. |
| Mat A = Vec::Random(m1) * Vec::Random(n1).transpose(); |
| Mat B = Vec::Random(m2) * Vec::Random(n2).transpose() + Vec::Random(m2) * Vec::Random(n2).transpose(); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| VERIFY_IS_EQUAL(K.rank(), dense.completeOrthogonalDecomposition().rank()); |
| |
| Vec b = Vec::Random(m1 * m2); |
| Vec x = K.leastSquaresSolve(b); |
| VERIFY_IS_APPROX(x, dense.completeOrthogonalDecomposition().solve(b).eval()); |
| } |
| |
| // Near-rank-deficiency must be judged at the product level: with |
| // A = B = diag(1, 1e-8) each factor is full rank against its own threshold, but |
| // the smallest singular value of the product, 1e-16, falls below the |
| // product-level threshold min(rows,cols) * eps * sigma_max(A) * sigma_max(B). |
| // The rank is 3 -- matching the dense complete orthogonal decomposition -- and |
| // leastSquaresSolve() must truncate the 1e-16 mode instead of inverting it. |
| void test_kron_product_level_rank() { |
| typedef Matrix<double, Dynamic, 1> Vec; |
| typedef Matrix<double, Dynamic, Dynamic> Mat; |
| |
| const double small = 1e-8; // sqrt(eps)-ish: kept per factor, truncated as a pairwise product |
| Mat A(2, 2); |
| A << 1, 0, 0, small; |
| Mat B = A; |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<double>(A, B); |
| |
| CompleteOrthogonalDecomposition<Mat> cod(dense); |
| VERIFY_IS_EQUAL(cod.rank(), 3); |
| VERIFY_IS_EQUAL(K.rank(), cod.rank()); |
| |
| Vec b = Vec::Random(4); |
| Vec x = K.leastSquaresSolve(b); |
| VERIFY_IS_APPROX(x, cod.solve(b).eval()); |
| // Direct encoding of "the 1e-16 mode is not inverted": the solution is bounded |
| // by the reciprocal of the smallest kept singular value (with slack), far below |
| // the 1e16 blow-up an inverted product mode would produce. |
| const double invertedModeBound = double(10) / small; // smallest kept product singular value is `small` |
| VERIFY(x.norm() <= invertedModeBound * b.norm()); |
| } |
| |
| // det(A (x) B) = det(A)^n2 * det(B)^n1 must be accumulated with exponent |
| // balancing: det(A)^n2 or det(B)^n1 can overflow or underflow on their own even |
| // when the result is representable. |
| void test_kron_determinant_scaling() { |
| typedef Matrix<double, Dynamic, Dynamic> Mat; |
| |
| // Reviewer repro: det(A)^2 = 1e400 overflows on its own, yet the determinant |
| // of K = diag(1e100, 1e100) is 1e200. |
| Mat A(1, 1); |
| A << 1e200; |
| Mat B = Mat::Zero(2, 2); |
| B(0, 0) = B(1, 1) = 1e-100; |
| KroneckerOperator<Mat, Mat> K(A, B); |
| VERIFY_IS_APPROX(K.determinant(), 1e200); |
| VERIFY_IS_APPROX(K.determinant(), reference_kron<double>(A, B).determinant()); |
| |
| // Underflow-side analogue: det(A2)^2 = 1e-400 underflows on its own, yet the |
| // determinant of K2 = diag(1e-100, 1e-100) is 1e-200. |
| Mat A2(1, 1); |
| A2 << 1e-200; |
| Mat B2 = Mat::Zero(2, 2); |
| B2(0, 0) = B2(1, 1) = 1e100; |
| KroneckerOperator<Mat, Mat> K2(A2, B2); |
| VERIFY_IS_APPROX(K2.determinant(), 1e-200); |
| VERIFY_IS_APPROX(K2.determinant(), reference_kron<double>(A2, B2).determinant()); |
| |
| // A genuinely overflowing determinant (1e800) must still give infinity ... |
| Mat B3 = Mat::Zero(2, 2); |
| B3(0, 0) = B3(1, 1) = 1e200; |
| KroneckerOperator<Mat, Mat> K3(A, B3); |
| VERIFY((numext::isinf)(K3.determinant())); |
| VERIFY(K3.determinant() > 0); |
| |
| // ... and a genuinely underflowing one (1e-800) must flush to zero. |
| Mat B4 = Mat::Zero(2, 2); |
| B4(0, 0) = B4(1, 1) = 1e-200; |
| KroneckerOperator<Mat, Mat> K4(A2, B4); |
| VERIFY_IS_EQUAL(K4.determinant(), 0.0); |
| } |
| |
| // Reviewer repro: (A (x) B) with A = [1e-200] and B = [1e200] is exactly the |
| // identity, but evaluating (B X) A^T without protection overflows in |
| // B X = 1e400 and returns Inf. The power-of-two pre-scaling must keep every |
| // intermediate finite whenever the true result is representable -- and powers of |
| // two shift only exponents, so no accuracy may be lost. |
| void test_kron_extreme_scale_product() { |
| typedef Matrix<double, Dynamic, 1> Vec; |
| typedef Matrix<double, Dynamic, Dynamic> Mat; |
| |
| Mat A(1, 1), B(1, 1); |
| A << 1e-200; |
| B << 1e200; |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Vec x(1); |
| x << 1e200; |
| Vec y = K * x; |
| VERIFY(y.allFinite()); |
| VERIFY_IS_APPROX(y[0], 1e200); |
| |
| // Same magnitudes through solve(): with the factors swapped the operator is |
| // still the identity, and the intermediate B^{-1} mat(b) = 1e400 overflows; |
| // the normalized-frame solve must return the exact solution. |
| KroneckerOperator<Mat, Mat> Ks(B, A); |
| Vec b(1); |
| b << 1e200; |
| Vec xs = Ks.solve(b); |
| VERIFY(xs.allFinite()); |
| VERIFY_IS_APPROX(xs[0], 1e200); |
| |
| // Identity built from scaled unit matrices: the first GEMM is ~1e400 while |
| // K * x == x exactly. |
| Mat A2 = 1e-200 * Mat::Identity(2, 2), B2 = 1e200 * Mat::Identity(3, 3); |
| KroneckerOperator<Mat, Mat> K2(A2, B2); |
| Vec x2 = 1e200 * Vec::Random(6); |
| Vec y2 = K2 * x2; |
| VERIFY(y2.allFinite()); |
| VERIFY_IS_APPROX(y2, x2); |
| |
| // Multi-dimensional overflow boundary: the intermediate B X ~ 2^1103 overflows |
| // while the true result ~ 2^904 is representable -- and the dense reference can |
| // compute it, because the materialized product has moderate entries ~ 2^400. |
| Mat A3 = std::ldexp(1.0, -200) * Mat::Random(2, 2); |
| Mat B3 = std::ldexp(1.0, 600) * Mat::Random(2, 2); |
| KroneckerOperator<Mat, Mat> K3(A3, B3); |
| Mat dense3 = reference_kron<double>(A3, B3); |
| Vec x3 = std::ldexp(1.0, 500) * Vec::Random(4); |
| Vec y3 = K3 * x3; |
| VERIFY(y3.allFinite()); |
| VERIFY_IS_APPROX(y3, (dense3 * x3).eval()); |
| |
| // A genuinely overflowing result must still saturate to infinity. |
| KroneckerOperator<Mat, Mat> K4(B, B); // [1e200] (x) [1e200] |
| Vec y4 = K4 * x; // true result 1e600 |
| VERIFY((numext::isinf)(y4[0])); |
| VERIFY(y4[0] > 0); |
| } |
| |
| // Complex analogue: the exponent bounds must come from the component-wise |
| // magnitudes. A finite complex value near the overflow threshold has a |
| // non-representable modulus, which would silently disable the scaling (and a |
| // result with components near the threshold has one too, which must not be |
| // destroyed on the way back). |
| void test_kron_extreme_scale_complex() { |
| typedef std::complex<double> Complex; |
| typedef Matrix<Complex, Dynamic, 1> Vec; |
| typedef Matrix<Complex, Dynamic, Dynamic> Mat; |
| |
| Mat A(1, 1), B(1, 1); |
| A << Complex(1e-300, 0.0); |
| B << Complex(1e308, 1e308); // |B(0,0)| overflows; the components are finite |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Vec x(1); |
| x << Complex(1e300, 0.0); |
| Vec y = K * x; |
| VERIFY(y.allFinite()); |
| // Exact scalar reference, evaluated in an order that never leaves the |
| // representable range: (A * B) * x = (1e8 + 1e8 i) * 1e300. |
| const Complex expected = (A(0, 0) * B(0, 0)) * x[0]; |
| VERIFY_IS_APPROX(y[0], expected); |
| |
| Mat A2(1, 1), B2(1, 1); |
| A2 << Complex(1e-200, 0.0); |
| B2 << Complex(1.2e200, -1.3e200); |
| KroneckerOperator<Mat, Mat> K2(A2, B2); |
| Vec x2(1); |
| x2 << Complex(1.1e200, -0.7e200); |
| Vec y2 = K2 * x2; |
| VERIFY(y2.allFinite()); |
| VERIFY_IS_APPROX(y2[0], ((A2(0, 0) * B2(0, 0)) * x2[0])); |
| } |
| |
| // Reviewer repro: with A = [1e-310] (subnormal) the absolute rank threshold |
| // min(rows,cols) * eps * sa_max underflows to zero before the sb_max |
| // multiplication, so every pairwise singular-value product used to pass and |
| // rank() returned 8 where the dense SVD says 1. The comparison must happen in |
| // ratio space: (sa_i/sa_0) * (sb_j/sb_0) against min(rows,cols) * eps. |
| void test_kron_rank_ratio_threshold() { |
| typedef Matrix<double, Dynamic, 1> Vec; |
| typedef Matrix<double, Dynamic, Dynamic> Mat; |
| |
| Mat A(1, 1); |
| A << 1e-310; // subnormal |
| Mat B = Mat::Zero(8, 8); |
| for (Index i = 0; i < 8; ++i) B(i, i) = std::pow(10.0, 308.0 - 15.0 * double(i)); // 1e308, 1e293, ... |
| KroneckerOperator<Mat, Mat> K(A, B); |
| |
| // The materialized product has moderate entries (1e-2 down to 1e-107), so the |
| // dense SVD is a trustworthy reference: the singular-value ratios decay by |
| // 1e-15 per mode, below the 8 * eps ~ 1.8e-15 threshold, leaving rank one. |
| Mat dense = reference_kron<double>(A, B); |
| JacobiSVD<Mat, ComputeThinU | ComputeThinV> svd(dense); |
| VERIFY_IS_EQUAL(svd.rank(), 1); |
| VERIFY_IS_EQUAL(K.rank(), svd.rank()); |
| |
| // leastSquaresSolve must truncate the seven sub-threshold modes instead of |
| // inverting them: inverting even the second one would blow the solution up by |
| // ~1e17 (the last by ~1e107). The kept mode has sigma = 1e-2. |
| Vec b = Vec::Random(8); |
| Vec x = K.leastSquaresSolve(b); |
| VERIFY(x.allFinite()); |
| VERIFY_IS_APPROX(x, svd.solve(b).eval()); |
| VERIFY(x.norm() <= 1e3 * b.norm()); |
| |
| // An exactly zero factor zeroes the whole operator: rank 0 and a zero |
| // pseudo-inverse (instead of 0/0 singular-value ratios). |
| Mat Z = Mat::Zero(2, 2); |
| KroneckerOperator<Mat, Mat> Kz(Z, B.topLeftCorner(2, 2).eval()); |
| VERIFY_IS_EQUAL(Kz.rank(), 0); |
| Vec bz = Vec::Random(4); |
| VERIFY_IS_EQUAL(Kz.leastSquaresSolve(bz).norm(), 0.0); |
| } |
| |
| // The ratio test alone dropped SVDBase's smallest-normal clamp: a subnormal |
| // product singular value, whose reciprocal overflows, must count as an exact |
| // zero. A = [1], B = [DBL_MIN/2] has the singular value DBL_MIN/2: the dense |
| // rank is 0 and the pseudo-inverse is zero, but without the clamp the |
| // structured rank was 1 and leastSquaresSolve returned ~9e307. The boundary |
| // DBL_MIN itself stays rank one, matching SVDBase::rank()'s >= convention. |
| void test_kron_rank_min_normal_clamp() { |
| typedef Matrix<double, Dynamic, 1> Vec; |
| typedef Matrix<double, Dynamic, Dynamic> Mat; |
| const double mn = (std::numeric_limits<double>::min)(); |
| |
| Mat A(1, 1), B(1, 1); |
| A << 1.0; |
| B << mn / 2; // subnormal |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<double>(A, B); |
| JacobiSVD<Mat, ComputeThinU | ComputeThinV> svd(dense); |
| VERIFY_IS_EQUAL(svd.rank(), 0); |
| VERIFY_IS_EQUAL(K.rank(), svd.rank()); |
| Vec b(1); |
| b << 1.0; |
| VERIFY_IS_EQUAL(K.leastSquaresSolve(b).norm(), 0.0); |
| VERIFY_IS_EQUAL(svd.solve(b).norm(), 0.0); |
| |
| // Boundary: the smallest normal number is the smallest kept singular value. |
| Mat B2(1, 1); |
| B2 << mn; |
| KroneckerOperator<Mat, Mat> K2(A, B2); |
| JacobiSVD<Mat> svd2(reference_kron<double>(A, B2)); |
| VERIFY_IS_EQUAL(svd2.rank(), 1); |
| VERIFY_IS_EQUAL(K2.rank(), svd2.rank()); |
| Vec x2 = K2.leastSquaresSolve(b); |
| VERIFY(x2.allFinite()); |
| VERIFY_IS_APPROX(x2[0], 1.0 / mn); |
| } |
| |
| // solve() must run in a normalized frame; the previous detect-and-retry missed |
| // (a) silent underflow -- the intermediate B^-1 mat(b) flushes to zero, which is |
| // finite, so nothing triggered a retry -- and (b) factors whose raw inverse is |
| // not representable even against a unit right-hand side. |
| void test_kron_solve_normalized() { |
| typedef Matrix<double, Dynamic, 1> Vec; |
| typedef Matrix<double, Dynamic, Dynamic> Mat; |
| |
| // (a) Identity operator, tiny right-hand side: B^-1 * b = 1e-400 used to |
| // underflow silently to zero. |
| Mat A(1, 1), B(1, 1); |
| A << 1e-200; |
| B << 1e200; |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Vec b(1); |
| b << 1e-200; |
| Vec x = K.solve(b); |
| VERIFY_IS_APPROX(x[0], 1e-200); |
| |
| // (b) B^-1 ~ 1e310 is not representable, yet the operator is ~0.01 and the |
| // solution ~100: in the normalized frame every intermediate is bounded by the |
| // conditioning of the factors. |
| Mat A2(1, 1), B2(1, 1); |
| A2 << 1e308; |
| B2 << 1e-310; // subnormal |
| KroneckerOperator<Mat, Mat> K2(A2, B2); |
| Vec b2(1); |
| b2 << 1.0; |
| Vec x2 = K2.solve(b2); |
| VERIFY(x2.allFinite()); |
| VERIFY_IS_APPROX(x2[0], 1.0 / (A2(0, 0) * B2(0, 0))); |
| } |
| |
| // Moderate inputs must be bit-identical to the unnormalized evaluation: partial |
| // pivoting is invariant under the uniform power-of-two normalization and every |
| // substitution step scales exactly with it. |
| template <typename Scalar> |
| void test_kron_solve_bit_identity(Index n1, Index n2) { |
| typedef typename NumTraits<Scalar>::Real RealScalar; |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| // The reference below reproduces the member-internal evaluation, which pins |
| // its workspaces to column-major storage. |
| typedef Matrix<Scalar, Dynamic, Dynamic, ColMajor> CmMat; |
| |
| Mat A = Mat::Random(n1, n1) + RealScalar(2 * n1) * Mat::Identity(n1, n1); |
| Mat B = Mat::Random(n2, n2) + RealScalar(2 * n2) * Mat::Identity(n2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| |
| Mat Bm = Mat::Random(n1 * n2, 2); |
| Mat X = K.solve(Bm); |
| |
| // Raw, unnormalized reference evaluation. |
| PartialPivLU<CmMat> luA(A), luB(B); |
| for (Index k = 0; k < Bm.cols(); ++k) { |
| const Vec bc = Bm.col(k); |
| const Map<const CmMat> Bmat(bc.data(), n2, n1); |
| const CmMat Xref = luA.solve(luB.solve(Bmat).transpose()).transpose(); |
| const Map<const Vec> xref(Xref.data(), Xref.size()); |
| VERIFY_IS_EQUAL(X.col(k).eval(), Vec(xref)); |
| } |
| } |
| |
| template <typename Scalar> |
| void test_kron_eigen(Index n1, Index n2) { |
| typedef typename NumTraits<Scalar>::Real RealScalar; |
| typedef std::complex<RealScalar> Complex; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| typedef Matrix<Complex, Dynamic, Dynamic> CMat; |
| typedef Matrix<Complex, Dynamic, 1> CVec; |
| |
| Mat A = Mat::Random(n1, n1), B = Mat::Random(n2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| CMat dense = reference_kron<Scalar>(A, B).template cast<Complex>(); |
| |
| CVec lambda = K.eigenvalues(); |
| CMat V = K.eigenvectors(); // materializes the Kronecker eigenvector operator |
| VERIFY_IS_APPROX((dense * V).eval(), (V * lambda.asDiagonal()).eval()); |
| } |
| |
| template <typename Scalar> |
| void test_kron_svd(Index m1, Index n1, Index m2, Index n2) { |
| typedef typename NumTraits<Scalar>::Real RealScalar; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| Mat A = Mat::Random(m1, n1), B = Mat::Random(m2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| |
| Matrix<RealScalar, Dynamic, 1> sv = K.singularValues(); |
| Mat U = K.matrixU(), V = K.matrixV(); |
| const Index k = sv.size(); |
| VERIFY_IS_EQUAL(U.cols(), k); |
| VERIFY_IS_EQUAL(V.cols(), k); |
| |
| // Thin SVD in Kronecker order: U and V have orthonormal columns and |
| // U * diag(sv) * V^H reconstructs the matrix. |
| VERIFY_IS_APPROX((U.adjoint() * U).eval(), Mat(Mat::Identity(k, k))); |
| VERIFY_IS_APPROX((V.adjoint() * V).eval(), Mat(Mat::Identity(k, k))); |
| VERIFY_IS_APPROX((U * sv.template cast<Scalar>().asDiagonal() * V.adjoint()).eval(), dense); |
| |
| // The value multiset matches the dense SVD (compare sorted). |
| Matrix<RealScalar, Dynamic, 1> svSorted = sv; |
| std::sort(svSorted.data(), svSorted.data() + svSorted.size(), std::greater<RealScalar>()); |
| JacobiSVD<Mat> svd(dense); |
| VERIFY_IS_APPROX(svSorted, svd.singularValues().head(k).eval()); |
| if (svd.singularValues().size() > k) // rectangular shapes pad with structural zeros |
| VERIFY(svd.singularValues().tail(svd.singularValues().size() - k).norm() <= |
| RealScalar(100) * NumTraits<RealScalar>::epsilon() * svd.singularValues()[0]); |
| } |
| |
| template <typename Scalar> |
| void test_kron_inverse_determinant(Index n1, Index n2) { |
| typedef typename NumTraits<Scalar>::Real RealScalar; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| // Modest diagonal boost: invertible, with the determinant's magnitude tame |
| // enough for a relative comparison in double precision. |
| Mat A = Mat::Random(n1, n1) + RealScalar(2) * Mat::Identity(n1, n1); |
| Mat B = Mat::Random(n2, n2) + RealScalar(2) * Mat::Identity(n2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| |
| Mat inv = K.inverse(); |
| VERIFY_IS_APPROX((inv * dense).eval(), Mat(Mat::Identity(dense.rows(), dense.cols()))); |
| VERIFY_IS_APPROX(K.determinant(), dense.determinant()); |
| } |
| |
| template <typename Scalar> |
| void test_kron_matrix_free_cg(Index n1, Index n2) { |
| typedef typename NumTraits<Scalar>::Real RealScalar; |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| // SPD factors make an SPD Kronecker product. |
| Mat Ar = Mat::Random(n1, n1), Br = Mat::Random(n2, n2); |
| Mat A = Ar * Ar.adjoint() + RealScalar(n1) * Mat::Identity(n1, n1); |
| Mat B = Br * Br.adjoint() + RealScalar(n2) * Mat::Identity(n2, n2); |
| KroneckerOperator<Mat, Mat> K(A, B); |
| Mat dense = reference_kron<Scalar>(A, B); |
| |
| Vec b = Vec::Random(n1 * n2); |
| ConjugateGradient<KroneckerOperator<Mat, Mat>, Lower | Upper, IdentityPreconditioner> cg; |
| cg.compute(K); |
| Vec x = cg.solve(b); |
| VERIFY(cg.info() == Success); |
| VERIFY_IS_APPROX((dense * x).eval(), b); |
| } |
| |
| template <typename Scalar, int M1, int N1, int M2, int N2> |
| void test_kron_fixed() { |
| typedef Matrix<Scalar, M1, N1> MatA; |
| typedef Matrix<Scalar, M2, N2> MatB; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| MatA A = MatA::Random(); |
| MatB B = MatB::Random(); |
| KroneckerOperator<MatA, MatB> K(A, B); |
| STATIC_CHECK((KroneckerOperator<MatA, MatB>::RowsAtCompileTime == M1 * M2)); |
| STATIC_CHECK((KroneckerOperator<MatA, MatB>::ColsAtCompileTime == N1 * N2)); |
| STATIC_CHECK((internal::remove_all_t<decltype(makeKroneckerOperator(A, B))>::RowsAtCompileTime == M1 * M2)); |
| STATIC_CHECK((internal::remove_all_t<decltype(K.transpose())>::RowsAtCompileTime == N1 * N2)); |
| STATIC_CHECK((internal::remove_all_t<decltype(K.transpose())>::ColsAtCompileTime == M1 * M2)); |
| |
| Mat dense = reference_kron<Scalar>(Mat(A), Mat(B)); |
| Matrix<Scalar, M1 * M2, N1 * N2> Kd = K; |
| VERIFY_IS_APPROX(Mat(Kd), dense); |
| |
| Matrix<Scalar, N1 * N2, 1> x = Matrix<Scalar, N1 * N2, 1>::Random(); |
| Matrix<Scalar, M1 * M2, 1> y = K * x; |
| VERIFY_IS_APPROX(y, (dense * x).eval()); |
| |
| // The solvers must propagate the compile-time product dimension instead of |
| // collapsing to Dynamic rows. |
| typedef Matrix<Scalar, M1 * M2, 1> FixedB; |
| STATIC_CHECK((internal::remove_all_t<decltype(std::declval<const KroneckerOperator<MatA, MatB>&>().leastSquaresSolve( |
| std::declval<const FixedB&>()))>::RowsAtCompileTime == N1 * N2)); |
| } |
| |
| // solve()/leastSquaresSolve() on fixed-size square factors: the returned type |
| // carries the fixed row count, and the values match the dense references. |
| template <typename Scalar, int N1, int N2> |
| void test_kron_fixed_solve() { |
| typedef typename NumTraits<Scalar>::Real RealScalar; |
| typedef Matrix<Scalar, N1, N1> MatA; |
| typedef Matrix<Scalar, N2, N2> MatB; |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| MatA A = MatA::Random() + RealScalar(2 * N1) * MatA::Identity(); |
| MatB B = MatB::Random() + RealScalar(2 * N2) * MatB::Identity(); |
| KroneckerOperator<MatA, MatB> K(A, B); |
| Mat dense = reference_kron<Scalar>(Mat(A), Mat(B)); |
| |
| const Matrix<Scalar, N1 * N2, 1> b = Matrix<Scalar, N1 * N2, 1>::Random(); |
| const auto x = K.solve(b); |
| STATIC_CHECK((internal::remove_all_t<decltype(x)>::RowsAtCompileTime == N1 * N2)); |
| VERIFY_IS_APPROX((dense * Vec(x)).eval(), Vec(b)); |
| |
| const auto xls = K.leastSquaresSolve(b); |
| STATIC_CHECK((internal::remove_all_t<decltype(xls)>::RowsAtCompileTime == N1 * N2)); |
| VERIFY_IS_APPROX(Vec(xls), Vec(x)); |
| } |
| |
| // ---- Diagonal and identity factors ----------------------------------------- |
| |
| // Diagonal factors: D (x) B, B (x) D and D1 (x) D2 must match the densified |
| // reference through dense assignment, coefficient access and every product |
| // path, while storing only the diagonals. |
| template <typename Scalar> |
| void test_kron_diag_product(Index n1, Index m2, Index n2) { |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| typedef DiagonalMatrix<Scalar, Dynamic> Diag; |
| |
| const Diag D1(Vec::Random(n1)), D2(Vec::Random(n2)); |
| Mat B = Mat::Random(m2, n2); |
| const Mat D1d = D1.toDenseMatrix(), D2d = D2.toDenseMatrix(); |
| |
| KroneckerOperator<Diag, Mat> KL(D1, B); // D1 (x) B |
| KroneckerOperator<Mat, Diag> KR(B, D2); // B (x) D2 |
| KroneckerOperator<Diag, Diag> KD(D1, D2); // D1 (x) D2 |
| |
| Mat refL = reference_kron<Scalar>(D1d, B); |
| Mat refR = reference_kron<Scalar>(B, D2d); |
| Mat refD = reference_kron<Scalar>(D1d, D2d); |
| |
| // Dense assignment through evalTo (a diagonal factor writes zero blocks off |
| // its diagonal) and coefficient access. |
| VERIFY_IS_APPROX(Mat(KL), refL); |
| VERIFY_IS_APPROX(Mat(KR), refR); |
| VERIFY_IS_APPROX(Mat(KD), refD); |
| for (Index t = 0; t < 5; ++t) { |
| Index i = internal::random<Index>(0, KL.rows() - 1), j = internal::random<Index>(0, KL.cols() - 1); |
| VERIFY_IS_APPROX(KL.coeff(i, j), refL(i, j)); |
| i = internal::random<Index>(0, KD.rows() - 1), j = internal::random<Index>(0, KD.cols() - 1); |
| VERIFY_IS_APPROX(KD.coeff(i, j), refD(i, j)); |
| } |
| |
| // Dense accumulation through addTo/subTo (a diagonal factor accumulates only |
| // the block diagonals). |
| Mat acc = Mat::Random(refL.rows(), refL.cols()); |
| const Mat acc0 = acc; |
| acc += KL; |
| VERIFY_IS_APPROX(acc, (acc0 + refL).eval()); |
| acc -= KL; |
| VERIFY_IS_APPROX(acc, acc0); |
| |
| // Vector, matrix and accumulated products via the vec identity. |
| Vec x = Vec::Random(refL.cols()); |
| VERIFY_IS_APPROX((KL * x).eval(), (refL * x).eval()); |
| Mat X = Mat::Random(refL.cols(), 3); |
| VERIFY_IS_APPROX((KL * X).eval(), (refL * X).eval()); |
| Vec y = Vec::Random(refL.rows()); |
| Vec y0 = y; |
| y.noalias() += KL * x; |
| VERIFY_IS_APPROX(y, (y0 + refL * x).eval()); |
| |
| Vec xr = Vec::Random(refR.cols()); |
| VERIFY_IS_APPROX((KR * xr).eval(), (refR * xr).eval()); |
| Vec xd = Vec::Random(refD.cols()); |
| VERIFY_IS_APPROX((KD * xd).eval(), (refD * xd).eval()); |
| |
| // Factory deduction from a diagonal expression maps to an owning |
| // DiagonalMatrix factor. |
| auto KF = makeKroneckerOperator(D1.diagonal().asDiagonal(), B); |
| STATIC_CHECK((std::is_same<decltype(KF), KroneckerOperator<Diag, Mat>>::value)); |
| VERIFY_IS_APPROX((KF * x).eval(), (refL * x).eval()); |
| } |
| |
| // Identity factors, the I (x) A and A (x) I operators of finite-difference |
| // discretizations: a unit-diagonal factor stores O(p) data and applies as a |
| // no-op scaling instead of a GEMM. Checked against the dense reference and |
| // against the vec identity (I (x) A) vec(X) = vec(A X) directly. |
| template <typename Scalar> |
| void test_kron_identity(Index p, Index m2, Index n2) { |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| typedef Matrix<Scalar, Dynamic, Dynamic, ColMajor> CmMat; // reshape frame of the vec identity |
| typedef DiagonalMatrix<Scalar, Dynamic> Diag; |
| |
| Mat A = Mat::Random(m2, n2); |
| auto KI = makeKroneckerOperator(Vec::Ones(p).asDiagonal(), A); // I_p (x) A |
| auto IK = makeKroneckerOperator(A, Vec::Ones(p).asDiagonal()); // A (x) I_p |
| STATIC_CHECK((std::is_same<decltype(KI), KroneckerOperator<Diag, Mat>>::value)); |
| STATIC_CHECK((std::is_same<decltype(IK), KroneckerOperator<Mat, Diag>>::value)); |
| |
| const Mat Ip = Mat::Identity(p, p); |
| Mat refI = reference_kron<Scalar>(Ip, A); |
| Mat refK = reference_kron<Scalar>(A, Ip); |
| |
| Vec x = Vec::Random(p * n2); |
| VERIFY_IS_APPROX((KI * x).eval(), (refI * x).eval()); |
| VERIFY_IS_APPROX((IK * x).eval(), (refK * x).eval()); |
| |
| // (I_p (x) A) vec(X) = vec(A X) with X of size n2 x p: block-wise application |
| // of A to each length-n2 segment. |
| const Map<const CmMat> Xmat(x.data(), n2, p); |
| CmMat Y = A * Xmat; |
| VERIFY_IS_APPROX((KI * x).eval(), Vec(Map<const Vec>(Y.data(), Y.size()))); |
| } |
| |
| // The transposition family of a diagonal factor stays diagonal, in type and in |
| // value. |
| template <typename Scalar> |
| void test_kron_diag_transpose(Index n1, Index m2, Index n2) { |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| typedef DiagonalMatrix<Scalar, Dynamic> Diag; |
| |
| const Diag D(Vec::Random(n1)); |
| Mat B = Mat::Random(m2, n2); |
| KroneckerOperator<Diag, Mat> K(D, B); |
| Mat dense = reference_kron<Scalar>(D.toDenseMatrix(), B); |
| |
| STATIC_CHECK((std::is_same<typename internal::remove_all_t<decltype(K.transpose())>, |
| KroneckerOperator<Diag, Matrix<Scalar, Dynamic, Dynamic>>>::value)); |
| STATIC_CHECK((std::is_same<typename internal::remove_all_t<decltype(K.adjoint())>, |
| KroneckerOperator<Diag, Matrix<Scalar, Dynamic, Dynamic>>>::value)); |
| STATIC_CHECK( |
| (std::is_same<typename internal::remove_all_t<decltype(K.conjugate())>, KroneckerOperator<Diag, Mat>>::value)); |
| |
| Mat Td = K.transpose(); |
| VERIFY_IS_APPROX(Td, Mat(dense.transpose())); |
| Mat Ad = K.adjoint(); |
| VERIFY_IS_APPROX(Ad, Mat(dense.adjoint())); |
| Mat Cd = K.conjugate(); |
| VERIFY_IS_APPROX(Cd, Mat(dense.conjugate())); |
| |
| Vec y = Vec::Random(dense.rows()); |
| VERIFY_IS_APPROX((K.transpose() * y).eval(), (dense.transpose() * y).eval()); |
| VERIFY_IS_APPROX((K.adjoint() * y).eval(), (dense.adjoint() * y).eval()); |
| } |
| |
| // solve() through diagonal factors: a diagonal side is an entrywise division |
| // instead of an LU substitution. All three factor-kind mixes against the dense |
| // LU solve. |
| template <typename Scalar> |
| void test_kron_diag_solve(Index n1, Index n2) { |
| typedef typename NumTraits<Scalar>::Real RealScalar; |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| typedef DiagonalMatrix<Scalar, Dynamic> Diag; |
| |
| // Diagonal entries bounded away from zero and diagonally boosted dense |
| // factors keep everything well conditioned. |
| const Diag D1(Vec::Random(n1) + Vec::Constant(n1, Scalar(2))); |
| const Diag D2(Vec::Random(n2) + Vec::Constant(n2, Scalar(2))); |
| Mat A = Mat::Random(n1, n1) + RealScalar(2 * n1) * Mat::Identity(n1, n1); |
| Mat B = Mat::Random(n2, n2) + RealScalar(2 * n2) * Mat::Identity(n2, n2); |
| |
| KroneckerOperator<Diag, Mat> KL(D1, B); |
| KroneckerOperator<Mat, Diag> KR(A, D2); |
| KroneckerOperator<Diag, Diag> KD(D1, D2); |
| |
| Mat denseL = reference_kron<Scalar>(D1.toDenseMatrix(), B); |
| Mat denseR = reference_kron<Scalar>(A, D2.toDenseMatrix()); |
| Mat denseD = reference_kron<Scalar>(D1.toDenseMatrix(), D2.toDenseMatrix()); |
| |
| Vec b = Vec::Random(n1 * n2); |
| VERIFY_IS_APPROX((denseL * KL.solve(b)).eval(), b); |
| VERIFY_IS_APPROX(KL.solve(b), denseL.partialPivLu().solve(b).eval()); |
| VERIFY_IS_APPROX(KR.solve(b), denseR.partialPivLu().solve(b).eval()); |
| VERIFY_IS_APPROX(KD.solve(b), denseD.partialPivLu().solve(b).eval()); |
| |
| Mat Bm = Mat::Random(n1 * n2, 3); |
| VERIFY_IS_APPROX(KL.solve(Bm), denseL.partialPivLu().solve(Bm).eval()); |
| VERIFY_IS_APPROX(KR.solve(Bm), denseR.partialPivLu().solve(Bm).eval()); |
| } |
| |
| // inverse() of a diagonal factor stays diagonal (entrywise reciprocals); |
| // determinant() accumulates directly from the diagonal, skipping the LU. |
| template <typename Scalar> |
| void test_kron_diag_inverse_determinant(Index n1, Index n2) { |
| typedef typename NumTraits<Scalar>::Real RealScalar; |
| typedef Matrix<Scalar, Dynamic, 1> Vec; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| typedef DiagonalMatrix<Scalar, Dynamic> Diag; |
| |
| const Diag D(Vec::Random(n1) + Vec::Constant(n1, Scalar(2))); |
| Mat B = Mat::Random(n2, n2) + RealScalar(2) * Mat::Identity(n2, n2); |
| KroneckerOperator<Diag, Mat> K(D, B); |
| Mat dense = reference_kron<Scalar>(D.toDenseMatrix(), B); |
| |
| auto Kinv = K.inverse(); |
| STATIC_CHECK((std::is_same<typename internal::remove_all_t<decltype(Kinv)>, |
| KroneckerOperator<Diag, Matrix<Scalar, Dynamic, Dynamic, ColMajor>>>::value)); |
| Mat inv = Kinv; |
| VERIFY_IS_APPROX((inv * dense).eval(), Mat(Mat::Identity(dense.rows(), dense.cols()))); |
| VERIFY_IS_APPROX(K.determinant(), dense.determinant()); |
| |
| const Diag D2(Vec::Random(n2) + Vec::Constant(n2, Scalar(2))); |
| KroneckerOperator<Diag, Diag> KD(D, D2); |
| Mat denseD = reference_kron<Scalar>(D.toDenseMatrix(), D2.toDenseMatrix()); |
| VERIFY_IS_APPROX(KD.determinant(), denseD.determinant()); |
| Mat invD = KD.inverse(); |
| VERIFY_IS_APPROX((invD * denseD).eval(), Mat(Mat::Identity(denseD.rows(), denseD.cols()))); |
| } |
| |
| // The decomposition family on diagonal factors (materialized densely for the |
| // factor decompositions): eigen-residual, SVD reconstruction, rank and |
| // minimum-norm least squares against the dense references. |
| template <typename Scalar> |
| void test_kron_diag_decompositions(Index n1, Index n2) { |
| 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; |
| typedef Matrix<Complex, Dynamic, 1> CVec; |
| typedef DiagonalMatrix<Scalar, Dynamic> Diag; |
| |
| const Diag D(Vec::Random(n1)); |
| Mat B = Mat::Random(n2, n2); |
| KroneckerOperator<Diag, Mat> K(D, B); |
| Mat dense = reference_kron<Scalar>(D.toDenseMatrix(), B); |
| |
| CVec lambda = K.eigenvalues(); |
| CMat V = K.eigenvectors(); |
| VERIFY_IS_APPROX((dense.template cast<Complex>() * V).eval(), (V * lambda.asDiagonal()).eval()); |
| |
| Matrix<RealScalar, Dynamic, 1> sv = K.singularValues(); |
| Mat U = K.matrixU(), W = K.matrixV(); |
| VERIFY_IS_APPROX((U * sv.template cast<Scalar>().asDiagonal() * W.adjoint()).eval(), dense); |
| |
| VERIFY_IS_EQUAL(K.rank(), dense.completeOrthogonalDecomposition().rank()); |
| Vec b = Vec::Random(n1 * n2); |
| VERIFY_IS_APPROX(K.leastSquaresSolve(b), dense.completeOrthogonalDecomposition().solve(b).eval()); |
| } |
| |
| // The overflow-hardening contract must hold through diagonal factors: products |
| // and solves with extreme diagonal magnitudes stay finite whenever the true |
| // result is representable, and the balanced determinant survives partial |
| // products that overflow on their own. |
| void test_kron_diag_extreme_scale() { |
| typedef Matrix<double, Dynamic, 1> Vec; |
| typedef Matrix<double, Dynamic, Dynamic> Mat; |
| typedef DiagonalMatrix<double, Dynamic> Diag; |
| |
| // diag(1e-200) (x) [1e200] is the identity; the unprotected application |
| // B X = 1e400 would overflow. |
| Diag D(Vec::Constant(1, 1e-200)); |
| Mat B(1, 1); |
| B << 1e200; |
| KroneckerOperator<Diag, Mat> K(D, B); |
| Vec x(1); |
| x << 1e200; |
| Vec y = K * x; |
| VERIFY(y.allFinite()); |
| VERIFY_IS_APPROX(y[0], 1e200); |
| |
| // Same magnitudes through solve(), diagonal on the divided side. |
| KroneckerOperator<Mat, Diag> Ks(B, D); |
| Vec b(1); |
| b << 1e200; |
| Vec xs = Ks.solve(b); |
| VERIFY(xs.allFinite()); |
| VERIFY_IS_APPROX(xs[0], 1e200); |
| |
| // Identity built from extreme diagonal scales: products and the entrywise |
| // divisions of the diagonal-diagonal solve run in the normalized frame. |
| Diag Da(Vec::Constant(2, 1e-200)), Db(Vec::Constant(3, 1e200)); |
| KroneckerOperator<Diag, Diag> K2(Da, Db); |
| Vec x2 = 1e200 * Vec::Random(6); |
| Vec y2 = K2 * x2; |
| VERIFY(y2.allFinite()); |
| VERIFY_IS_APPROX(y2, x2); |
| Vec b2 = 1e-200 * Vec::Random(6); |
| Vec xd = K2.solve(b2); |
| VERIFY(xd.allFinite()); |
| VERIFY_IS_APPROX(xd, b2); |
| |
| // det(diag(1e200))^2 = 1e400 overflows on its own, yet the determinant of |
| // diag(1e200) (x) diag(1e-100, 1e-100) is 1e200 -- and the diagonal path |
| // must balance exactly like the LU path. |
| Diag A1(Vec::Constant(1, 1e200)), B1(Vec::Constant(2, 1e-100)); |
| KroneckerOperator<Diag, Diag> Kd(A1, B1); |
| VERIFY_IS_APPROX(Kd.determinant(), 1e200); |
| } |
| |
| // Mixed-scalar promotion through a diagonal factor: a real operator applied to |
| // a complex right-hand side runs in the promoted scalar. |
| template <typename RealScalar> |
| void test_kron_diag_mixed_scalar(Index n1, Index m2, Index n2) { |
| typedef std::complex<RealScalar> Complex; |
| typedef Matrix<RealScalar, Dynamic, 1> RVec; |
| typedef Matrix<RealScalar, Dynamic, Dynamic> RMat; |
| typedef Matrix<Complex, Dynamic, 1> CVec; |
| typedef Matrix<Complex, Dynamic, Dynamic> CMat; |
| typedef DiagonalMatrix<RealScalar, Dynamic> RDiag; |
| |
| const RDiag D(RVec::Random(n1)); |
| RMat B = RMat::Random(m2, n2); |
| KroneckerOperator<RDiag, RMat> K(D, B); |
| CMat dense = reference_kron<RealScalar>(D.toDenseMatrix(), B).template cast<Complex>(); |
| |
| CVec x = CVec::Random(n1 * n2); |
| CVec y = K * x; |
| VERIFY_IS_APPROX(y, (dense * x).eval()); |
| CVec y0 = CVec::Random(n1 * m2); |
| y = y0; |
| y.noalias() += K * x; |
| VERIFY_IS_APPROX(y, (y0 + dense * x).eval()); |
| } |
| |
| // Fixed-size diagonal factors propagate compile-time dimensions. |
| template <typename Scalar, int N1, int M2, int N2> |
| void test_kron_diag_fixed() { |
| typedef DiagonalMatrix<Scalar, N1> Diag; |
| typedef Matrix<Scalar, M2, N2> MatB; |
| typedef Matrix<Scalar, Dynamic, Dynamic> Mat; |
| |
| const Diag D(Matrix<Scalar, N1, 1>::Random()); |
| MatB B = MatB::Random(); |
| KroneckerOperator<Diag, MatB> K(D, B); |
| STATIC_CHECK((KroneckerOperator<Diag, MatB>::RowsAtCompileTime == N1 * M2)); |
| STATIC_CHECK((KroneckerOperator<Diag, MatB>::ColsAtCompileTime == N1 * N2)); |
| |
| Mat dense = reference_kron<Scalar>(Mat(D.toDenseMatrix()), Mat(B)); |
| Matrix<Scalar, N1 * M2, N1 * N2> Kd = K; |
| VERIFY_IS_APPROX(Mat(Kd), dense); |
| |
| Matrix<Scalar, N1 * N2, 1> x = Matrix<Scalar, N1 * N2, 1>::Random(); |
| Matrix<Scalar, N1 * M2, 1> y = K * x; |
| VERIFY_IS_APPROX(y, (dense * x).eval()); |
| } |
| |
| // Matrix-free CG through a diagonal-factor operator: a positive diagonal |
| // tensored with an SPD dense factor is SPD. |
| void test_kron_diag_matrix_free_cg(Index n1, Index n2) { |
| typedef Matrix<double, Dynamic, 1> Vec; |
| typedef Matrix<double, Dynamic, Dynamic> Mat; |
| typedef DiagonalMatrix<double, Dynamic> Diag; |
| |
| const Diag D(Vec::Random(n1).cwiseAbs() + Vec::Constant(n1, 1.0)); |
| Mat Br = Mat::Random(n2, n2); |
| Mat B = Br * Br.adjoint() + double(n2) * Mat::Identity(n2, n2); |
| KroneckerOperator<Diag, Mat> K(D, B); |
| Mat dense = reference_kron<double>(D.toDenseMatrix(), B); |
| |
| Vec b = Vec::Random(n1 * n2); |
| ConjugateGradient<KroneckerOperator<Diag, Mat>, Lower | Upper, IdentityPreconditioner> cg; |
| cg.compute(K); |
| Vec x = cg.solve(b); |
| VERIFY(cg.info() == Success); |
| VERIFY_IS_APPROX((dense * x).eval(), b); |
| } |
| |
| EIGEN_DECLARE_TEST(structured_kronecker) { |
| for (int i = 0; i < g_repeat; ++i) { |
| // Products, dense assignment, coefficient access across factor shapes. |
| CALL_SUBTEST_1((test_kron_product<double>(1, 1, 1, 1))); |
| CALL_SUBTEST_1((test_kron_product<double>(2, 3, 3, 2))); |
| CALL_SUBTEST_1((test_kron_product<double>(4, 4, 5, 5))); |
| CALL_SUBTEST_1((test_kron_product<double>(8, 3, 2, 7))); |
| CALL_SUBTEST_1((test_kron_product<double>(1, 6, 7, 1))); // vector-shaped factors |
| CALL_SUBTEST_1((test_kron_product<float>(5, 4, 4, 5))); |
| CALL_SUBTEST_1((test_kron_product<std::complex<double>>(3, 5, 4, 2))); |
| CALL_SUBTEST_1((test_kron_product<std::complex<float>>(4, 3, 3, 4))); |
| |
| // Transposition family. |
| CALL_SUBTEST_2((test_kron_transpose<double>(4, 6, 5, 3))); |
| CALL_SUBTEST_2((test_kron_transpose<std::complex<double>>(3, 4, 5, 2))); |
| |
| // Direct solves through factor LU. |
| CALL_SUBTEST_3((test_kron_solve<double>(1, 1))); |
| CALL_SUBTEST_3((test_kron_solve<double>(4, 7))); |
| CALL_SUBTEST_3((test_kron_solve<double>(9, 5))); |
| CALL_SUBTEST_3((test_kron_solve<float>(5, 4))); |
| CALL_SUBTEST_3((test_kron_solve<std::complex<double>>(6, 4))); |
| CALL_SUBTEST_3(test_kron_solve_normalized()); |
| CALL_SUBTEST_3((test_kron_solve_bit_identity<double>(4, 3))); |
| CALL_SUBTEST_3((test_kron_solve_bit_identity<std::complex<double>>(3, 4))); |
| |
| // Minimum-norm least squares through factor pseudo-inverses. |
| CALL_SUBTEST_3((test_kron_least_squares<double>(8, 5, 7, 4))); // tall x tall |
| CALL_SUBTEST_3((test_kron_least_squares<double>(8, 5, 3, 6))); // tall x wide |
| CALL_SUBTEST_3((test_kron_least_squares<std::complex<double>>(6, 4, 5, 3))); |
| CALL_SUBTEST_3((test_kron_least_squares_rank_deficient<double>(6, 4, 5, 3))); |
| CALL_SUBTEST_3((test_kron_least_squares_rank_deficient<std::complex<double>>(5, 3, 4, 4))); |
| |
| // Eigendecomposition, SVD, inverse, determinant. |
| CALL_SUBTEST_4((test_kron_eigen<double>(4, 5))); |
| CALL_SUBTEST_4((test_kron_eigen<double>(1, 6))); |
| CALL_SUBTEST_4((test_kron_eigen<std::complex<double>>(3, 4))); |
| CALL_SUBTEST_4((test_kron_svd<double>(4, 4, 5, 5))); |
| CALL_SUBTEST_4((test_kron_svd<double>(6, 4, 3, 5))); // rectangular: structural zeros |
| CALL_SUBTEST_4((test_kron_svd<std::complex<double>>(4, 3, 4, 4))); |
| CALL_SUBTEST_4((test_kron_svd<float>(4, 4, 4, 4))); |
| CALL_SUBTEST_4((test_kron_inverse_determinant<double>(4, 5))); |
| CALL_SUBTEST_4((test_kron_inverse_determinant<std::complex<double>>(3, 4))); |
| |
| // Matrix-free iterative solve and fixed-size factors. |
| CALL_SUBTEST_5((test_kron_matrix_free_cg<double>(6, 7))); |
| CALL_SUBTEST_5((test_kron_fixed<double, 3, 4, 2, 5>())); |
| CALL_SUBTEST_5((test_kron_fixed<std::complex<float>, 2, 3, 3, 2>())); |
| CALL_SUBTEST_5((test_kron_fixed_solve<double, 2, 3>())); |
| CALL_SUBTEST_5((test_kron_fixed_solve<std::complex<double>, 3, 2>())); |
| |
| // Lifetime and aliasing of the product expression, mixed-scalar promotion. |
| CALL_SUBTEST_6((test_kron_aliased_product<double>(3, 4))); |
| CALL_SUBTEST_6((test_kron_aliased_product<std::complex<double>>(4, 3))); |
| CALL_SUBTEST_6((test_kron_aliased_resize<double>(4, 3, 2, 5))); |
| CALL_SUBTEST_6((test_kron_aliased_resize<std::complex<double>>(2, 4, 5, 3))); |
| CALL_SUBTEST_6((test_kron_delayed_product<double>(4, 3, 2, 5))); |
| CALL_SUBTEST_6((test_kron_delayed_product<std::complex<float>>(3, 3, 4, 2))); |
| CALL_SUBTEST_6((test_kron_mixed_scalar<double>(4, 3, 3, 5))); |
| CALL_SUBTEST_6((test_kron_mixed_scalar<float>(3, 4, 2, 3))); |
| |
| // Numerical boundaries: product-level rank truncation, balanced determinant, |
| // overflow-safe product scaling and ratio-space rank thresholds. |
| CALL_SUBTEST_7(test_kron_product_level_rank()); |
| CALL_SUBTEST_7(test_kron_determinant_scaling()); |
| CALL_SUBTEST_7(test_kron_extreme_scale_product()); |
| CALL_SUBTEST_7(test_kron_extreme_scale_complex()); |
| CALL_SUBTEST_7(test_kron_rank_ratio_threshold()); |
| CALL_SUBTEST_7(test_kron_rank_min_normal_clamp()); |
| |
| // Diagonal and identity factors: products, transposition family, |
| // mixed-scalar promotion, fixed sizes. |
| CALL_SUBTEST_8((test_kron_diag_product<double>(4, 5, 3))); |
| CALL_SUBTEST_8((test_kron_diag_product<float>(3, 4, 4))); |
| CALL_SUBTEST_8((test_kron_diag_product<std::complex<double>>(3, 4, 2))); |
| CALL_SUBTEST_8((test_kron_identity<double>(5, 4, 3))); |
| CALL_SUBTEST_8((test_kron_identity<std::complex<double>>(3, 3, 4))); |
| CALL_SUBTEST_8((test_kron_diag_transpose<double>(4, 5, 3))); |
| CALL_SUBTEST_8((test_kron_diag_transpose<std::complex<double>>(3, 4, 5))); |
| CALL_SUBTEST_8((test_kron_diag_mixed_scalar<double>(4, 3, 5))); |
| CALL_SUBTEST_8((test_kron_diag_mixed_scalar<float>(3, 4, 2))); |
| CALL_SUBTEST_8((test_kron_diag_fixed<double, 3, 2, 4>())); |
| CALL_SUBTEST_8((test_kron_diag_fixed<std::complex<float>, 2, 3, 3>())); |
| |
| // Diagonal factors through the solvers, inverse/determinant, the |
| // decomposition family, extreme scales and matrix-free CG. |
| CALL_SUBTEST_9((test_kron_diag_solve<double>(4, 7))); |
| CALL_SUBTEST_9((test_kron_diag_solve<float>(5, 4))); |
| CALL_SUBTEST_9((test_kron_diag_solve<std::complex<double>>(6, 4))); |
| CALL_SUBTEST_9((test_kron_diag_inverse_determinant<double>(4, 5))); |
| CALL_SUBTEST_9((test_kron_diag_inverse_determinant<std::complex<double>>(3, 4))); |
| CALL_SUBTEST_9((test_kron_diag_decompositions<double>(4, 5))); |
| CALL_SUBTEST_9((test_kron_diag_decompositions<std::complex<double>>(3, 4))); |
| CALL_SUBTEST_9(test_kron_diag_extreme_scale()); |
| CALL_SUBTEST_9(test_kron_diag_matrix_free_cg(6, 7)); |
| } |
| } |