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