blob: 20bb66a01fa3ab95593f30acdb6610281886ada0 [file]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Copyright (C) 2012 The Eigen Authors
//
// 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-License-Identifier: MPL-2.0
#define EIGEN_RUNTIME_NO_MALLOC
#include "main.h"
#include <Eigen/Eigenvalues>
/* this test covers the following files:
ComplexQZ.h
*/
template <typename MatrixType>
void generate_random_matrix_pair(const Index dim, MatrixType& A, MatrixType& B) {
A.setRandom(dim, dim);
B.setRandom(dim, dim);
// Zero out each row of B to with a probability of 10%.
for (int i = 0; i < dim; i++) {
if (internal::random<int>(0, 10) == 0) B.row(i).setZero();
}
}
template <typename MatrixType>
void complex_qz(const MatrixType& A, const MatrixType& B) {
using std::abs;
const Index dim = A.rows();
ComplexQZ<MatrixType> qz(A, B);
VERIFY_IS_EQUAL(qz.info(), Success);
auto T = qz.matrixT(), S = qz.matrixS();
bool is_all_zero_T = true, is_all_zero_S = true;
using RealScalar = typename MatrixType::RealScalar;
RealScalar tol = dim * 10 * NumTraits<RealScalar>::epsilon();
for (Index j = 0; j < dim; j++) {
for (Index i = j + 1; i < dim; i++) {
if (std::abs(T(i, j)) > tol) {
std::cerr << std::abs(T(i, j)) << std::endl;
is_all_zero_T = false;
}
if (std::abs(S(i, j)) > tol) {
std::cerr << std::abs(S(i, j)) << std::endl;
is_all_zero_S = false;
}
}
}
VERIFY_IS_EQUAL(is_all_zero_T, true);
VERIFY_IS_EQUAL(is_all_zero_S, true);
VERIFY_IS_APPROX(qz.matrixQ() * qz.matrixS() * qz.matrixZ(), A);
VERIFY_IS_APPROX(qz.matrixQ() * qz.matrixT() * qz.matrixZ(), B);
VERIFY_IS_APPROX(qz.matrixQ() * qz.matrixQ().adjoint(), MatrixType::Identity(dim, dim));
VERIFY_IS_APPROX(qz.matrixZ() * qz.matrixZ().adjoint(), MatrixType::Identity(dim, dim));
}
template <typename MatrixType, typename QZType>
void verify_complex_qz_convergence(const MatrixType& a, const MatrixType& b, const QZType& qz) {
using RealScalar = typename MatrixType::RealScalar;
// Accumulated rounding in the unitary transformations is O(n * epsilon). stableNorm, since the squared entries of
// complex_qz_scaled's pencils underflow.
const RealScalar tolerance = RealScalar(128 * a.rows()) * NumTraits<RealScalar>::epsilon();
VERIFY_IS_EQUAL(qz.info(), Success);
VERIFY((a - qz.matrixQ() * qz.matrixS() * qz.matrixZ()).stableNorm() <= tolerance * a.stableNorm());
VERIFY((b - qz.matrixQ() * qz.matrixT() * qz.matrixZ()).stableNorm() <= tolerance * b.stableNorm());
VERIFY(qz.matrixQ().isUnitary(tolerance));
VERIFY(qz.matrixZ().isUnitary(tolerance));
const MatrixType lowerS = qz.matrixS().template triangularView<StrictlyLower>();
const MatrixType lowerT = qz.matrixT().template triangularView<StrictlyLower>();
VERIFY(lowerS.stableNorm() <= tolerance * a.stableNorm());
VERIFY(lowerT.stableNorm() <= tolerance * b.stableNorm());
}
template <typename MatrixType>
void complex_qz_exceptional_shift() {
using Scalar = typename MatrixType::Scalar;
using RealScalar = typename MatrixType::RealScalar;
for (Index dim : {3, 4, 5, 8}) {
for (Scalar phase : {Scalar(1), Scalar(0, 1)}) {
// With corner -1 and phase 1, the exceptional shift 2 is equidistant from the eigenvalues exp(+-i*pi/dim).
for (RealScalar corner : {RealScalar(1), RealScalar(-1)}) {
// The trailing 2x2 block has two zero shifts: ordinary double shifts cycle without deflation.
MatrixType a = MatrixType::Zero(dim, dim), b = MatrixType::Identity(dim, dim);
a.diagonal(-1).setConstant(phase);
a(0, dim - 1) = corner * phase;
for (Index j = 0; j < dim; ++j) {
b(j, j) = Scalar(1 << (j % 3));
a.col(j) *= b(j, j);
}
const RealScalar tolerance = RealScalar(128 * dim) * NumTraits<RealScalar>::epsilon();
ComplexQZ<MatrixType> qz(a, b);
verify_complex_qz_convergence(a, b, qz);
// a*b^-1 = phase*P with P^dim = corner*I, so lambda_k = phase*w_k with w_k^dim = corner. These are
// 2*sin(pi/dim) apart with unit condition number (y^* b x = 1 for unit left y and |x| <= 1), so each moves by
// at most the backward error, including the dropped strictly lower parts of S and T.
const Matrix<Scalar, Dynamic, 1> lambda = qz.matrixS().diagonal().cwiseQuotient(qz.matrixT().diagonal());
const RealScalar eigenvalueTolerance = RealScalar(2) * tolerance * (a.norm() + b.norm());
for (Index k = 0; k < dim; ++k) {
const RealScalar angle = (RealScalar(2 * k) + (corner < 0 ? RealScalar(1) : RealScalar(0))) *
RealScalar(EIGEN_PI) / RealScalar(dim);
const Scalar expected = phase * std::polar(RealScalar(1), angle);
VERIFY((lambda.array() - expected).abs().minCoeff() <= eigenvalueTolerance);
}
ComplexQZ<MatrixType> limited(a, b, true, 1);
VERIFY_IS_EQUAL(limited.info(), NoConvergence);
VERIFY_IS_EQUAL(limited.iterations(), 1);
MatrixType inplaceA = a, inplaceB = b;
ComplexQZ<Ref<MatrixType>> inplace(inplaceA, inplaceB);
verify_complex_qz_convergence(a, b, inplace);
const MatrixType s = qz.matrixS(), t = qz.matrixT();
qz.compute(a, b, false);
VERIFY_IS_EQUAL(qz.info(), Success);
VERIFY((qz.matrixS() - s).norm() <= tolerance * a.norm());
VERIFY((qz.matrixT() - t).norm() <= tolerance * b.norm());
qz.compute(a, b);
verify_complex_qz_convergence(a, b, qz);
}
}
}
}
// The 2x2 deflation compared the diagonal of T with an absolute epsilon, so a pencil with ||B|| << 1 failed.
template <typename MatrixType>
void complex_qz_small_b() {
using RealScalar = typename MatrixType::RealScalar;
for (Index dim : {2, 3, 5}) {
const MatrixType a = MatrixType::Random(dim, dim),
b = MatrixType::Random(dim, dim) * (NumTraits<RealScalar>::epsilon() / 1024);
ComplexQZ<MatrixType> qz(a, b);
verify_complex_qz_convergence(a, b, qz);
}
}
// The DARE example "darex #14" of SciPy's solve_discrete_are tests, from Benner, Laub and Mehrmann, "A Collection of
// Benchmark Examples for the Numerical Solution of Algebraic Riccati Equations II: Discrete-Time Case", TU
// Chemnitz-Zwickau SPC 95_23 (1995): the balanced symplectic pencil with the column of R deflated. Eigenvalues 0 (x3,
// one Jordan chain), infinity (x3) and 1 -+ sqrt(5) 1e-8. With AVX2 and FMA, an iterate had S(1, 0) = 1.8e-17 next to
// diagonal entries 2.4e-15 and 3e-3; sweeps starting at row 0 then stalled for 400 iterations.
template <typename MatrixType>
void complex_qz_darex14() {
using Scalar = typename MatrixType::Scalar;
using RealScalar = typename MatrixType::RealScalar;
const Index m = 4, p = 2 * m + 1;
// H = [A 0 B; -Q I 0; 0 0 R], J = [I 0 0; 0 A^* 0; 0 -B^* 0] with A = (1 - 1e-8) e0 e0^T + shift down,
// B = 1e-8 e0, Q = e3 e3^T and R = 1/4.
MatrixType H = MatrixType::Zero(p, p), J = MatrixType::Zero(p, p);
H(0, 0) = J(m, m) = Scalar(RealScalar(1) - RealScalar(1e-8));
for (Index i = 1; i < m; ++i) H(i, i - 1) = J(m + i - 1, m + i) = Scalar(1);
H(0, 2 * m) = Scalar(RealScalar(1e-8));
H(m + 3, 3) = Scalar(-1);
H.block(m, m, m, m).setIdentity();
H(2 * m, 2 * m) = Scalar(RealScalar(0.25));
J.topLeftCorner(m, m).setIdentity();
J(2 * m, m) = Scalar(RealScalar(-1e-8));
// Symplectic balancing (Benner, SIAM J. Sci. Comput. 22(5), 2001) by diag(D, D^-1, 1/2) with
// D = diag(2^20, 2^14, 2^8, 2^2).
Matrix<RealScalar, Dynamic, 1> scale(p);
const int exponents[m] = {20, 14, 8, 2};
for (Index i = 0; i < m; ++i) {
scale(i) = std::ldexp(RealScalar(1), exponents[i]);
scale(m + i) = std::ldexp(RealScalar(1), -exponents[i]);
}
scale(2 * m) = RealScalar(0.5);
H = scale.template cast<Scalar>().asDiagonal() * H * scale.cwiseInverse().template cast<Scalar>().asDiagonal();
J = scale.template cast<Scalar>().asDiagonal() * J * scale.cwiseInverse().template cast<Scalar>().asDiagonal();
const HouseholderQR<MatrixType> qr(H.rightCols(1));
const MatrixType a = (qr.householderQ().adjoint() * H.leftCols(2 * m)).bottomRows(2 * m);
const MatrixType b = (qr.householderQ().adjoint() * J.leftCols(2 * m)).bottomRows(2 * m);
ComplexQZ<MatrixType> qz(a, b);
verify_complex_qz_convergence(a, b, qz);
}
// a = diag(A1, s A2), b = I with A2 = U diag(lambda) U^* normal and |lambda_k| in [1/2, 1]. Relative deflation
// decomposes the small block as if it were alone, so by Bauer-Fike its eigenvalues move by at most its backward error,
// ~128 n eps s. A normwise deflation threshold eps ||a|| would allow errors ~eps ||A1|| >> s.
template <typename MatrixType>
void complex_qz_reducible(typename MatrixType::RealScalar s) {
using Scalar = typename MatrixType::Scalar;
using RealScalar = typename MatrixType::RealScalar;
using Vector = Matrix<Scalar, Dynamic, 1>;
const Index n1 = 8, n2 = 8, n = n1 + n2;
Vector lambda(n2);
for (Index k = 0; k < n2; ++k)
lambda(k) = std::polar(internal::random<RealScalar>(RealScalar(0.5), RealScalar(1)),
internal::random<RealScalar>(-RealScalar(EIGEN_PI), RealScalar(EIGEN_PI)));
const MatrixType U = HouseholderQR<MatrixType>(MatrixType::Random(n2, n2)).householderQ();
MatrixType a = MatrixType::Zero(n, n);
a.topLeftCorner(n1, n1).setRandom();
a.bottomRightCorner(n2, n2) = s * U * lambda.asDiagonal() * U.adjoint();
const MatrixType b = MatrixType::Identity(n, n);
ComplexQZ<MatrixType> qz(a, b);
verify_complex_qz_convergence(a, b, qz);
const Vector computed = qz.matrixS().diagonal().cwiseQuotient(qz.matrixT().diagonal());
const RealScalar tolerance = RealScalar(128 * n) * NumTraits<RealScalar>::epsilon() * s;
for (Index k = 0; k < n2; ++k) VERIFY((computed.array() - s * lambda(k)).abs().minCoeff() <= tolerance);
}
// Graded pencils leave 2x2 blocks that the closed-form reduction in reduceDiagonal2x2block does not deflate on its own.
template <typename MatrixType>
void complex_qz_graded() {
using RealScalar = typename MatrixType::RealScalar;
for (Index dim : {4, 8}) {
for (int trial = 0; trial < 50; ++trial) {
MatrixType a = MatrixType::Random(dim, dim), b = MatrixType::Random(dim, dim);
for (Index i = 0; i < dim; ++i) {
const RealScalar grade = std::pow(RealScalar(10), -RealScalar(8 * i) / RealScalar(dim));
a.row(i) *= grade;
b.col(i) *= grade;
}
ComplexQZ<MatrixType> qz(a, b);
verify_complex_qz_convergence(a, b, qz);
}
}
}
// a = s N for the down-shift N, b = I. At s = 1 the 2x2 blocks have the eigenvalue 0, for which a left rotation formed
// from lambda T is the identity and leaves T(i+1, i) in place. At s = sqrt(min) eps the products of the sweep-start
// test underflow to 0, and an unscaled test accepted the first row it tried (0 <= 0) and dropped fill of size s.
template <typename MatrixType>
void complex_qz_scaled() {
using Scalar = typename MatrixType::Scalar;
using RealScalar = typename MatrixType::RealScalar;
const RealScalar tiny = std::sqrt((std::numeric_limits<RealScalar>::min)()) * NumTraits<RealScalar>::epsilon();
for (RealScalar s : {RealScalar(1), tiny}) {
for (Index dim : {4, 5, 8}) {
MatrixType a = MatrixType::Zero(dim, dim);
a.diagonal(-1).setConstant(Scalar(s));
const MatrixType b = MatrixType::Identity(dim, dim);
ComplexQZ<MatrixType> qz(a, b);
verify_complex_qz_convergence(a, b, qz);
}
}
}
// a = V J W^*, b = V W^* with J the 2x2 Jordan block of the eigenvalue 0. On a defective block, refinement steps in
// reduceDiagonal2x2block can lower |S(1, 0)| / (|S(0, 0)| + |S(1, 1)|) by growing the diagonal while |S(1, 0)| grows
// past eps ||S||.
template <typename MatrixType>
void complex_qz_jordan() {
using Scalar = typename MatrixType::Scalar;
MatrixType J = MatrixType::Zero(2, 2);
J(0, 1) = Scalar(1);
for (int trial = 0; trial < 100; ++trial) {
for (bool real : {true, false}) {
MatrixType V = MatrixType::Random(2, 2), W = MatrixType::Random(2, 2);
if (real) {
V = V.real().template cast<Scalar>();
W = W.real().template cast<Scalar>();
}
const MatrixType a = V * J * W.adjoint(), b = V * W.adjoint();
ComplexQZ<MatrixType> qz(a, b);
verify_complex_qz_convergence(a, b, qz);
}
}
}
// Block Householder operations still allocate at larger sizes (issue #3163).
template <typename MatrixType>
void complex_qz_no_malloc() {
const Index dim = 24;
MatrixType A = MatrixType::Random(dim, dim), B = MatrixType::Random(dim, dim);
ComplexQZ<MatrixType> qz(dim);
internal::set_is_malloc_allowed(false);
qz.compute(A, B, false);
internal::set_is_malloc_allowed(true);
VERIFY_IS_EQUAL(qz.info(), Success);
const MatrixType S = qz.matrixS(), T = qz.matrixT();
ComplexQZ<MatrixType> resized(1, false);
resized.compute(A, B, false);
internal::set_is_malloc_allowed(false);
resized.compute(A, B, false);
internal::set_is_malloc_allowed(true);
VERIFY_IS_EQUAL(resized.info(), Success);
VERIFY_IS_APPROX(resized.matrixS(), S);
VERIFY_IS_APPROX(resized.matrixT(), T);
for (int i = 0; i < 2; ++i) {
internal::set_is_malloc_allowed(false);
qz.compute(A, B, true);
internal::set_is_malloc_allowed(true);
VERIFY_IS_EQUAL(qz.info(), Success);
VERIFY_IS_APPROX(qz.matrixS(), S);
VERIFY_IS_APPROX(qz.matrixT(), T);
VERIFY_IS_APPROX(qz.matrixQ() * qz.matrixS() * qz.matrixZ(), A);
VERIFY_IS_APPROX(qz.matrixQ() * qz.matrixT() * qz.matrixZ(), B);
VERIFY_IS_APPROX(qz.matrixQ() * qz.matrixQ().adjoint(), MatrixType::Identity(dim, dim));
VERIFY_IS_APPROX(qz.matrixZ() * qz.matrixZ().adjoint(), MatrixType::Identity(dim, dim));
}
}
EIGEN_DECLARE_TEST(complex_qz) {
CALL_SUBTEST_3((complex_qz_no_malloc<MatrixXcf>()));
CALL_SUBTEST_4((complex_qz_no_malloc<MatrixXcd>()));
CALL_SUBTEST_5((complex_qz_no_malloc<Matrix<std::complex<float>, Dynamic, Dynamic, RowMajor>>()));
CALL_SUBTEST_6((complex_qz_no_malloc<Matrix<std::complex<double>, Dynamic, Dynamic, RowMajor>>()));
CALL_SUBTEST_7((complex_qz_exceptional_shift<MatrixXcf>()));
CALL_SUBTEST_8((complex_qz_exceptional_shift<MatrixXcd>()));
CALL_SUBTEST_7((complex_qz_small_b<MatrixXcf>()));
CALL_SUBTEST_8((complex_qz_small_b<MatrixXcd>()));
CALL_SUBTEST_8((complex_qz_darex14<MatrixXcd>()));
CALL_SUBTEST_10((complex_qz_darex14<Matrix<std::complex<double>, Dynamic, Dynamic, RowMajor>>()));
CALL_SUBTEST_7((complex_qz_reducible<MatrixXcf>(1e-6f)));
CALL_SUBTEST_8((complex_qz_reducible<MatrixXcd>(1e-10)));
CALL_SUBTEST_9((complex_qz_reducible<Matrix<std::complex<float>, Dynamic, Dynamic, RowMajor>>(1e-6f)));
CALL_SUBTEST_10((complex_qz_reducible<Matrix<std::complex<double>, Dynamic, Dynamic, RowMajor>>(1e-10)));
CALL_SUBTEST_7((complex_qz_graded<MatrixXcf>()));
CALL_SUBTEST_8((complex_qz_graded<MatrixXcd>()));
CALL_SUBTEST_9((complex_qz_graded<Matrix<std::complex<float>, Dynamic, Dynamic, RowMajor>>()));
CALL_SUBTEST_10((complex_qz_graded<Matrix<std::complex<double>, Dynamic, Dynamic, RowMajor>>()));
CALL_SUBTEST_7((complex_qz_scaled<MatrixXcf>()));
CALL_SUBTEST_8((complex_qz_scaled<MatrixXcd>()));
CALL_SUBTEST_9((complex_qz_scaled<Matrix<std::complex<float>, Dynamic, Dynamic, RowMajor>>()));
CALL_SUBTEST_10((complex_qz_scaled<Matrix<std::complex<double>, Dynamic, Dynamic, RowMajor>>()));
CALL_SUBTEST_7((complex_qz_jordan<MatrixXcf>()));
CALL_SUBTEST_8((complex_qz_jordan<MatrixXcd>()));
CALL_SUBTEST_9((complex_qz_jordan<Matrix<std::complex<float>, Dynamic, Dynamic, RowMajor>>()));
CALL_SUBTEST_10((complex_qz_jordan<Matrix<std::complex<double>, Dynamic, Dynamic, RowMajor>>()));
CALL_SUBTEST_9((complex_qz_exceptional_shift<Matrix<std::complex<float>, Dynamic, Dynamic, RowMajor>>()));
CALL_SUBTEST_10((complex_qz_exceptional_shift<Matrix<std::complex<double>, Dynamic, Dynamic, RowMajor>>()));
for (int i = 0; i < g_repeat; i++) {
// Check for very small, fixed-sized double- and float complex matrices
Eigen::Matrix2cd A_2x2, B_2x2;
A_2x2.setRandom();
B_2x2.setRandom();
B_2x2.row(1).setZero();
Eigen::Matrix3cf A_3x3, B_3x3;
A_3x3.setRandom();
B_3x3.setRandom();
B_3x3.col(i % 3).setRandom();
CALL_SUBTEST_1(complex_qz(A_2x2, B_2x2));
CALL_SUBTEST_2(complex_qz(A_3x3, B_3x3));
// Test for float complex matrices
const Index dim = internal::random<Index>(15, 80);
Eigen::MatrixXcf A_float, B_float;
generate_random_matrix_pair(dim, A_float, B_float);
CALL_SUBTEST_3(complex_qz(A_float, B_float));
CALL_SUBTEST_5((complex_qz(Matrix<std::complex<float>, Dynamic, Dynamic, RowMajor>(A_float),
Matrix<std::complex<float>, Dynamic, Dynamic, RowMajor>(B_float))));
// Test for double complex matrices
Eigen::MatrixXcd A_double, B_double;
generate_random_matrix_pair(dim, A_double, B_double);
CALL_SUBTEST_4(complex_qz(A_double, B_double));
CALL_SUBTEST_6((complex_qz(Matrix<std::complex<double>, Dynamic, Dynamic, RowMajor>(A_double),
Matrix<std::complex<double>, Dynamic, Dynamic, RowMajor>(B_double))));
}
}