blob: be59af970be1f7f1899b50af492bbdc6e05a9524 [file] [edit]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-FileCopyrightText: The Eigen Authors
// SPDX-License-Identifier: MPL-2.0
#include "main.h"
#include "fp_control.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());
}
// Products and solves stack right-hand sides in chunks sized by the L2 cache
// size. A tiny L2 forces chunks of one or two columns with a short last chunk,
// and row-major operands are read and written through strided columns.
template <typename Lhs, typename Rhs>
void check_kron_chunked_product(const KroneckerOperator<Lhs, Rhs>& K) {
using Scalar = typename Lhs::Scalar;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using RowMat = Matrix<Scalar, Dynamic, Dynamic, RowMajor>;
const Mat dense = K;
const RowMat X = RowMat::Random(K.cols(), 5), Y0 = RowMat::Random(K.rows(), 5);
const RowMat expected = dense * X;
RowMat Y = K * X;
VERIFY_IS_APPROX(Y, expected);
Y = Y0;
Y.noalias() += K * X;
VERIFY_IS_APPROX(Y, (Y0 + expected).eval());
Y = Y0;
Y.noalias() -= K * X;
VERIFY_IS_APPROX(Y, (Y0 - expected).eval());
VERIFY_IS_APPROX(Mat(K * Mat(X)), Mat(expected));
}
template <typename Scalar>
void test_kron_chunked_products() {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
using Diag = DiagonalMatrix<Scalar, Dynamic>;
const std::ptrdiff_t l1 = l1CacheSize(), l2 = l2CacheSize(), l3 = l3CacheSize();
// A budget of 32 stacked entries: 2 columns of the 16-entry workspace of
// 3x2 (x) 2x3, chunks of 2, 2, 1 for 5 right-hand sides.
setCpuCacheSizes(l1, std::ptrdiff_t(8 * 32 * sizeof(Scalar)), l3);
const Mat A = Mat::Random(3, 2), B = Mat::Random(2, 3);
const Sparse As = A.sparseView(), Bs = B.sparseView();
check_kron_chunked_product(makeKroneckerOperator(A, B));
check_kron_chunked_product(makeKroneckerOperator(As, Bs));
check_kron_chunked_product(makeKroneckerOperator(A, Diag(Vec::Random(3))));
check_kron_chunked_product(makeKroneckerOperator(Diag(Vec::Random(2)), Bs));
check_kron_chunked_product(makeKroneckerOperator(Mat::Identity(2, 2), Bs));
check_kron_chunked_product(makeKroneckerOperator(A, Mat::Identity(3, 3)));
check_kron_chunked_product(makeKroneckerOperator(Mat::Identity(2, 3), B));
check_kron_chunked_product(makeKroneckerOperator(Mat::Identity(2, 2), Mat::Identity(2, 2)));
check_kron_chunked_product(makeKroneckerOperator(Mat::Random(1, 2), As, Mat::Random(2, 1)));
setCpuCacheSizes(l1, l2, l3);
}
template <typename Scalar>
void test_kron_chunked_solves() {
using RealScalar = typename NumTraits<Scalar>::Real;
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using RowMat = Matrix<Scalar, Dynamic, Dynamic, RowMajor>;
using Sparse = SparseMatrix<Scalar>;
using Diag = DiagonalMatrix<Scalar, Dynamic>;
const std::ptrdiff_t l1 = l1CacheSize(), l2 = l2CacheSize(), l3 = l3CacheSize();
// A budget of 32 stacked entries: 2 columns of the 16-entry workspace of
// 3x2 (x) 2x3, chunks of 2, 2, 1 for 5 right-hand sides.
setCpuCacheSizes(l1, std::ptrdiff_t(8 * 32 * sizeof(Scalar)), l3);
const RowMat b = RowMat::Random(4, 5);
const Mat Ad = Mat::Random(2, 2) + RealScalar(4) * Mat::Identity(2, 2);
const Mat Bd = Mat::Random(2, 2) + RealScalar(4) * Mat::Identity(2, 2);
const KroneckerOperator<Mat, Mat> Ks(Ad, Bd);
const Mat x = Mat(Ks).partialPivLu().solve(Mat(b));
VERIFY_IS_APPROX(Ks.solve(b), x);
const KroneckerOperator<Sparse, Mat> Kss(Ad.sparseView(), Bd);
VERIFY_IS_APPROX(Kss.solve(b), x);
const KroneckerOperator<Mat, Mat> Kl(Mat::Random(2, 1), Mat::Random(2, 2));
VERIFY_IS_APPROX(Kl.leastSquaresSolve(b), Mat(Mat(Kl).completeOrthogonalDecomposition().solve(Mat(b))));
// A diagonal factor solves by entrywise division of the stacked right-hand sides.
const Vec d = Vec::Random(2) + Vec::Constant(2, Scalar(4));
const KroneckerOperator<Diag, Mat> Kdl(Diag(d), Bd);
VERIFY_IS_APPROX(Kdl.solve(b), Mat(Mat(Kdl).partialPivLu().solve(Mat(b))));
const KroneckerOperator<Mat, Diag> Kdr(Ad, Diag(d));
VERIFY_IS_APPROX(Kdr.solve(b), Mat(Mat(Kdr).partialPivLu().solve(Mat(b))));
const auto Kil = makeKroneckerOperator(Mat::Identity(2, 2), Bd);
VERIFY_IS_APPROX(Kil.solve(b), Mat(Mat(Kil).partialPivLu().solve(Mat(b))));
const auto Kn = makeKroneckerOperator(Ad, Bd, Mat::Identity(1, 1));
VERIFY_IS_APPROX(Kn.solve(b), x);
setCpuCacheSizes(l1, l2, l3);
}
// A real operator applied to complex right-hand sides, in several chunks.
template <typename RealScalar>
void test_kron_chunked_mixed_scalar() {
using Complex = std::complex<RealScalar>;
using RMat = Matrix<RealScalar, Dynamic, Dynamic>;
using CMat = Matrix<Complex, Dynamic, Dynamic>;
using CRowMat = Matrix<Complex, Dynamic, Dynamic, RowMajor>;
const std::ptrdiff_t l1 = l1CacheSize(), l2 = l2CacheSize(), l3 = l3CacheSize();
setCpuCacheSizes(l1, std::ptrdiff_t(8 * 32 * sizeof(Complex)), l3);
const KroneckerOperator<RMat, RMat> K(RMat::Random(3, 2), RMat::Random(2, 3));
const CMat dense = RMat(K).template cast<Complex>();
const CRowMat X = CRowMat::Random(K.cols(), 5);
CRowMat Y = K * X;
VERIFY_IS_APPROX(Y, CRowMat(dense * X));
const CRowMat Y0 = CRowMat::Random(K.rows(), 5);
Y = Y0;
Y.noalias() += K * X;
VERIFY_IS_APPROX(Y, CRowMat(Y0 + dense * X));
setCpuCacheSizes(l1, l2, l3);
}
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, 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.
Mat b = Mat::Random(m1 * m2, 3);
Mat 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());
Mat b = Mat::Random(m1 * m2, 3);
Mat x = K.leastSquaresSolve(b);
VERIFY_IS_APPROX(x, dense.completeOrthogonalDecomposition().solve(b).eval());
}
// leastSquaresSolve() decomposes each factor scaled by an exact power of two,
// so scaling a factor by 2^s scales the solution by exactly 2^-s -- including
// for s where the unscaled pivoted QR's squared column norms overflow
// (2^(2s) > max) or underflow. A non-finite factor solves to NaN
// instead of being ranked out by the pivoted QR.
template <typename Scalar>
void test_kron_least_squares_scaling(Index m1, Index n1, Index m2, Index n2) {
typedef Matrix<Scalar, Dynamic, Dynamic> Mat;
typedef typename NumTraits<Scalar>::Real RealScalar;
const int maxExp = std::numeric_limits<RealScalar>::max_exponent;
Mat A = Mat::Random(m1, n1), B = Mat::Random(m2, n2), b = Mat::Random(m1 * m2, 3);
const Mat x0 = KroneckerOperator<Mat, Mat>(A, B).leastSquaresSolve(b);
for (int s : {maxExp / 2 + 8, -(maxExp / 2 + 12)}) {
const RealScalar up = std::ldexp(RealScalar(1), s), down = std::ldexp(RealScalar(1), -s);
const Mat xa = KroneckerOperator<Mat, Mat>(A * up, B).leastSquaresSolve(b);
const Mat xb = KroneckerOperator<Mat, Mat>(A, B * down).leastSquaresSolve(b);
VERIFY((xa * up).eval() == x0);
VERIFY((xb * down).eval() == x0);
}
Mat Ainf = A, Bnan = B;
Ainf(m1 - 1, 0) = Scalar(std::numeric_limits<RealScalar>::infinity());
Bnan(0, n2 - 1) = Scalar(std::numeric_limits<RealScalar>::quiet_NaN());
const Mat xinf = KroneckerOperator<Mat, Mat>(Ainf, B).leastSquaresSolve(b);
const Mat xnan = KroneckerOperator<Mat, Mat>(A, Bnan).leastSquaresSolve(b);
VERIFY(xinf.array().isNaN().all());
VERIFY(xnan.array().isNaN().all());
}
// rank() judges near-rank-deficiency 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),
// so rank() is 3 -- matching the dense complete orthogonal decomposition.
// leastSquaresSolve() decides rank per factor instead (it never forms the
// pairwise products), so it applies A^+ (x) B^+ with both 1e-8 modes inverted.
void test_kron_product_level_rank() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef Matrix<double, Dynamic, Dynamic> Mat;
const double small = 1e-8;
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);
VERIFY_IS_EQUAL(K.rank(), 3);
VERIFY_IS_EQUAL(K.rank(), CompleteOrthogonalDecomposition<Mat>(dense).rank());
Mat Ainv(2, 2);
Ainv << 1, 0, 0, 1 / small;
Vec b = Vec::Random(4);
Vec x = K.leastSquaresSolve(b);
VERIFY_IS_APPROX(x, (reference_kron<double>(Ainv, Ainv) * b).eval());
}
// 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: 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, but without the clamp the structured rank was 1. 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(svd.solve(b).norm(), 0.0);
// leastSquaresSolve has no singular values to clamp: the factor is full rank
// against its own scale, and 1 / (DBL_MIN / 2) is representable.
Vec x = K.leastSquaresSolve(b);
VERIFY(x.allFinite());
VERIFY_IS_APPROX(x[0], 2 / mn);
// 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);
}
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>
struct DiagKronecker {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Diag = DiagonalMatrix<Scalar, Dynamic>;
DiagKronecker(Index n1, Index n2)
: D(Vec::Random(n1)), B(Mat::Random(n2, n2)), K(D, B), dense(reference_kron<Scalar>(D.toDenseMatrix(), B)) {}
Diag D;
Mat B;
KroneckerOperator<Diag, Mat> K;
Mat dense;
};
// The decomposition family on a diagonal factor, one test per decomposition to
// keep each part's compile small.
template <typename Scalar>
void test_kron_diag_eigen(Index n1, Index n2) {
using Complex = std::complex<typename NumTraits<Scalar>::Real>;
using CMat = Matrix<Complex, Dynamic, Dynamic>;
using CVec = Matrix<Complex, Dynamic, 1>;
const DiagKronecker<Scalar> f(n1, n2);
const CVec lambda = f.K.eigenvalues();
const CMat V = f.K.eigenvectors();
VERIFY_IS_APPROX((f.dense.template cast<Complex>() * V).eval(), (V * lambda.asDiagonal()).eval());
}
template <typename Scalar>
void test_kron_diag_svd(Index n1, Index n2) {
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
const DiagKronecker<Scalar> f(n1, n2);
const Matrix<typename NumTraits<Scalar>::Real, Dynamic, 1> sv = f.K.singularValues();
const Mat U = f.K.matrixU(), W = f.K.matrixV();
VERIFY_IS_APPROX((U * sv.template cast<Scalar>().asDiagonal() * W.adjoint()).eval(), f.dense);
}
template <typename Scalar>
void test_kron_diag_rank(Index n1, Index n2) {
const DiagKronecker<Scalar> f(n1, n2);
VERIFY_IS_EQUAL(f.K.rank(), f.dense.completeOrthogonalDecomposition().rank());
}
template <typename Scalar>
void test_kron_diag_least_squares(Index n1, Index n2) {
using Vec = Matrix<Scalar, Dynamic, 1>;
const DiagKronecker<Scalar> f(n1, n2);
const Vec b = Vec::Random(n1 * n2);
VERIFY_IS_APPROX(f.K.leastSquaresSolve(b), f.dense.completeOrthogonalDecomposition().solve(b).eval());
}
// Through diagonal factors, the balanced determinant survives partial products
// that overflow on their own.
void test_kron_diag_determinant_scaling() {
typedef Matrix<double, Dynamic, 1> Vec;
typedef DiagonalMatrix<double, Dynamic> Diag;
// 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);
}
// ---- Sparse factors ---------------------------------------------------------
// A random sparse test matrix with about half of its entries stored, drawn
// densely so that the dense reference is exact.
template <typename Scalar, int Options = ColMajor>
SparseMatrix<Scalar, Options> random_sparse(Index rows, Index cols) {
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
Mat dense = Mat::Random(rows, cols);
for (Index j = 0; j < cols; ++j)
for (Index i = 0; i < rows; ++i)
if (internal::random<int>(0, 1) == 0) dense(i, j) = Scalar(0);
return dense.sparseView();
}
// A random sparse factor made diagonally dominant by the shifts 2n, 4n, ...,
// 2n^2: the Gershgorin discs are disjoint, so it is nonsingular, well
// conditioned and diagonalizable with distinct eigenvalues (a half-zeroed
// random sparse matrix alone is often nilpotent).
template <typename Scalar>
SparseMatrix<Scalar> random_sparse_dominant(Index n) {
using RealScalar = typename NumTraits<Scalar>::Real;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using RealVec = Matrix<RealScalar, Dynamic, 1>;
Mat dense = Mat(random_sparse<Scalar>(n, n));
dense.diagonal() += (RealScalar(2 * n) * RealVec::LinSpaced(n, RealScalar(1), RealScalar(n))).template cast<Scalar>();
return dense.sparseView();
}
// Sparse factors: S (x) B, B (x) S and S1 (x) S2 must match the densified
// reference through dense assignment, coefficient access and every product
// path, while storing only the sparse factors' nonzeros. A row-major sparse
// factor takes the same paths.
template <typename Scalar>
void test_kron_sparse_product(Index m1, Index n1, Index m2, Index n2) {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
using RowSparse = SparseMatrix<Scalar, RowMajor>;
const Sparse S1 = random_sparse<Scalar>(m1, n1), S2 = random_sparse<Scalar>(m2, n2);
const RowSparse R1(S1);
const Mat B = Mat::Random(m2, n2);
const Mat S1d = S1, S2d = S2;
KroneckerOperator<Sparse, Mat> KL(S1, B); // S1 (x) B
KroneckerOperator<Mat, Sparse> KR(B, S2); // B (x) S2
KroneckerOperator<Sparse, Sparse> KS(S1, S2); // S1 (x) S2
KroneckerOperator<RowSparse, Mat> KM(R1, B); // row-major sparse factor
Mat refL = reference_kron<Scalar>(S1d, B);
Mat refR = reference_kron<Scalar>(B, S2d);
Mat refS = reference_kron<Scalar>(S1d, S2d);
// Dense assignment through evalTo (only the blocks of stored entries are
// visited, the rest is cleared) and coefficient access.
VERIFY_IS_APPROX(Mat(KL), refL);
VERIFY_IS_APPROX(Mat(KR), refR);
VERIFY_IS_APPROX(Mat(KS), refS);
VERIFY_IS_APPROX(Mat(KM), refL);
for (Index t = 0; t < 5; ++t) {
Index i = internal::random<Index>(0, KS.rows() - 1), j = internal::random<Index>(0, KS.cols() - 1);
VERIFY_IS_APPROX(KS.coeff(i, j), refS(i, j));
}
// Dense accumulation writes only the blocks of stored entries.
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 through the sparse-dense kernels.
Vec x = Vec::Random(refL.cols());
VERIFY_IS_APPROX((KL * x).eval(), (refL * x).eval());
VERIFY_IS_APPROX((KM * 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 xs = Vec::Random(refS.cols());
VERIFY_IS_APPROX((KS * xs).eval(), (refS * xs).eval());
Mat Xs = Mat::Random(refS.cols(), 2);
VERIFY_IS_APPROX((KS * Xs).eval(), (refS * Xs).eval());
// Factory deduction: a sparse expression maps to an owning SparseMatrix
// factor, a transposed column-major matrix to a row-major one.
auto KF = makeKroneckerOperator(S1, B);
STATIC_CHECK((std::is_same<decltype(KF), KroneckerOperator<Sparse, Mat>>::value));
VERIFY_IS_APPROX((KF * x).eval(), (refL * x).eval());
auto KT = makeKroneckerOperator(S1.transpose(), S2);
STATIC_CHECK((std::is_same<decltype(KT), KroneckerOperator<RowSparse, Sparse>>::value));
VERIFY_IS_APPROX(Mat(KT), reference_kron<Scalar>(Mat(S1d.transpose()), S2d));
}
// Assigning the operator to a SparseMatrix materializes the product sparsely:
// exactly one stored entry per pair of stored factor entries, compressed, for
// every factor-kind mix and both destination storage orders.
template <typename Scalar>
void test_kron_sparse_materialize(Index m1, Index n1, Index m2, Index n2) {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
using RowSparse = SparseMatrix<Scalar, RowMajor>;
using Diag = DiagonalMatrix<Scalar, Dynamic>;
const Sparse S1 = random_sparse<Scalar>(m1, n1), S2 = random_sparse<Scalar>(m2, n2);
const Mat B = Mat::Random(m2, n2);
const Diag D(Vec::Random(m1));
const Mat S1d = S1, S2d = S2;
const Mat refS = reference_kron<Scalar>(S1d, S2d);
KroneckerOperator<Sparse, Sparse> KS(S1, S2);
Sparse M;
M = KS;
VERIFY(M.isCompressed());
VERIFY_IS_EQUAL(M.nonZeros(), S1.nonZeros() * S2.nonZeros());
VERIFY_IS_APPROX(Mat(M), refS);
RowSparse Mr;
Mr = KS;
VERIFY_IS_EQUAL(Mr.nonZeros(), S1.nonZeros() * S2.nonZeros());
VERIFY_IS_APPROX(Mat(Mr), refS);
// Dense and diagonal factors contribute all their stored entries.
Sparse Md;
Md = KroneckerOperator<Mat, Sparse>(B, S2);
VERIFY_IS_EQUAL(Md.nonZeros(), B.size() * S2.nonZeros());
VERIFY_IS_APPROX(Mat(Md), reference_kron<Scalar>(B, S2d));
Sparse Mdiag;
Mdiag = KroneckerOperator<Diag, Sparse>(D, S2);
VERIFY_IS_EQUAL(Mdiag.nonZeros(), m1 * S2.nonZeros());
VERIFY_IS_APPROX(Mat(Mdiag), reference_kron<Scalar>(D.toDenseMatrix(), S2d));
Sparse Mright;
Mright = KroneckerOperator<Sparse, Diag>(S2, D);
VERIFY_IS_EQUAL(Mright.nonZeros(), S2.nonZeros() * m1);
VERIFY_IS_APPROX(Mat(Mright), reference_kron<Scalar>(S2d, D.toDenseMatrix()));
Sparse Mdense;
Mdense = KroneckerOperator<Mat, Mat>(B, B);
VERIFY_IS_EQUAL(Mdense.nonZeros(), B.size() * B.size());
VERIFY_IS_APPROX(Mat(Mdense), reference_kron<Scalar>(B, B));
// Row-major sparse factors visit the destination columns out of order, each
// column still in increasing row index: every insertion is an append into
// the per-inner-vector reservation.
const RowSparse R1(S1), R2(S2);
KroneckerOperator<RowSparse, RowSparse> KRM(R1, R2);
Sparse Mrm;
Mrm = KRM;
VERIFY(Mrm.isCompressed());
VERIFY(Mrm.innerIndicesAreSorted());
VERIFY_IS_EQUAL(Mrm.nonZeros(), S1.nonZeros() * S2.nonZeros());
VERIFY_IS_APPROX(Mat(Mrm), refS);
// Sparse accumulation.
Sparse acc = random_sparse<Scalar>(M.rows(), M.cols());
const Mat acc0 = acc;
acc += KS;
VERIFY_IS_APPROX(Mat(acc), (acc0 + refS).eval());
acc -= KS;
VERIFY_IS_APPROX(Mat(acc), acc0);
}
// The identity-Kronecker operators of finite-difference discretizations with a
// sparse S -- I_p (x) S, S (x) I_p and I_p (x) S (x) I_q -- against the dense
// reference and the vec identity. The triple product nests a materialized
// S (x) I_q as the sparse factor.
template <typename Scalar>
void test_kron_sparse_identity(Index p, Index m2, Index n2, Index q) {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
typedef Matrix<Scalar, Dynamic, Dynamic, ColMajor> CmMat; // reshape frame of the vec identity
using Sparse = SparseMatrix<Scalar>;
using Diag = DiagonalMatrix<Scalar, Dynamic>;
const Sparse S = random_sparse<Scalar>(m2, n2);
const Mat Sd = S, Ip = Mat::Identity(p, p), Iq = Mat::Identity(q, q);
auto KI = makeKroneckerOperator(Vec::Ones(p).asDiagonal(), S); // I_p (x) S
auto IK = makeKroneckerOperator(S, Vec::Ones(p).asDiagonal()); // S (x) I_p
STATIC_CHECK((std::is_same<decltype(KI), KroneckerOperator<Diag, Sparse>>::value));
STATIC_CHECK((std::is_same<decltype(IK), KroneckerOperator<Sparse, Diag>>::value));
Mat refI = reference_kron<Scalar>(Ip, Sd), refK = reference_kron<Scalar>(Sd, Ip);
VERIFY_IS_APPROX(Mat(KI), refI);
VERIFY_IS_APPROX(Mat(IK), refK);
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) S) vec(X) = vec(S X) with X of size n2 x p: one sparse-dense product.
const Map<const CmMat> Xmat(x.data(), n2, p);
CmMat Y = S * Xmat;
VERIFY_IS_APPROX((KI * x).eval(), Vec(Map<const Vec>(Y.data(), Y.size())));
// Sparse materialization stores p nnz(S) entries on either side.
Sparse MI;
MI = KI;
VERIFY_IS_EQUAL(MI.nonZeros(), p * S.nonZeros());
VERIFY_IS_APPROX(Mat(MI), refI);
Sparse MK;
MK = IK;
VERIFY_IS_EQUAL(MK.nonZeros(), p * S.nonZeros());
VERIFY_IS_APPROX(Mat(MK), refK);
// I_p (x) S (x) I_q = I_p (x) (S (x) I_q), the inner product materialized
// with q nnz(S) entries and nested as the sparse factor.
Sparse SI;
SI = makeKroneckerOperator(S, Vec::Ones(q).asDiagonal());
auto K3 = makeKroneckerOperator(Vec::Ones(p).asDiagonal(), SI);
Mat ref3 = reference_kron<Scalar>(Ip, reference_kron<Scalar>(Sd, Iq));
VERIFY_IS_APPROX(Mat(K3), ref3);
Vec x3 = Vec::Random(ref3.cols());
VERIFY_IS_APPROX((K3 * x3).eval(), (ref3 * x3).eval());
Sparse M3;
M3 = K3;
VERIFY_IS_EQUAL(M3.nonZeros(), p * q * S.nonZeros());
VERIFY_IS_APPROX(Mat(M3), ref3);
}
// The transposition family of a sparse factor stays sparse, in type and value.
template <typename Scalar>
void test_kron_sparse_transpose(Index m1, Index n1, Index m2, Index n2) {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
const Sparse S = random_sparse<Scalar>(m1, n1);
const Mat B = Mat::Random(m2, n2);
KroneckerOperator<Sparse, Mat> K(S, B);
Mat dense = reference_kron<Scalar>(Mat(S), B);
STATIC_CHECK((std::is_same<typename internal::remove_all_t<decltype(K.transpose())>,
KroneckerOperator<Sparse, Matrix<Scalar, Dynamic, Dynamic>>>::value));
STATIC_CHECK((std::is_same<typename internal::remove_all_t<decltype(K.adjoint())>,
KroneckerOperator<Sparse, Matrix<Scalar, Dynamic, Dynamic>>>::value));
STATIC_CHECK(
(std::is_same<typename internal::remove_all_t<decltype(K.conjugate())>, KroneckerOperator<Sparse, 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()));
VERIFY_IS_EQUAL(K.transpose().lhs().nonZeros(), S.nonZeros());
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 sparse factors: one SparseLU per sparse factor, then its
// substitutions. All factor-kind mixes against the dense LU.
template <typename Scalar>
void test_kron_sparse_solve(Index n1, Index n2) {
using RealScalar = typename NumTraits<Scalar>::Real;
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
using RowSparse = SparseMatrix<Scalar, RowMajor>;
using Diag = DiagonalMatrix<Scalar, Dynamic>;
const Sparse S1 = random_sparse_dominant<Scalar>(n1), S2 = random_sparse_dominant<Scalar>(n2);
const Mat A = Mat::Random(n1, n1) + RealScalar(2 * n1) * Mat::Identity(n1, n1);
const Mat B = Mat::Random(n2, n2) + RealScalar(2 * n2) * Mat::Identity(n2, n2);
const Diag D(Vec::Random(n2) + Vec::Constant(n2, Scalar(2)));
KroneckerOperator<Sparse, Mat> KL(S1, B);
KroneckerOperator<Mat, Sparse> KR(A, S2);
KroneckerOperator<Sparse, Sparse> KS(S1, S2);
KroneckerOperator<Sparse, Diag> KD(S1, D);
const RowSparse R1(S1);
KroneckerOperator<RowSparse, Mat> KM(R1, B);
Mat denseL = reference_kron<Scalar>(Mat(S1), B), denseR = reference_kron<Scalar>(A, Mat(S2));
Mat denseS = reference_kron<Scalar>(Mat(S1), Mat(S2)), denseD = reference_kron<Scalar>(Mat(S1), D.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(KS.solve(b), denseS.partialPivLu().solve(b).eval());
VERIFY_IS_APPROX(KD.solve(b), denseD.partialPivLu().solve(b).eval());
VERIFY_IS_APPROX(KM.solve(b), denseL.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());
VERIFY_IS_APPROX(KS.solve(Bm), denseS.partialPivLu().solve(Bm).eval());
}
// inverse() of a sparse factor is a dense matrix from one SparseLU solve;
// determinant() accumulates the SparseLU pivots with the balancing of the dense
// path, and an exactly singular sparse factor gives an exact zero.
template <typename Scalar>
void test_kron_sparse_inverse_determinant(Index n1, Index n2) {
using RealScalar = typename NumTraits<Scalar>::Real;
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using CmMat = Matrix<Scalar, Dynamic, Dynamic, ColMajor>;
using Sparse = SparseMatrix<Scalar>;
using Diag = DiagonalMatrix<Scalar, Dynamic>;
const Sparse S = random_sparse_dominant<Scalar>(n1);
const Mat B = Mat::Random(n2, n2) + RealScalar(2) * Mat::Identity(n2, n2);
KroneckerOperator<Sparse, Mat> K(S, B);
Mat dense = reference_kron<Scalar>(Mat(S), B);
auto Kinv = K.inverse();
STATIC_CHECK((std::is_same<typename internal::remove_all_t<decltype(Kinv)>, KroneckerOperator<CmMat, CmMat>>::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 D(Vec::Random(n2) + Vec::Constant(n2, Scalar(2)));
KroneckerOperator<Sparse, Diag> KD(S, D);
Mat denseD = reference_kron<Scalar>(Mat(S), D.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())));
// A structurally empty column makes the factor exactly singular.
Mat Zd = Mat(S);
Zd.col(0).setZero();
const Sparse Z = Zd.sparseView();
KroneckerOperator<Sparse, Mat> KZ(Z, B);
VERIFY_IS_EQUAL(KZ.determinant(), Scalar(0));
// [[0, 2], [3, 0]] forces a row interchange: the permutation sign must enter.
Mat Pd(2, 2);
Pd << Scalar(0), Scalar(2), Scalar(3), Scalar(0);
const Sparse P = Pd.sparseView();
KroneckerOperator<Sparse, Mat> KP(P, B);
VERIFY_IS_APPROX(KP.determinant(), reference_kron<Scalar>(Pd, B).determinant());
}
// Degenerate sparse factors. An exactly singular factor (a structurally empty
// column) fails its SparseLU: solve() and inverse() fill with NaN, the
// determinant is an exact zero. A NaN entry propagates through products, solves
// and the determinant. An all-zero factor gives zero products, determinant and
// rank, and an empty materialization.
void test_kron_sparse_degenerate(Index n1, Index n2) {
using Vec = Matrix<double, Dynamic, 1>;
using Mat = Matrix<double, Dynamic, Dynamic>;
using Sparse = SparseMatrix<double>;
const Mat B = Mat::Random(n2, n2) + 2.0 * Mat::Identity(n2, n2);
Vec b = Vec::Random(n1 * n2);
Mat Zd = Mat(random_sparse_dominant<double>(n1));
Zd.col(0).setZero();
const Sparse Z = Zd.sparseView();
KroneckerOperator<Sparse, Mat> KZ(Z, B);
KroneckerOperator<Mat, Sparse> KZr(B, Z);
VERIFY(KZ.solve(b).array().isNaN().all());
VERIFY(KZr.solve(b).array().isNaN().all());
Mat inv = KZ.inverse();
VERIFY(inv.array().isNaN().all());
VERIFY_IS_EQUAL(KZ.determinant(), 0.0);
VERIFY_IS_EQUAL(KZr.determinant(), 0.0);
Mat Nd = Mat(random_sparse_dominant<double>(n1));
Nd(0, 0) = std::numeric_limits<double>::quiet_NaN();
const Sparse N = Nd.sparseView();
KroneckerOperator<Sparse, Mat> KN(N, B);
VERIFY((numext::isnan)(KN.determinant()));
VERIFY(!(KN * b).eval().allFinite());
VERIFY(!KN.solve(b).allFinite());
const Sparse O(n1, n1);
KroneckerOperator<Sparse, Mat> KO(O, B);
VERIFY_IS_EQUAL((KO * b).eval(), Vec(Vec::Zero(n1 * n2)));
VERIFY_IS_EQUAL(KO.determinant(), 0.0);
VERIFY_IS_EQUAL(KO.rank(), 0);
Sparse MO;
MO = KO;
VERIFY_IS_EQUAL(MO.nonZeros(), 0);
VERIFY_IS_EQUAL(MO.rows(), n1 * n2);
VERIFY_IS_EQUAL(MO.cols(), n1 * n2);
}
// A factor handed over uncompressed (reserve + insert) is compressed on
// construction, and its explicitly stored zeros count as stored entries of the
// materialization, exactly as in kroneckerProduct().
void test_kron_sparse_stored_zeros(Index n1, Index n2) {
using Vec = Matrix<double, Dynamic, 1>;
using Mat = Matrix<double, Dynamic, Dynamic>;
using Sparse = SparseMatrix<double>;
Sparse U(n1, n1);
U.reserve(VectorXi::Constant(int(n1), 2));
for (Index j = 0; j < n1; ++j) {
U.insert(j, j) = double(j + 1);
U.insert((j + 1) % n1, j) = 0.0;
}
VERIFY(!U.isCompressed());
const Mat B = Mat::Random(n2, n2);
KroneckerOperator<Sparse, Mat> K(U, B);
VERIFY(K.lhs().isCompressed());
VERIFY_IS_EQUAL(K.lhs().nonZeros(), 2 * n1);
Mat dense = reference_kron<double>(Mat(U), B);
VERIFY_IS_APPROX(Mat(K), dense);
Vec x = Vec::Random(dense.cols());
VERIFY_IS_APPROX((K * x).eval(), (dense * x).eval());
Sparse M;
M = K;
VERIFY_IS_EQUAL(M.nonZeros(), 2 * n1 * n2 * n2);
VERIFY_IS_APPROX(Mat(M), dense);
}
// An ill-conditioned sparse factor: the tridiagonal Laplacian of order n has
// condition number ~0.4 n^2, so I_p (x) L is judged by its backward error
// ||b - K x|| <= n eps ||L||_2 ||x||, with ||L||_2 < 4, not entrywise.
void test_kron_sparse_solve_laplacian(Index n, Index p) {
using Vec = Matrix<double, Dynamic, 1>;
using Sparse = SparseMatrix<double>;
Sparse L(n, n);
L.reserve(VectorXi::Constant(int(n), 3));
for (Index j = 0; j < n; ++j) {
if (j > 0) L.insert(j - 1, j) = -1.0;
L.insert(j, j) = 2.0;
if (j + 1 < n) L.insert(j + 1, j) = -1.0;
}
auto K = makeKroneckerOperator(Vec::Ones(p).asDiagonal(), L);
Vec b = Vec::Random(n * p);
Vec x = K.solve(b);
Vec r = b - K * x;
VERIFY(r.norm() <= double(n) * NumTraits<double>::epsilon() * 4.0 * x.norm());
}
// The decomposition family on a sparse factor (materialized densely for the
// factor decompositions) against the dense references, one test per
// decomposition to keep each part's compile small.
template <typename Scalar>
void test_kron_sparse_eigen(Index n1, Index n2) {
using RealScalar = typename NumTraits<Scalar>::Real;
using Complex = std::complex<RealScalar>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using CMat = Matrix<Complex, Dynamic, Dynamic>;
using CVec = Matrix<Complex, Dynamic, 1>;
using Sparse = SparseMatrix<Scalar>;
const Sparse S = random_sparse_dominant<Scalar>(n1);
const Mat B = Mat::Random(n2, n2);
const KroneckerOperator<Sparse, Mat> K(S, B);
const Mat dense = reference_kron<Scalar>(Mat(S), B);
const CVec lambda = K.eigenvalues();
const CMat V = K.eigenvectors();
VERIFY_IS_APPROX((dense.template cast<Complex>() * V).eval(), (V * lambda.asDiagonal()).eval());
}
template <typename Scalar>
void test_kron_sparse_svd(Index n1, Index n2) {
using RealScalar = typename NumTraits<Scalar>::Real;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
const Sparse S = random_sparse_dominant<Scalar>(n1);
const Mat B = Mat::Random(n2, n2);
const KroneckerOperator<Sparse, Mat> K(S, B);
const Mat dense = reference_kron<Scalar>(Mat(S), B);
const Matrix<RealScalar, Dynamic, 1> sv = K.singularValues();
const Mat U = K.matrixU(), W = K.matrixV();
VERIFY_IS_APPROX((U * sv.template cast<Scalar>().asDiagonal() * W.adjoint()).eval(), dense);
}
template <typename Scalar>
void test_kron_sparse_rank(Index n1, Index n2) {
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
const Sparse S = random_sparse_dominant<Scalar>(n1);
const KroneckerOperator<Sparse, Mat> K(S, Mat::Random(n2, n2));
VERIFY_IS_EQUAL(K.rank(), Mat(K).completeOrthogonalDecomposition().rank());
}
template <typename Scalar>
void test_kron_sparse_least_squares(Index n1, Index n2) {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
const Sparse S = random_sparse_dominant<Scalar>(n1);
const Mat B = Mat::Random(n2, n2);
const KroneckerOperator<Sparse, Mat> K(S, B);
const Mat dense = reference_kron<Scalar>(Mat(S), B);
const Vec b = Vec::Random(n1 * n2);
VERIFY_IS_APPROX(K.leastSquaresSolve(b), dense.completeOrthogonalDecomposition().solve(b).eval());
}
// The balanced SparseLU determinant survives partial products that overflow on
// their own.
void test_kron_sparse_determinant_scaling() {
using Vec = Matrix<double, Dynamic, 1>;
using Mat = Matrix<double, Dynamic, Dynamic>;
using Sparse = SparseMatrix<double>;
// det([1e200])^2 = 1e400 overflows on its own, yet the determinant of
// [1e200] (x) diag(1e-100, 1e-100) is 1e200 -- through the SparseLU pivots.
Mat A1(1, 1);
A1 << 1e200;
Vec d(2);
d << 1e-100, 1e-100;
const Sparse SA = A1.sparseView(), SB = Mat(d.asDiagonal()).sparseView();
KroneckerOperator<Sparse, Sparse> Kd(SA, SB);
VERIFY_IS_APPROX(Kd.determinant(), 1e200);
}
// Mixed-scalar promotion through a sparse factor on either side: a real
// operator applied to a complex right-hand side runs in the promoted scalar.
template <typename RealScalar>
void test_kron_sparse_mixed_scalar(Index n1, Index m2, Index n2) {
using Complex = std::complex<RealScalar>;
using RMat = Matrix<RealScalar, Dynamic, Dynamic>;
using CVec = Matrix<Complex, Dynamic, 1>;
using CMat = Matrix<Complex, Dynamic, Dynamic>;
using RSparse = SparseMatrix<RealScalar>;
const RSparse S = random_sparse<RealScalar>(n1, n1);
RMat B = RMat::Random(m2, n2);
KroneckerOperator<RSparse, RMat> KL(S, B);
KroneckerOperator<RMat, RSparse> KR(B, S);
CMat denseL = reference_kron<RealScalar>(RMat(S), B).template cast<Complex>();
CMat denseR = reference_kron<RealScalar>(B, RMat(S)).template cast<Complex>();
CVec x = CVec::Random(n1 * n2);
CVec y = KL * x;
VERIFY_IS_APPROX(y, (denseL * x).eval());
CVec y0 = CVec::Random(n1 * m2);
y = y0;
y.noalias() += KL * x;
VERIFY_IS_APPROX(y, (y0 + denseL * x).eval());
CVec xr = CVec::Random(n2 * n1);
CVec yr = KR * xr;
VERIFY_IS_APPROX(yr, (denseR * xr).eval());
}
// Matrix-free CG through a sparse-factor operator: an SPD sparse factor
// tensored with an SPD dense factor is SPD.
void test_kron_sparse_matrix_free_cg(Index n1, Index n2) {
using Vec = Matrix<double, Dynamic, 1>;
using Mat = Matrix<double, Dynamic, Dynamic>;
using Sparse = SparseMatrix<double>;
const Mat R = Mat(random_sparse<double>(n1, n1));
const Mat Sd = R * R.transpose() + double(n1) * Mat::Identity(n1, n1);
const Sparse S = Sd.sparseView();
Mat Br = Mat::Random(n2, n2);
Mat B = Br * Br.adjoint() + double(n2) * Mat::Identity(n2, n2);
KroneckerOperator<Sparse, Mat> K(S, B);
Mat dense = reference_kron<double>(Sd, B);
Vec b = Vec::Random(n1 * n2);
ConjugateGradient<KroneckerOperator<Sparse, 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 Options>
void test_kron_sparse_determinant_subnormal(const Scalar& phase) {
using Real = typename NumTraits<Scalar>::Real;
using Mat = Matrix<Scalar, 2, 2>;
using Sparse = SparseMatrix<Scalar, Options>;
const Real normalMin = (std::numeric_limits<Real>::min)();
const Real large = Real(1) / normalMin;
Matrix<Scalar, 1, 1> b;
b << Scalar(1);
// Scaling this whole factor down would discard its small, essential pivot.
Mat a = Mat::Zero();
Sparse wide(2, 2);
wide.insert(0, 0) = Scalar(large);
wide.insert(1, 1) = Scalar(normalMin);
VERIFY_IS_EQUAL(makeKroneckerOperator(wide, b).determinant(), Scalar(1));
if (underflowProbe<Real>() == Real(0) || ScopedFlushToZero::hardwareFlushesSubnormalInputs()) return;
const Real small = normalMin / Real(16);
b << Scalar(large);
const Real tolerance = Real(64) * NumTraits<Real>::epsilon();
Sparse sparseA(2, 2), singular(2, 2);
for (int swap = 0; swap < 2; ++swap) {
a << Real(2) * small * phase, small * phase, small * phase, Real(2) * small * phase;
if (swap) a.row(0).swap(a.row(1));
// Insert explicitly: sparseView's complex magnitude test can underflow.
sparseA.setZero();
for (Index j = 0; j < 2; ++j)
for (Index i = 0; i < 2; ++i) sparseA.insert(i, j) = a(i, j);
// (small * large)^2 * det([[2,1],[1,2]]) = 3/256, exactly.
const Scalar expected = Scalar(swap ? -Real(3) / Real(256) : Real(3) / Real(256)) * phase * phase;
const Scalar left = makeKroneckerOperator(sparseA, b).determinant();
const Scalar right = makeKroneckerOperator(b, sparseA).determinant();
VERIFY((numext::isfinite)(left));
VERIFY((numext::isfinite)(right));
VERIFY(numext::abs(left - expected) <= tolerance * numext::abs(expected));
VERIFY(numext::abs(right - expected) <= tolerance * numext::abs(expected));
const DiagonalMatrix<Scalar, 2> identityScale(Matrix<Scalar, 2, 1>::Constant(Scalar(large)));
const Scalar repeated = makeKroneckerOperator(sparseA, identityScale).determinant();
VERIFY((numext::isfinite)(repeated));
VERIFY(numext::abs(repeated - expected * expected) <= tolerance * numext::abs(expected * expected));
singular = sparseA;
for (Index j = 0; j < 2; ++j) singular.coeffRef(1, j) = singular.coeff(0, j);
VERIFY_IS_EQUAL(makeKroneckerOperator(singular, b).determinant(), Scalar(0));
}
// Non-dyadic entries near tiny = min * 2^-(digits/2), which carry about
// digits/2 bits: eliminating there would lose the other half, the exact
// scale-up does not. The reference is the determinant of the stored data.
// Coefficient-wise arithmetic keeps the subnormal entries out of packets,
// which ARMv7 NEON flushes regardless of FPSCR.FZ.
const Real tiny = std::ldexp(normalMin, -(std::numeric_limits<Real>::digits / 2));
const Real huge = std::ldexp(Real(1), std::numeric_limits<Real>::max_exponent - 1);
a << Real(4.1), Real(1.3), Real(1.1), Real(5.3);
Mat stored;
sparseA.setZero();
for (Index j = 0; j < 2; ++j)
for (Index i = 0; i < 2; ++i) {
a(i, j) = Scalar(numext::real(a(i, j)) * tiny) * phase;
stored(i, j) = a(i, j) / tiny; // exact
sparseA.insert(i, j) = a(i, j);
}
const Scalar expectedStored = stored.determinant() * Scalar((tiny * huge) * (tiny * huge));
b << Scalar(huge);
const Scalar precise = makeKroneckerOperator(sparseA, b).determinant();
VERIFY(numext::abs(precise - expectedStored) <= tolerance * numext::abs(expectedStored));
}
template <typename Scalar>
struct kron_subnormal_entry {
static Scalar run(const Scalar& value) { return value; }
static Scalar phaseSquared() { return Scalar(1); }
};
template <typename Real>
struct kron_subnormal_entry<std::complex<Real>> {
// Purely imaginary entries route the subnormal data through realView().
static std::complex<Real> run(const Real& value) { return std::complex<Real>(Real(0), value); }
static std::complex<Real> phaseSquared() { return std::complex<Real>(Real(-1), Real(0)); }
};
// All-subnormal factors of every kind, built from bit patterns so that FTZ/DAZ
// cannot flush the fixture: det(a (x) b) = det(m) (denorm_min b)^2 with
// b = 2^(max_exponent - 1), a normal reference, plainly and under flush-to-zero.
template <typename Scalar>
void test_kron_determinant_flushed_subnormal() {
using Real = typename NumTraits<Scalar>::Real;
using Bits = typename internal::binary_floating_point_traits<Real>::Bits;
using Entry = kron_subnormal_entry<Scalar>;
using Mat = Matrix<Scalar, 2, 2>;
using Sparse = SparseMatrix<Scalar>;
// a = m * denorm_min with m = [4113 1301; 1103 5309], det m = 20400914: digits/2 bits per entry.
const Bits significands[2][2] = {{Bits(4113), Bits(1301)}, {Bits(1103), Bits(5309)}};
const double detSignificands = 4113.0 * 5309.0 - 1301.0 * 1103.0;
const double detDiagonalSignificands = 4113.0 * 5309.0;
// denorm_min * b = 2^(min_exponent - digits) * 2^(max_exponent - 1), a normal power of two.
const Real scaledUnit =
numext::ldexp(Real(1), std::numeric_limits<Real>::min_exponent - std::numeric_limits<Real>::digits +
std::numeric_limits<Real>::max_exponent - 1);
const Scalar expected = Scalar(Real(detSignificands) * scaledUnit * scaledUnit) * Entry::phaseSquared();
const Scalar expectedDiagonal =
Scalar(Real(detDiagonalSignificands) * scaledUnit * scaledUnit) * Entry::phaseSquared();
// The LU rounds each pivot a few times and the balanced accumulation once per factor.
const Real tolerance = Real(64) * NumTraits<Real>::epsilon();
Mat a;
for (Index i = 0; i < 2; ++i)
for (Index j = 0; j < 2; ++j) a(i, j) = Entry::run(numext::bit_cast<Real>(significands[i][j]));
Sparse sparseA(2, 2);
for (Index j = 0; j < 2; ++j)
for (Index i = 0; i < 2; ++i) sparseA.insert(i, j) = a(i, j);
const DiagonalMatrix<Scalar, 2> diagonalA(a(0, 0), a(1, 1));
Matrix<Scalar, 1, 1> b;
b << Scalar(numext::ldexp(Real(1), std::numeric_limits<Real>::max_exponent - 1));
const auto check = [&]() {
const auto close = [&](const Scalar& actual, const Scalar& reference) {
VERIFY((numext::isfinite)(actual));
VERIFY(numext::abs(actual - reference) <= tolerance * numext::abs(reference));
};
close(makeKroneckerOperator(a, b).determinant(), expected);
close(makeKroneckerOperator(b, a).determinant(), expected);
close(makeKroneckerOperator(sparseA, b).determinant(), expected);
close(makeKroneckerOperator(b, sparseA).determinant(), expected);
close(makeKroneckerOperator(diagonalA, b).determinant(), expectedDiagonal);
close(makeKroneckerOperator(b, diagonalA).determinant(), expectedDiagonal);
};
forEachFlushToZeroMode([&](FlushToZeroMode) { check(); });
}
// A factor with a normal and a subnormal pivot 40 octaves apart: the packet
// scale-up would flush the small pivot under flush-to-zero, and the exact one
// keeps det(A (x) b) = 2^-100 2^-140 (2^120)^2 = 1 for every factor kind.
template <typename Scalar>
void test_kron_determinant_flushed_pivot() {
using Real = typename NumTraits<Scalar>::Real;
using Entry = kron_subnormal_entry<Scalar>;
using Mat = Matrix<Scalar, 2, 2>;
using Sparse = SparseMatrix<Scalar>;
constexpr int minExponent = std::numeric_limits<Real>::min_exponent;
// 2^(min_exponent + 25) and 2^(min_exponent - 15): 40 octaves apart, the second one subnormal.
const Real large = numext::ldexp(Real(1), minExponent + 25);
const Real small = numext::bit_cast<Real>(typename internal::binary_floating_point_traits<Real>::Bits(1)
<< (std::numeric_limits<Real>::digits - 15));
const Real b = numext::ldexp(Real(1), -minExponent - 5); // large * small * b^2 == 1
Mat a = Mat::Zero();
a(0, 0) = Entry::run(large);
a(1, 1) = Entry::run(small);
Sparse sparseA(2, 2);
sparseA.insert(0, 0) = a(0, 0);
sparseA.insert(1, 1) = a(1, 1);
const DiagonalMatrix<Scalar, 2> diagonalA(a(0, 0), a(1, 1));
Matrix<Scalar, 1, 1> bMat;
bMat << Scalar(b);
const Scalar expected = Entry::phaseSquared();
const Real tolerance = Real(16) * NumTraits<Real>::epsilon();
const auto check = [&]() {
const auto close = [&](const Scalar& actual) {
VERIFY(numext::abs(actual - expected) <= tolerance * numext::abs(expected));
};
close(makeKroneckerOperator(a, bMat).determinant());
close(makeKroneckerOperator(sparseA, bMat).determinant());
close(makeKroneckerOperator(diagonalA, bMat).determinant());
};
forEachFlushToZeroMode([&](FlushToZeroMode) { check(); });
}
// Materialization is entrywise, so it stays well-defined for non-finite
// factors: coefficient access, dense and sparse assignment must agree (products
// carry no such contract; Inf/NaN propagate as through the factor products).
template <typename Lhs, typename Rhs>
void check_kron_sparse_nonfinite(const KroneckerOperator<Lhs, Rhs>& k) {
using Scalar = typename Lhs::Scalar;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
SparseMatrix<Scalar> sparse;
sparse = k;
const Mat dense = k;
Mat coefficients(k.rows(), k.cols());
for (Index j = 0; j < k.cols(); ++j)
for (Index i = 0; i < k.rows(); ++i) coefficients(i, j) = k.coeff(i, j);
VERIFY_IS_CWISE_EQUAL(coefficients.realView(), dense.realView());
VERIFY_IS_CWISE_EQUAL(Mat(sparse).realView(), dense.realView());
}
template <typename Scalar, int Options>
void test_kron_sparse_nonfinite() {
using Real = typename NumTraits<Scalar>::Real;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar, Options>;
const Sparse empty(1, 1);
Sparse storedZero(1, 1);
storedZero.insert(0, 0) = Scalar(0);
Sparse pattern(3, 3);
pattern.insert(0, 0) = Scalar(1);
pattern.insert(2, 0) = Scalar(0);
pattern.insert(0, 2) = Scalar(2); // Row 1 and column 1 are structurally empty.
Mat finite = Mat::Ones(2, 2);
check_kron_sparse_nonfinite(makeKroneckerOperator(finite, pattern));
for (Real special : {NumTraits<Real>::infinity(), NumTraits<Real>::quiet_NaN()}) {
const Mat single = Mat::Constant(1, 1, Scalar(special));
check_kron_sparse_nonfinite(makeKroneckerOperator(single, empty));
check_kron_sparse_nonfinite(makeKroneckerOperator(empty, single));
check_kron_sparse_nonfinite(makeKroneckerOperator(single, storedZero));
check_kron_sparse_nonfinite(makeKroneckerOperator(storedZero, single));
Mat nonfinite = finite;
nonfinite(0, 0) = Scalar(special);
const Sparse sparseNonfinite = nonfinite.sparseView();
check_kron_sparse_nonfinite(makeKroneckerOperator(nonfinite, pattern));
check_kron_sparse_nonfinite(makeKroneckerOperator(pattern, nonfinite));
check_kron_sparse_nonfinite(makeKroneckerOperator(sparseNonfinite, pattern));
check_kron_sparse_nonfinite(makeKroneckerOperator(pattern, sparseNonfinite));
const DiagonalMatrix<Scalar, 2> diagonal(nonfinite.diagonal());
check_kron_sparse_nonfinite(makeKroneckerOperator(diagonal, pattern));
check_kron_sparse_nonfinite(makeKroneckerOperator(pattern, diagonal));
}
}
// Checks every product form against the dense reference: one right-hand side
// (reshaped in place), several (stacked), and the accumulating forms.
template <typename Op, typename Mat>
void check_kron_products(const Op& K, const Mat& ref) {
using Scalar = typename Mat::Scalar;
using Complex = std::complex<typename NumTraits<Scalar>::Real>;
using Vec = Matrix<Scalar, Dynamic, 1>;
using CVec = Matrix<Complex, Dynamic, 1>;
const Vec x = Vec::Random(ref.cols());
VERIFY_IS_APPROX((K * x).eval(), (ref * x).eval());
const Mat X = Mat::Random(ref.cols(), 3);
VERIFY_IS_APPROX((K * X).eval(), (ref * X).eval());
Vec y = Vec::Random(ref.rows());
const Vec y0 = y;
y.noalias() += K * x;
VERIFY_IS_APPROX(y, (y0 + ref * x).eval());
y = y0;
y.noalias() -= K * x;
VERIFY_IS_APPROX(y, (y0 - ref * x).eval());
// A complex right-hand side promotes the workspaces.
const CVec xc = CVec::Random(ref.cols());
VERIFY_IS_APPROX((K * xc).eval(), (ref.template cast<Complex>() * xc).eval());
}
// Identity() factors are stored as their dimensions and skipped in products,
// square or rectangular, on either side.
template <typename Scalar>
void test_kron_identity_factor(Index p, Index m2, Index n2) {
using RealScalar = typename NumTraits<Scalar>::Real;
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
using RowSparse = SparseMatrix<Scalar, RowMajor>;
using Id = internal::kron_identity_factor<Scalar, Dynamic, Dynamic>;
const Mat A = Mat::Random(m2, n2);
const Sparse S = random_sparse<Scalar>(m2, n2);
const Mat Ip = Mat::Identity(p, p), Sd = S;
auto KI = makeKroneckerOperator(Mat::Identity(p, p), A); // I_p (x) A
auto IK = makeKroneckerOperator(S, Mat::Identity(p, p)); // S (x) I_p
STATIC_CHECK((std::is_same<decltype(KI), KroneckerOperator<Id, Mat>>::value));
STATIC_CHECK((std::is_same<decltype(IK), KroneckerOperator<Sparse, Id>>::value));
const Mat refI = reference_kron<Scalar>(Ip, A), refK = reference_kron<Scalar>(Sd, Ip);
check_kron_products(KI, refI);
check_kron_products(IK, refK);
const Vec x = Vec::Random(p * n2);
VERIFY_IS_APPROX((KI * x).eval(), (makeKroneckerOperator(Vec::Ones(p).asDiagonal(), A) * x).eval());
// Unlike the Identity() expression, the stored identity is assignable, and so
// is an operator holding one.
auto KA = makeKroneckerOperator(Mat::Identity(p + 1, p + 1), A);
KA = KI;
VERIFY_IS_EQUAL(Mat(KA), refI);
// Materialization copies the other factor's entries, with p copies of each.
VERIFY_IS_EQUAL(Mat(KI), refI);
VERIFY_IS_EQUAL(Mat(IK), refK);
Sparse MI;
MI = KI;
VERIFY_IS_EQUAL(MI.nonZeros(), p * m2 * n2);
VERIFY_IS_EQUAL(Mat(MI), refI);
RowSparse MK;
MK = IK;
VERIFY_IS_EQUAL(MK.nonZeros(), p * S.nonZeros());
VERIFY_IS_EQUAL(Mat(MK), refK);
for (Index t = 0; t < 5; ++t) {
const Index i = internal::random<Index>(0, KI.rows() - 1), j = internal::random<Index>(0, KI.cols() - 1);
VERIFY_IS_EQUAL(KI.coeff(i, j), refI(i, j));
}
// The transposition family keeps the identity.
STATIC_CHECK((std::is_same<decltype(KI.transpose()), KroneckerOperator<Id, Mat>>::value));
VERIFY_IS_EQUAL(Mat(KI.transpose()), Mat(refI.transpose()));
VERIFY_IS_EQUAL(Mat(IK.adjoint()), Mat(refK.adjoint()));
VERIFY_IS_EQUAL(Mat(IK.conjugate()), Mat(refK.conjugate()));
// Rectangular identities: I_{p x (p+2)} (x) A and A (x) I_{(p+1) x p}.
const Mat Iw = Mat::Identity(p, p + 2), It = Mat::Identity(p + 1, p);
auto KW = makeKroneckerOperator(Mat::Identity(p, p + 2), A);
auto KT = makeKroneckerOperator(A, Mat::Identity(p + 1, p));
const Mat refW = reference_kron<Scalar>(Iw, A), refT = reference_kron<Scalar>(A, It);
check_kron_products(KW, refW);
check_kron_products(KT, refT);
check_kron_products(KW.adjoint(), Mat(refW.adjoint()));
check_kron_products(KT.transpose(), Mat(refT.transpose()));
Sparse MT;
MT = KT;
VERIFY_IS_EQUAL(MT.nonZeros(), p * m2 * n2);
VERIFY_IS_EQUAL(Mat(MT), refT);
VERIFY_IS_EQUAL(Mat(KW), refW);
// Square factors: the identity solves, inverts and has determinant one.
const Mat B = Mat::Random(m2, m2) + RealScalar(2) * Mat::Identity(m2, m2);
auto KB = makeKroneckerOperator(Mat::Identity(p, p), B);
const Mat refB = reference_kron<Scalar>(Ip, B);
const Vec b = Vec::Random(p * m2);
VERIFY_IS_APPROX((refB * KB.solve(b)).eval(), b);
const Mat Bb = Mat::Random(p * m2, 3);
VERIFY_IS_APPROX((reference_kron<Scalar>(B, Ip) * makeKroneckerOperator(B, Mat::Identity(p, p)).solve(Bb)).eval(),
Bb);
VERIFY_IS_APPROX((Mat(KB.inverse()) * refB).eval(), Mat(Mat::Identity(p * m2, p * m2)));
VERIFY_IS_APPROX(KB.determinant(), refB.determinant());
STATIC_CHECK(
(std::is_same<decltype(KB.inverse()), KroneckerOperator<Id, Matrix<Scalar, Dynamic, Dynamic, ColMajor>>>::value));
VERIFY_IS_EQUAL(makeKroneckerOperator(Mat::Identity(p, p), Mat::Identity(m2, m2)).determinant(), Scalar(1));
}
// Nested factors: A (x) (B (x) C) and (A (x) B) (x) C against the dense
// reference, with dense, sparse and identity leaves. Moderate diagonal boost:
// invertible, with determinants tame enough for a relative comparison. The
// operation families are separate tests to keep each part's compile small.
template <typename Scalar>
struct NestedKronecker {
using RealScalar = typename NumTraits<Scalar>::Real;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
using RightNested = KroneckerOperator<Mat, KroneckerOperator<Sparse, Mat>>;
using LeftNested = KroneckerOperator<KroneckerOperator<Mat, Sparse>, Mat>;
NestedKronecker(Index n1, Index n2, Index n3)
: A(Mat::Random(n1, n1) + RealScalar(2) * Mat::Identity(n1, n1)),
Bs(random_sparse_dominant<Scalar>(n2)),
C(Mat::Random(n3, n3) + RealScalar(2) * Mat::Identity(n3, n3)),
Bd(Bs),
ref(reference_kron<Scalar>(A, reference_kron<Scalar>(Bd, C))),
KR(makeKroneckerOperator(A, Bs, C)),
KL(makeKroneckerOperator(A, Bs), C) {}
Mat A;
Sparse Bs;
Mat C, Bd, ref;
RightNested KR;
LeftNested KL;
};
template <typename Scalar>
void test_kron_nested_products(Index n1, Index n2, Index n3) {
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using Sparse = SparseMatrix<Scalar>;
using RowSparse = SparseMatrix<Scalar, RowMajor>;
using Id = internal::kron_identity_factor<Scalar, Dynamic, Dynamic>;
const NestedKronecker<Scalar> f(n1, n2, n3);
const Mat& ref = f.ref;
STATIC_CHECK((std::is_same<decltype(makeKroneckerOperator(f.A, f.Bs, f.C)),
typename NestedKronecker<Scalar>::RightNested>::value));
VERIFY_IS_EQUAL(f.KR.rows(), n1 * n2 * n3);
VERIFY_IS_EQUAL(f.KL.cols(), n1 * n2 * n3);
check_kron_products(f.KR, ref);
check_kron_products(f.KL, ref);
VERIFY_IS_APPROX(Mat(f.KR), ref);
VERIFY_IS_APPROX(Mat(f.KL), ref);
Mat acc = Mat::Random(ref.rows(), ref.cols());
const Mat acc0 = acc;
acc += f.KL;
acc -= f.KR;
VERIFY_IS_APPROX(acc, acc0);
for (Index t = 0; t < 5; ++t) {
const Index i = internal::random<Index>(0, ref.rows() - 1), j = internal::random<Index>(0, ref.cols() - 1);
VERIFY_IS_APPROX(f.KR.coeff(i, j), ref(i, j));
VERIFY_IS_APPROX(f.KL.coeff(i, j), ref(i, j));
}
// Sparse materialization stores the products of the leaves' stored entries.
const Index nnz = n1 * n1 * f.Bs.nonZeros() * n3 * n3;
Sparse M;
M = f.KR;
VERIFY_IS_EQUAL(M.nonZeros(), nnz);
VERIFY_IS_APPROX(Mat(M), ref);
RowSparse MR;
MR = f.KL;
VERIFY_IS_EQUAL(MR.nonZeros(), nnz);
VERIFY_IS_APPROX(Mat(MR), ref);
// The transposition family recurses into the nested factor.
check_kron_products(f.KR.transpose(), Mat(ref.transpose()));
check_kron_products(f.KL.adjoint(), Mat(ref.adjoint()));
check_kron_products(f.KR.conjugate(), Mat(ref.conjugate()));
// The identity sandwich I_p (x) S (x) I_q of a 3-D finite-difference term.
const Index p = n1, q = n3;
auto K3 = makeKroneckerOperator(Mat::Identity(p, p), f.Bs, Mat::Identity(q, q));
STATIC_CHECK((std::is_same<decltype(K3), KroneckerOperator<Id, KroneckerOperator<Sparse, Id>>>::value));
const Mat ref3 = reference_kron<Scalar>(Mat::Identity(p, p), reference_kron<Scalar>(f.Bd, Mat::Identity(q, q)));
check_kron_products(K3, ref3);
Sparse M3;
M3 = K3;
VERIFY_IS_EQUAL(M3.nonZeros(), p * q * f.Bs.nonZeros());
VERIFY_IS_EQUAL(Mat(M3), ref3);
}
// A left-nested factor applies from the right: with a square identity on
// either side of it, and with a nested factor of its own.
template <typename Scalar>
void test_kron_left_nested_products(Index n1, Index n2, Index n3) {
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
const NestedKronecker<Scalar> f(n1, n2, n3);
const Index p = n1, q = n3;
const Mat Ip = Mat::Identity(p, p), Iq = Mat::Identity(q, q);
check_kron_products(makeKroneckerOperator(makeKroneckerOperator(Mat::Identity(p, p), f.Bs), Mat::Identity(q, q)),
reference_kron<Scalar>(reference_kron<Scalar>(Ip, f.Bd), Iq));
check_kron_products(makeKroneckerOperator(makeKroneckerOperator(f.Bs, Mat::Identity(q, q)), f.C),
reference_kron<Scalar>(reference_kron<Scalar>(f.Bd, Iq), f.C));
check_kron_products(makeKroneckerOperator(f.KR, f.C), reference_kron<Scalar>(f.ref, f.C));
}
// Solves factorize each leaf once; the inverse and determinant recurse.
template <typename Scalar>
void test_kron_nested_solve(Index n1, Index n2, Index n3) {
using Vec = Matrix<Scalar, Dynamic, 1>;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
const NestedKronecker<Scalar> f(n1, n2, n3);
const Mat& ref = f.ref;
const Vec b = Vec::Random(ref.rows());
VERIFY_IS_APPROX((ref * f.KR.solve(b)).eval(), b);
const Mat Bm = Mat::Random(ref.rows(), 3);
VERIFY_IS_APPROX((ref * f.KL.solve(Bm)).eval(), Bm);
VERIFY_IS_APPROX((Mat(f.KR.inverse()) * ref).eval(), Mat(Mat::Identity(ref.rows(), ref.cols())));
VERIFY_IS_APPROX(f.KR.determinant(), ref.determinant());
VERIFY_IS_APPROX(f.KL.determinant(), ref.determinant());
const Index p = n1, q = n3;
const auto K3 = makeKroneckerOperator(Mat::Identity(p, p), f.Bs, Mat::Identity(q, q));
const Mat ref3 = reference_kron<Scalar>(Mat::Identity(p, p), reference_kron<Scalar>(f.Bd, Mat::Identity(q, q)));
const Vec b3 = Vec::Random(ref3.rows());
VERIFY_IS_APPROX((ref3 * K3.solve(b3)).eval(), b3);
}
// The eigendecomposition and the SVD recurse into the nested factor.
template <typename Scalar>
void test_kron_nested_eigen(Index n1, Index n2, Index n3) {
using Complex = std::complex<typename NumTraits<Scalar>::Real>;
using CMat = Matrix<Complex, Dynamic, Dynamic>;
using CVec = Matrix<Complex, Dynamic, 1>;
using ColMajorCMat = Matrix<Complex, Dynamic, Dynamic, ColMajor>;
const NestedKronecker<Scalar> f(n1, n2, n3);
const CVec lambda = f.KL.eigenvalues();
const CMat V = f.KL.eigenvectors();
VERIFY_IS_APPROX((f.ref.template cast<Complex>() * V).eval(), (V * lambda.asDiagonal()).eval());
STATIC_CHECK((std::is_same<decltype(f.KR.eigenvectors()),
KroneckerOperator<ColMajorCMat, KroneckerOperator<ColMajorCMat, ColMajorCMat>>>::value));
const CVec mu = f.KR.eigenvalues();
const CMat W = f.KR.eigenvectors();
VERIFY_IS_APPROX((f.ref.template cast<Complex>() * W).eval(), (W * mu.asDiagonal()).eval());
}
template <typename Scalar>
void test_kron_nested_svd(Index n1, Index n2, Index n3) {
using RealScalar = typename NumTraits<Scalar>::Real;
using Mat = Matrix<Scalar, Dynamic, Dynamic>;
using RealVec = Matrix<RealScalar, Dynamic, 1>;
const NestedKronecker<Scalar> f(n1, n2, n3);
const Mat& ref = f.ref;
const RealVec s = f.KR.singularValues();
RealVec sv = s;
std::sort(sv.data(), sv.data() + sv.size(), std::greater<RealScalar>());
VERIFY_IS_APPROX(sv, RealVec(BDCSVD<Mat>(ref).singularValues()));
VERIFY_IS_EQUAL(f.KR.rank(), ref.rows());
// U and V nest like the operator and pair with the unsorted singular values.
const Mat U = f.KR.matrixU(), V = f.KR.matrixV();
VERIFY_IS_APPROX((U * s.asDiagonal() * V.adjoint()).eval(), ref);
VERIFY_IS_APPROX((U.adjoint() * U).eval(), Mat(Mat::Identity(ref.cols(), ref.cols())));
const Mat UL = f.KL.matrixU(), VL = f.KL.matrixV();
VERIFY_IS_APPROX((UL * f.KL.singularValues().asDiagonal() * VL.adjoint()).eval(), ref);
}
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_15((test_kron_solve<float>(5, 4)));
CALL_SUBTEST_15((test_kron_solve<std::complex<double>>(6, 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_26((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_27((test_kron_least_squares_rank_deficient<std::complex<double>>(5, 3, 4, 4)));
CALL_SUBTEST_3((test_kron_least_squares<float>(6, 4, 5, 3)));
CALL_SUBTEST_3((test_kron_least_squares_scaling<double>(6, 4, 3, 5)));
CALL_SUBTEST_28((test_kron_least_squares_scaling<std::complex<float>>(4, 5, 6, 3)));
// Eigendecomposition, SVD, inverse, determinant.
CALL_SUBTEST_4((test_kron_eigen<double>(4, 5)));
CALL_SUBTEST_4((test_kron_eigen<double>(1, 6)));
CALL_SUBTEST_16((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_20((test_kron_svd<std::complex<double>>(4, 3, 4, 4)));
CALL_SUBTEST_29((test_kron_svd<float>(4, 4, 4, 4)));
CALL_SUBTEST_4((test_kron_inverse_determinant<double>(4, 5)));
CALL_SUBTEST_30((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_17((test_kron_fixed<std::complex<float>, 2, 3, 3, 2>()));
CALL_SUBTEST_5((test_kron_fixed_solve<double, 2, 3>()));
CALL_SUBTEST_17((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_18((test_kron_aliased_product<std::complex<double>>(4, 3)));
CALL_SUBTEST_6((test_kron_aliased_resize<double>(4, 3, 2, 5)));
CALL_SUBTEST_18((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_18((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_18((test_kron_mixed_scalar<float>(3, 4, 2, 3)));
CALL_SUBTEST_19((test_kron_chunked_products<double>()));
CALL_SUBTEST_31((test_kron_chunked_solves<double>()));
CALL_SUBTEST_32((test_kron_chunked_products<std::complex<double>>()));
CALL_SUBTEST_33((test_kron_chunked_solves<std::complex<double>>()));
CALL_SUBTEST_31((test_kron_chunked_mixed_scalar<double>()));
// Numerical boundaries: product-level rank truncation, balanced determinant
// 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_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 and matrix-free CG.
CALL_SUBTEST_9((test_kron_diag_solve<double>(4, 7)));
CALL_SUBTEST_21((test_kron_diag_solve<float>(5, 4)));
CALL_SUBTEST_21((test_kron_diag_solve<std::complex<double>>(6, 4)));
CALL_SUBTEST_9((test_kron_diag_inverse_determinant<double>(4, 5)));
CALL_SUBTEST_30((test_kron_diag_inverse_determinant<std::complex<double>>(3, 4)));
CALL_SUBTEST_9((test_kron_diag_eigen<double>(4, 5)));
CALL_SUBTEST_9((test_kron_diag_svd<double>(4, 5)));
CALL_SUBTEST_9((test_kron_diag_rank<double>(4, 5)));
CALL_SUBTEST_9((test_kron_diag_least_squares<double>(4, 5)));
CALL_SUBTEST_34((test_kron_diag_eigen<std::complex<double>>(3, 4)));
CALL_SUBTEST_35((test_kron_diag_svd<std::complex<double>>(3, 4)));
CALL_SUBTEST_36((test_kron_diag_rank<std::complex<double>>(3, 4)));
CALL_SUBTEST_37((test_kron_diag_least_squares<std::complex<double>>(3, 4)));
CALL_SUBTEST_9(test_kron_diag_determinant_scaling());
CALL_SUBTEST_9(test_kron_diag_matrix_free_cg(6, 7));
// Sparse factors: products, materialization, the identity-Kronecker
// operators, the transposition family, mixed-scalar promotion.
CALL_SUBTEST_10((test_kron_sparse_product<double>(4, 5, 3, 4)));
CALL_SUBTEST_38((test_kron_sparse_product<float>(3, 4, 4, 2)));
CALL_SUBTEST_39((test_kron_sparse_product<std::complex<double>>(3, 4, 2, 3)));
CALL_SUBTEST_10((test_kron_sparse_product<double>(1, 1, 1, 1)));
CALL_SUBTEST_10((test_kron_sparse_materialize<double>(4, 5, 3, 4)));
CALL_SUBTEST_38((test_kron_sparse_materialize<std::complex<float>>(3, 2, 4, 3)));
CALL_SUBTEST_10((test_kron_sparse_identity<double>(5, 4, 3, 2)));
CALL_SUBTEST_39((test_kron_sparse_identity<std::complex<double>>(3, 3, 4, 2)));
CALL_SUBTEST_10((test_kron_sparse_transpose<double>(4, 5, 3, 2)));
CALL_SUBTEST_40((test_kron_sparse_transpose<std::complex<double>>(3, 4, 2, 5)));
CALL_SUBTEST_10((test_kron_sparse_mixed_scalar<double>(4, 3, 5)));
CALL_SUBTEST_40((test_kron_sparse_mixed_scalar<float>(3, 4, 2)));
CALL_SUBTEST_10(test_kron_sparse_stored_zeros(4, 3));
// Sparse factors through the solvers, inverse/determinant, the
// decomposition family and matrix-free CG.
CALL_SUBTEST_11((test_kron_sparse_solve<double>(4, 7)));
CALL_SUBTEST_22((test_kron_sparse_solve<float>(5, 4)));
CALL_SUBTEST_22((test_kron_sparse_solve<std::complex<double>>(6, 4)));
CALL_SUBTEST_11((test_kron_sparse_inverse_determinant<double>(4, 5)));
CALL_SUBTEST_22((test_kron_sparse_inverse_determinant<std::complex<double>>(3, 4)));
CALL_SUBTEST_11((test_kron_sparse_eigen<double>(4, 5)));
CALL_SUBTEST_11((test_kron_sparse_svd<double>(4, 5)));
CALL_SUBTEST_11((test_kron_sparse_rank<double>(4, 5)));
CALL_SUBTEST_11((test_kron_sparse_least_squares<double>(4, 5)));
CALL_SUBTEST_23((test_kron_sparse_eigen<std::complex<double>>(3, 4)));
CALL_SUBTEST_41((test_kron_sparse_svd<std::complex<double>>(3, 4)));
CALL_SUBTEST_42((test_kron_sparse_rank<std::complex<double>>(3, 4)));
CALL_SUBTEST_43((test_kron_sparse_least_squares<std::complex<double>>(3, 4)));
CALL_SUBTEST_11(test_kron_sparse_solve_laplacian(64, 4));
CALL_SUBTEST_11(test_kron_sparse_degenerate(4, 3));
CALL_SUBTEST_11(test_kron_sparse_determinant_scaling());
CALL_SUBTEST_11(test_kron_sparse_matrix_free_cg(6, 7));
CALL_SUBTEST_12((test_kron_sparse_determinant_subnormal<float, ColMajor>(1)));
CALL_SUBTEST_12((test_kron_sparse_determinant_subnormal<double, RowMajor>(1)));
CALL_SUBTEST_12((test_kron_sparse_determinant_subnormal<std::complex<float>, RowMajor>({1, 1})));
CALL_SUBTEST_12((test_kron_sparse_determinant_subnormal<std::complex<double>, ColMajor>({1, 1})));
CALL_SUBTEST_13((test_kron_sparse_nonfinite<float, ColMajor>()));
CALL_SUBTEST_13((test_kron_sparse_nonfinite<double, RowMajor>()));
CALL_SUBTEST_13((test_kron_sparse_nonfinite<std::complex<float>, RowMajor>()));
CALL_SUBTEST_13((test_kron_sparse_nonfinite<std::complex<double>, ColMajor>()));
CALL_SUBTEST_14((test_kron_determinant_flushed_subnormal<float>()));
CALL_SUBTEST_14((test_kron_determinant_flushed_subnormal<double>()));
CALL_SUBTEST_14((test_kron_determinant_flushed_subnormal<std::complex<float>>()));
CALL_SUBTEST_14((test_kron_determinant_flushed_subnormal<std::complex<double>>()));
CALL_SUBTEST_14((test_kron_determinant_flushed_pivot<float>()));
CALL_SUBTEST_14((test_kron_determinant_flushed_pivot<double>()));
CALL_SUBTEST_14((test_kron_determinant_flushed_pivot<std::complex<float>>()));
CALL_SUBTEST_14((test_kron_determinant_flushed_pivot<std::complex<double>>()));
// Identity() factors and nested Kronecker factors.
CALL_SUBTEST_24((test_kron_identity_factor<double>(3, 4, 5)));
CALL_SUBTEST_44((test_kron_identity_factor<float>(4, 3, 2)));
CALL_SUBTEST_45((test_kron_identity_factor<std::complex<double>>(2, 3, 4)));
CALL_SUBTEST_24((test_kron_identity_factor<double>(1, 1, 1)));
CALL_SUBTEST_25((test_kron_nested_products<double>(3, 4, 2)));
CALL_SUBTEST_31((test_kron_nested_solve<double>(3, 4, 2)));
CALL_SUBTEST_46((test_kron_nested_eigen<double>(3, 4, 2)));
CALL_SUBTEST_31((test_kron_nested_svd<double>(3, 4, 2)));
CALL_SUBTEST_47((test_kron_nested_products<std::complex<double>>(2, 3, 3)));
CALL_SUBTEST_48((test_kron_nested_solve<std::complex<double>>(2, 3, 3)));
CALL_SUBTEST_49((test_kron_nested_eigen<std::complex<double>>(2, 3, 3)));
CALL_SUBTEST_50((test_kron_nested_svd<std::complex<double>>(2, 3, 3)));
CALL_SUBTEST_51((test_kron_nested_products<float>(2, 2, 3)));
CALL_SUBTEST_52((test_kron_nested_solve<float>(2, 2, 3)));
CALL_SUBTEST_53((test_kron_nested_eigen<float>(2, 2, 3)));
CALL_SUBTEST_54((test_kron_nested_svd<float>(2, 2, 3)));
CALL_SUBTEST_55((test_kron_left_nested_products<double>(3, 4, 2)));
CALL_SUBTEST_56((test_kron_left_nested_products<std::complex<double>>(2, 3, 3)));
}
}