blob: 1a6f1cd40aa9ddbc79013455661413652a594054 [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-FileCopyrightText: 2001-2002 Enthought, Inc.
// SPDX-FileCopyrightText: 2003 SciPy Developers
// SPDX-License-Identifier: MPL-2.0 AND BSD-3-Clause
//
// This file incorporates work covered by the following copyright and
// permission notice:
//
// dare_scipy_complex_cases(), dare_scipy_real_cases() and dare_generalized_scipy_cases()
// are adapted from the solve_discrete_are tests of SciPy 1.18.0 (scipy/linalg/tests/test_solvers.py and
// scipy/linalg/tests/data/gendare_20170120_data.npz):
//
// Copyright (c) 2001-2002 Enthought, Inc. 2003, SciPy Developers.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following
// disclaimer in the documentation and/or other materials provided
// with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "main.h"
#include <contrib/Eigen/Riccati>
#include <algorithm>
#include <vector>
namespace {
// Residual of A^* X A - E^* X E - (A^* X B + S) (R + B^* X B)^-1 (B^* X A + S^*) + Q.
template <typename MatrixType>
MatrixType dare_residual(const MatrixType& A, const MatrixType& B, const MatrixType& Q, const MatrixType& R,
const MatrixType& E, const MatrixType& S, const MatrixType& X) {
const MatrixType AXB = A.adjoint() * X * B + S;
const MatrixType gain = (R + B.adjoint() * X * B).fullPivLu().solve(AXB.adjoint());
return A.adjoint() * X * A - E.adjoint() * X * E - AXB * gain + Q;
}
template <typename MatrixType>
MatrixType dare_residual(const MatrixType& A, const MatrixType& B, const MatrixType& Q, const MatrixType& R,
const MatrixType& X) {
return dare_residual(A, B, Q, R, MatrixType::Identity(A.rows(), A.rows()).eval(),
MatrixType::Zero(B.rows(), B.cols()).eval(), X);
}
// Solves the DARE and checks the residual entrywise against SciPy's absolute tolerance for the case.
template <typename MatrixType>
void check_dare(const MatrixType& A, const MatrixType& B, const MatrixType& Q, const MatrixType& R,
typename MatrixType::RealScalar atol) {
DiscreteAlgebraicRiccatiSolver<MatrixType> dare(A, B, Q, R);
VERIFY_IS_EQUAL(dare.info(), Success);
const MatrixType& X = dare.solution();
VERIFY_IS_EQUAL(X, X.adjoint());
VERIFY(dare_residual(A, B, Q, R, X).cwiseAbs().maxCoeff() <= atol);
}
template <typename MatrixType>
void check_generalized_dare(const MatrixType& A, const MatrixType& B, const MatrixType& Q, const MatrixType& R,
const MatrixType& E, const MatrixType& S, typename MatrixType::RealScalar atol) {
DiscreteAlgebraicRiccatiSolver<MatrixType> dare(A, B, Q, R, E, S);
VERIFY_IS_EQUAL(dare.info(), Success);
const MatrixType& X = dare.solution();
VERIFY_IS_EQUAL(X, X.adjoint());
VERIFY(dare_residual(A, B, Q, R, E, S, X).cwiseAbs().maxCoeff() <= atol);
}
// Solves a real test case in the arithmetic of MatrixType; the real cases are also solved in complex arithmetic to
// exercise the ComplexQZ code path.
template <typename MatrixType>
void check_real_dare(const MatrixXd& A, const MatrixXd& B, const MatrixXd& Q, const MatrixXd& R, double atol) {
using Scalar = typename MatrixType::Scalar;
check_dare<MatrixType>(A.cast<Scalar>(), B.cast<Scalar>(), Q.cast<Scalar>(), R.cast<Scalar>(), atol);
}
// Backward error check for a computed solution X:
// rho = ||res||_F / (m eps (||A^* X A||_F + ||E^* X E||_F + ||Q||_F) kappa) <= 30,
// kappa = max(1, ||E^* X E||_F / ||Q||_F),
// where kappa accounts for the amplification of rounding errors by a large solution: for the problems with B scaled by
// 10^-k, the residual of an accurately computed solution grows in proportion to ||X|| / ||Q||, also with SciPy
// (LAPACK), up to 1e10 eps for double. Over 160000 solves of dare_random() problems in all four scalar types, rho had
// p99.9 <= 3.9 and maximum 9.7.
template <typename DynMatrix>
void verify_dare_backward_error(const DynMatrix& A, const DynMatrix& B, const DynMatrix& Q, const DynMatrix& R,
const DynMatrix& E, const DynMatrix& S, const DynMatrix& X) {
using RealScalar = typename NumTraits<typename DynMatrix::Scalar>::Real;
VERIFY(X.allFinite());
const RealScalar normEXE = (E.adjoint() * X * E).norm();
const RealScalar scale = (A.adjoint() * X * A).norm() + normEXE + Q.norm();
const RealScalar kappa = numext::maxi(RealScalar(1), normEXE / Q.norm());
const RealScalar bound = RealScalar(30) * RealScalar(A.rows()) * NumTraits<RealScalar>::epsilon() * scale * kappa;
VERIFY(dare_residual(A, B, Q, R, E, S, X).norm() <= bound);
}
// The closed-loop pencil (A - B K, E), K = (R + B^* X B)^-1 (B^* X A + S^*), of the stabilizing solution is stable.
template <typename DynMatrix>
void verify_stabilizing(const DynMatrix& A, const DynMatrix& B, const DynMatrix& R, const DynMatrix& E,
const DynMatrix& S, const DynMatrix& X) {
using RealScalar = typename NumTraits<typename DynMatrix::Scalar>::Real;
using ComplexMatrix = Matrix<std::complex<RealScalar>, Dynamic, Dynamic>;
const DynMatrix K = (R + B.adjoint() * X * B).fullPivLu().solve(B.adjoint() * X * A + S.adjoint());
const ComplexMatrix closedLoop = E.fullPivLu().solve(A - B * K).template cast<std::complex<RealScalar>>();
VERIFY(ComplexEigenSolver<ComplexMatrix>(closedLoop).eigenvalues().cwiseAbs().maxCoeff() < RealScalar(1));
}
} // namespace
#if defined(EIGEN_TEST_PART_7) || defined(EIGEN_TEST_PART_ALL)
// The test cases of scipy.linalg.solve_discrete_are with complex data.
void dare_scipy_complex_cases() {
using cd = std::complex<double>;
{ // complex A; real B, Q, R
MatrixXcd A(2, 2), B(2, 1), Q(2, 2), R(1, 1);
A << cd(2, 0), cd(1, -2), cd(0, 0), cd(0, -3);
B << 0, 1;
Q << 1, 0, 0, 2;
R << 1;
check_dare(A, B, Q, R, 1.5e-12);
}
{ // real A, Q, R; complex B
MatrixXcd A(2, 2), B(2, 1), Q(2, 2), R(1, 1);
A << 2, 1, 0, -1;
B << cd(0, -2), cd(0, 1);
Q << 1, 0, 0, 2;
R << 1;
check_dare(A, B, Q, R, 1.5e-14);
}
{ // real A, B; complex Q, R
MatrixXcd A(2, 2), B(2, 2), Q(2, 2), R(2, 2);
A << 3, 1, 0, -1;
B << 1, 2, 1, 3;
Q << cd(1, 0), cd(1, 1), cd(1, -1), cd(2, 0);
R << cd(2, 0), cd(0, -2), cd(0, 2), cd(3, 0);
check_dare(A, B, Q, R, 1.5e-13);
}
}
#endif
// The test cases of scipy.linalg.solve_discrete_are with real data, most of them from
// P. Benner, A. J. Laub, V. Mehrmann, "A Collection of Benchmark Examples for the Numerical Solution of Algebraic
// Riccati Equations II: Discrete-Time Case", Tech. Report SPC 95_23, TU Chemnitz-Zwickau, 1995 (DAREX).
template <typename MatrixType>
void dare_scipy_real_cases() {
{ // SciPy gh-2251
MatrixXd A(3, 3), B(3, 1);
A << 0.63399379, 0.54906824, 0.76253406, 0.5404729, 0.53745766, 0.08731853, 0.27524045, 0.84922129, 0.4681622;
B << 0.96861695, 0.05532739, 0.78934047;
check_real_dare<MatrixType>(A, B, MatrixXd::Identity(3, 3), MatrixXd::Identity(1, 1), 1.5e-14);
}
{ // DAREX 1
MatrixXd A(2, 2), B(2, 1), Q(2, 2), R(1, 1);
A << 4, 3, -4.5, -3.5;
B << 1, -1;
Q << 9, 6, 6, 4;
R << 1;
check_real_dare<MatrixType>(A, B, Q, R, 1.5e-13);
}
{ // DAREX 2
MatrixXd A(2, 2), B(2, 2), Q(2, 2), R(2, 2);
A << 0.9512, 0, 0, 0.9048;
B << 4.877, 4.877, -1.1895, 3.569;
Q << 0.005, 0, 0, 0.02;
R << 1. / 3, 0, 0, 3;
check_real_dare<MatrixType>(A, B, Q, R, 1.5e-16);
}
{ // DAREX 3: singular R
MatrixXd A(2, 2), B(2, 1), Q(2, 2), R(1, 1);
A << 2, -1, 1, 0;
B << 1, 0;
Q << 0, 0, 0, 1;
R << 0;
// SciPy's bound of 1.5e-18 holds there only because LAPACK happens to round to the exact solution; X has O(1)
// entries, so a residual of a few eps is backward stable.
check_real_dare<MatrixType>(A, B, Q, R, 4 * NumTraits<double>::epsilon());
}
{ // DAREX 4 (without the generalized term S)
MatrixXd A(2, 2), B(2, 2), Q(2, 2), R(2, 2);
A << 0, 1, 0, -1;
B << 1, 0, 2, 1;
Q << -4, -4, -4, 7;
Q /= 11;
R << 9, 3, 3, 1;
check_real_dare<MatrixType>(A, B, Q, R, 1.5e-14);
}
{ // DAREX 5
MatrixXd A(2, 2), B(2, 1), Q(2, 2), R(1, 1);
A << 0, 1, 0, 0;
B << 0, 1;
Q << 1, 2, 2, 4;
R << 1;
check_real_dare<MatrixType>(A, B, Q, R, 1.5e-14);
}
{ // DAREX 6
MatrixXd A(4, 4), B(4, 2), Q(4, 4);
A << 0.998, 0.067, 0, 0, -.067, 0.998, 0, 0, 0, 0, 0.998, 0.153, 0, 0, -.153, 0.998;
B << 0.0033, 0.0200, 0.1000, -.0007, 0.0400, 0.0073, -.0028, 0.1000;
Q << 1.87, 0, 0, -0.244, 0, 0.744, 0.205, 0, 0, 0.205, 0.589, 0, -0.244, 0, 0, 1.048;
check_real_dare<MatrixType>(A, B, Q, MatrixXd::Identity(2, 2), 1.5e-13);
}
{ // DAREX 7
MatrixXd A(4, 4), B(4, 2);
A << 0.984750, -.079903, 0.0009054, -.0010765, 0.041588, 0.998990, -.0358550, 0.0126840, -.546620, 0.044916,
-.3299100, 0.1931800, 2.662400, -.100450, -.9245500, -.2632500;
B << 0.0037112, 0.0007361, -.0870510, 9.3411e-6, -1.198440, -4.1378e-4, -3.192700, 9.2535e-4;
check_real_dare<MatrixType>(A, B, MatrixXd::Identity(4, 4) * 1e-2, MatrixXd::Identity(2, 2), 1.5e-14);
}
{ // DAREX 8
MatrixXd A(4, 4), B(4, 4), Q(4, 4);
A << -0.6, -2.2, -3.6, -5.400018, 1.0, 0.6, 0.8, 3.399982, 0.0, 1.0, 1.8, 3.799982, 0.0, 0.0, 0.0, -0.999982;
B << 1.0, -1.0, -1.0, -1.0, 0.0, 1.0, -1.0, -1.0, 0.0, 0.0, 1.0, -1.0, 0.0, 0.0, 0.0, 1.0;
Q << 2, 1, 3, 6, 1, 2, 2, 5, 3, 2, 6, 11, 6, 5, 11, 22;
check_real_dare<MatrixType>(A, B, Q, MatrixXd::Identity(4, 4), 2.5e-13);
}
{ // DAREX 9
MatrixXd A(5, 5), B(5, 2);
A << 95.4070, 1.9643, 0.3597, 0.0673, 0.0190, 40.8490, 41.3170, 16.0840, 4.4679, 1.1971, 12.2170, 26.3260, 36.1490,
15.9300, 12.3830, 4.1118, 12.8580, 27.2090, 21.4420, 40.9760, 0.1305, 0.5808, 1.8750, 3.6162, 94.2800;
A *= 0.01;
B << 0.0434, -0.0122, 2.6606, -1.0453, 3.7530, -5.5100, 3.6076, -6.6000, 0.4617, -0.9148;
B *= 0.01;
// SciPy's 1.5e-13 matches LAPACK's rounding; without vectorization the residual is 1.53e-13.
check_real_dare<MatrixType>(A, B, MatrixXd::Identity(5, 5), MatrixXd::Identity(2, 2), 3e-13);
}
{ // DAREX 10
MatrixXd A = MatrixXd::Zero(6, 6), B = MatrixXd::Zero(6, 2), Q(6, 6), R(2, 2);
A(0, 1) = A(1, 2) = A(3, 4) = A(4, 5) = 1;
B(2, 0) = B(5, 1) = 1;
Q << 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -1, 0, 0, 0, 0, -1, 1, 0, 0, 0, 0, 0, 0, 0;
R << 3, 0, 0, 1;
check_real_dare<MatrixType>(A, B, Q, R, 1.5e-14);
}
{ // DAREX 11
MatrixXd A(9, 9), B(9, 3);
A << 870.1, 135.0, 11.59, .5014, -37.22, .3484, 0, 4.242, 7.249, //
76.55, 897.4, 12.72, 0.5504, -40.16, .3743, 0, 4.53, 7.499, //
-127.2, 357.5, 817, 1.455, -102.8, .987, 0, 11.85, 18.72, //
-363.5, 633.9, 74.91, 796.6, -273.5, 2.653, 0, 31.72, 48.82, //
-960, 1645.9, -128.9, -5.597, 71.42, 7.108, 0, 84.52, 125.9, //
-664.4, 112.96, -88.89, -3.854, 84.47, 13.6, 0, 144.3, 101.6, //
-410.2, 693, -54.71, -2.371, 66.49, 12.49, .1063, 99.97, 69.67, //
-179.9, 301.7, -23.93, -1.035, 60.59, 22.16, 0, 213.9, 35.54, //
-345.1, 580.4, -45.96, -1.989, 105.6, 19.86, 0, 219.1, 215.2;
A *= 0.001;
B << 4.7600, -0.5701, -83.6800, 0.8790, -4.7730, -2.7300, 1.4820, -13.1200, 8.8760, 3.8920, -35.1300, 24.8000,
10.3400, -92.7500, 66.8000, 7.2030, -61.5900, 38.3400, 4.4540, -36.8300, 20.2900, 1.9710, -15.5400, 6.9370,
3.7730, -30.2800, 14.6900;
B *= 0.001;
VectorXd q = VectorXd::Zero(9);
q(0) = q(4) = 50;
check_real_dare<MatrixType>(A, B, MatrixXd(q.asDiagonal()), MatrixXd::Identity(3, 3), 1.5e-12);
}
{ // DAREX 12: numerically the least accurate example
MatrixXd A(2, 2), B(2, 1);
A << 0, 1e6, 0, 0;
B << 0, 1;
check_real_dare<MatrixType>(A, B, MatrixXd::Identity(2, 2), MatrixXd::Identity(1, 1), 2.0e-2);
}
{ // DAREX 13
MatrixXd A(3, 3);
A << 16, 10, -2, 10, 13, -8, -2, -8, 7;
A /= 9;
check_real_dare<MatrixType>(A, MatrixXd::Identity(3, 3), 1e6 * MatrixXd::Identity(3, 3),
1e6 * MatrixXd::Identity(3, 3), 2.0e-4);
}
{ // DAREX 14
MatrixXd A = MatrixXd::Zero(4, 4), B = MatrixXd::Zero(4, 1), Q = MatrixXd::Zero(4, 4), R(1, 1);
A(0, 0) = 1 - 1 / 1e8;
A(1, 0) = A(2, 1) = A(3, 2) = 1;
B(0, 0) = 1e-8;
Q(3, 3) = 1;
R << 0.25;
// In complex arithmetic the tolerance is raised: where ComplexQZ converges on the balanced pencil (GCC with SSE2),
// the residual is 7.6e-5, against 2.4e-8 with LAPACK's ZGGES. With AVX2 and FMA, or with Clang, ComplexQZ stalls
// on it, and the solver falls back to the unbalanced pencil, whose residual is close to this tolerance for any QZ
// (0.11 here, 5.0e-2 with ZGGES). To be tightened once ComplexQZ converges on this pencil (!3430).
check_real_dare<MatrixType>(A, B, Q, R, NumTraits<typename MatrixType::Scalar>::IsComplex ? 5e-2 : 1.5e-6);
}
{ // DAREX 15
const Index n = 100;
MatrixXd A = MatrixXd::Zero(n, n), B = MatrixXd::Zero(n, 1);
A.diagonal(1).setOnes();
B(n - 1, 0) = 1;
check_real_dare<MatrixType>(A, B, MatrixXd::Identity(n, n), MatrixXd::Identity(1, 1), 1.5e-10);
}
}
// The generalized test cases of scipy.linalg.solve_discrete_are.
template <typename MatrixType>
void dare_generalized_scipy_cases() {
using Scalar = typename MatrixType::Scalar;
MatrixXd A(3, 3), B(3, 2), E(3, 3);
A << 2.769230e-01, 8.234578e-01, 9.502220e-01, 4.617139e-02, 6.948286e-01, 3.444608e-02, 9.713178e-02, 3.170995e-01,
4.387444e-01;
B << 3.815585e-01, 1.868726e-01, 7.655168e-01, 4.897644e-01, 7.951999e-01, 4.455862e-01;
E << 6.463130e-01, 2.760251e-01, 1.626117e-01, 7.093648e-01, 6.797027e-01, 1.189977e-01, 7.546867e-01, 6.550980e-01,
4.983641e-01;
const MatrixXd Q = MatrixXd::Identity(3, 3), R = MatrixXd::Identity(2, 2);
// SciPy's 1.5e-11 matches LAPACK's rounding; depending on vectorization and scalar type, the residuals reach
// 1.8e-11.
const double atol = 3e-11;
check_generalized_dare<MatrixType>(A.cast<Scalar>(), B.cast<Scalar>(), Q.cast<Scalar>(), R.cast<Scalar>(),
E.cast<Scalar>(), MatrixType::Zero(3, 2), atol);
check_generalized_dare<MatrixType>(A.cast<Scalar>(), B.cast<Scalar>(), Q.cast<Scalar>(), R.cast<Scalar>(),
E.cast<Scalar>(), MatrixType::Ones(3, 2), atol);
if (NumTraits<Scalar>::IsComplex) return;
// Reported under SciPy PR 6616: E omitted, S given.
MatrixXd A2(8, 8), B2(8, 2), Q2(8, 8), R2(2, 2), S2(8, 2);
A2 << 0.9790596124208226, 0.15428665211630566, -0.0399419325090604, 0.003911636788878224, -0.011803107183894452,
-0.02494092597437882, -0.008920788197667088, 0.014335952211974737, //
-0.14834721727344974, 0.9873863186572593, 0.006106682107487801, -0.0012366115496408269, 0.013575311081540167,
0.020672662811912077, -0.0017998184559520581, 0.0017523155301884849, //
0.07162947799991767, -0.010401328690850057, 0.9845726088881807, 0.04377286183324468, 0.03170819532733946,
0.049510008354081356, -0.01203976564345253, 0.01388147319653372, //
-0.0019726741801273217, -0.024879350443434263, -0.12719519221321976, 0.9804305159866238, 0.021632354678924177,
0.07905312980830177, -0.011451886896987448, 0.026787582574920674, //
-0.051702322194735015, 0.00969666725317041, 0.02104971125084225, 0.10007182592961279, 0.7141026413334377,
-0.6004593068202806, 0.004777582730886375, 0.03141438420954447, //
0.040354013728618784, -0.015350748134960568, -0.05579631944984332, -0.10543722475599439, 0.6343856366274718,
0.35004282911065215, -0.03776729806891735, 0.0074099601797860785, //
-0.03584359294207154, 0.0025900981016803853, -0.005668147016789041, -0.06465860121122835, -0.012656894372629887,
0.04360315609168892, 0.7043110256864276, 0.6066975504180661, //
-0.02851398087932866, -0.0015631274113090954, -0.012577246358475958, -0.053341773444025395, -0.03769226823991803,
-0.007888383180591867, -0.6280157636492598, 0.34907000148948886;
B2 << -1.0436871916355555, 1.0931665872601304, 0.5133620925013083, -0.1514390795683095, 0.7861828230513443,
0.3888125024790475, 0.9089599716479574, 0.7320641966547645, -0.6781510875985723, 0.12416864282640695,
0.9048515879878576, -0.009000096506101762, -0.008637666934928385, 0.5564531625467788, 0.007083540587022204,
0.6553914999236834;
Q2 << 1.7551250678842602e-06, -4.272252214687373e-07, -9.347904861120448e-07, -6.106412578819786e-07,
1.6283957160863591e-06, 2.226201360118831e-06, 1.0890695975480443e-06, -1.3619304943943357e-06, //
-4.2722522146873673e-07, 5.11729442524146e-07, 1.0331612636389516e-06, 9.016609232641012e-07,
-9.306700600646157e-07, -1.4657838525589338e-06, 3.4201260458425485e-07, -4.2057289495481993e-07, //
-9.347904861120447e-07, 1.0331612636389514e-06, 2.0896827294140204e-06, 1.8133390743261377e-06,
-1.9225671817044196e-06, -3.0098180593831633e-06, 6.189175278904355e-07, -7.587103608495501e-07, //
-6.106412578819799e-07, 9.016609232641022e-07, 1.8133390743261417e-06, 1.6055691910525155e-06,
-1.5469792580036601e-06, -2.460263283152522e-06, 7.394613770930514e-07, -9.082211730936819e-07, //
1.6283957160863587e-06, -9.30670060064614e-07, -1.9225671817044158e-06, -1.5469792580036527e-06,
2.2352617341558302e-06, 3.3544676997154134e-06, 2.14575519310723e-07, -2.9072025953141554e-07, //
2.226201360118829e-06, -1.465783852558932e-06, -3.0098180593831616e-06, -2.460263283152514e-06,
3.3544676997154143e-06, 5.1697298143974225e-06, 4.095247131836552e-09, -6.178901942620202e-08, //
1.089069597548046e-06, 3.4201260458425506e-07, 6.189175278904347e-07, 7.394613770930501e-07,
2.145755193107224e-07, 4.095247131839212e-09, 1.58987128162285e-06, -2.001906554441249e-06, //
-1.361930494394337e-06, -4.2057289495481967e-07, -7.587103608495486e-07, -9.0822117309368e-07,
-2.9072025953141474e-07, -6.178901942620546e-08, -2.001906554441248e-06, 2.587024318215873e-06;
R2 << 6.86199839251629e-07, -1.4227980596335797e-08, -1.4227980596333721e-08, 1.811308664394915e-07;
S2 << -6.272561526745687e-07, 2.805546571188463e-07, 4.2782756682953457e-07, 8.54401685835999e-08,
8.762219354284593e-07, 1.5285528673410845e-07, 7.043760079665012e-07, 1.8058957219255395e-07,
-1.0243476139042563e-06, 6.619287295779458e-08, -1.6830289137993863e-06, 2.9471233851484255e-08,
2.409523332122225e-08, 4.306143497422768e-07, 1.2366259786610292e-08, -6.06921957130162e-07;
check_generalized_dare<MatrixType>(A2.cast<Scalar>(), B2.cast<Scalar>(), Q2.cast<Scalar>(), R2.cast<Scalar>(),
MatrixType::Identity(8, 8), S2.cast<Scalar>(), 3.5e-16);
}
// All modes of A are unstable and B = 0, so no stabilizing solution exists. In a rotated basis, rounding errors leave
// U1 of the order of eps instead of zero, which the scale-invariant rcond(U1) does not detect: the solver returned
// Success with ||X|| = 2.5e20.
template <typename MatrixType>
void dare_unreachable_unstable_modes() {
using Scalar = typename MatrixType::Scalar;
const Vector3d v(1, 2, 3);
const Matrix3d P = Matrix3d::Identity() - 2 * v * v.transpose() / v.squaredNorm();
const MatrixType A = (P * Vector3d(150, -200, 250).asDiagonal() * P).template cast<Scalar>();
DiscreteAlgebraicRiccatiSolver<MatrixType> dare;
for (bool balance : {true, false})
VERIFY_IS_EQUAL(
dare.compute(A, MatrixType::Zero(3, 1), MatrixType::Identity(3, 3), MatrixType::Identity(1, 1), balance).info(),
NumericalIssue);
}
#if defined(EIGEN_TEST_PART_1) || defined(EIGEN_TEST_PART_ALL)
// Regression test: with E != I, the stable subspace of the balanced pencil must be combined with the balanced E.
// Combining it with the original E, as SciPy does, returns a solution with residual 3.9 for this problem.
void dare_generalized_balanced() {
MatrixXd A(2, 2), B(2, 1), Q(2, 2), R(1, 1), E(2, 2), S(2, 1);
A << 0.9, 0, 0, 2;
B << 1e-4, 1;
Q << 1, 0, 0, 1e4;
R << 1;
E << 1, 0.5, 0, 1;
S << 0, 0;
DiscreteAlgebraicRiccatiSolver<MatrixXd> dare(A, B, Q, R, E, S);
VERIFY_IS_EQUAL(dare.info(), Success);
verify_dare_backward_error<MatrixXd>(A, B, Q, R, E, S, dare.solution());
verify_stabilizing<MatrixXd>(A, B, R, E, S, dare.solution());
}
#endif
#if defined(EIGEN_TEST_PART_4) || defined(EIGEN_TEST_PART_ALL)
// Regression test: ComplexQZ reports NumericalIssue for the deflated 2x2 pencil of this problem, whose subdiagonal
// entry its direct 2x2 reduction leaves at 2.1 eps ||S||; the decomposition is still backward stable.
void dare_complex_qz_2x2_block() {
using cf = std::complex<float>;
MatrixXcf A(1, 1), B(1, 1), Q(1, 1), R(1, 1), E(1, 1), S(1, 1);
A << cf(1.34278035f, -1.30643785f);
B << cf(-0.928204715f, 1.51564276f);
Q << cf(2.26285052f, 0);
R << cf(1.4296701f, 0);
E << cf(0.936707854f, 0.0122698545f);
S << cf(-0.0921620131f, -0.450842977f);
DiscreteAlgebraicRiccatiSolver<MatrixXcf> dare(A, B, Q, R, E, S);
VERIFY_IS_EQUAL(dare.info(), Success);
verify_dare_backward_error<MatrixXcf>(A, B, Q, R, E, S, dare.solution());
verify_stabilizing<MatrixXcf>(A, B, R, E, S, dare.solution());
}
#endif
#if defined(EIGEN_TEST_PART_1) || defined(EIGEN_TEST_PART_ALL)
void dare_invalid_input() {
const MatrixXd Id = MatrixXd::Identity(3, 3);
DiscreteAlgebraicRiccatiSolver<MatrixXd> dare;
// Q or R not symmetric.
MatrixXd nonsymmetric(3, 3);
nonsymmetric << 0, 1, 2, 3, 4, 5, 6, 7, 8;
VERIFY_IS_EQUAL(dare.compute(Id, Id, nonsymmetric, Id).info(), InvalidInput);
VERIFY_IS_EQUAL(dare.compute(Id, Id, Id, nonsymmetric).info(), InvalidInput);
// Singular E.
MatrixXd singular = MatrixXd::Constant(3, 3, 1e12);
singular(2, 2) -= 1;
VERIFY_IS_EQUAL(dare.compute(Id, Id, Id, Id, singular, MatrixXd::Zero(3, 3)).info(), InvalidInput);
// Non-finite input.
const MatrixXd nan = MatrixXd::Constant(3, 3, std::numeric_limits<double>::quiet_NaN());
VERIFY_IS_EQUAL(dare.compute(nan, Id, Id, Id).info(), InvalidInput);
VERIFY_IS_EQUAL(dare.compute(Id, nan, Id, Id).info(), InvalidInput);
VERIFY_IS_EQUAL(dare.compute(Id, Id, nan, Id).info(), InvalidInput);
VERIFY_IS_EQUAL(dare.compute(Id, Id, Id, nan).info(), InvalidInput);
VERIFY_IS_EQUAL(dare.compute(Id, Id, Id, Id, nan, Id).info(), InvalidInput);
VERIFY_IS_EQUAL(dare.compute(Id, Id, Id, Id, Id, nan).info(), InvalidInput);
// Incompatible sizes.
const MatrixXd rect = MatrixXd::Ones(3, 2);
VERIFY_RAISES_ASSERT(dare.compute(rect, Id, Id, Id));
VERIFY_RAISES_ASSERT(dare.compute(Id, Id, rect, Id));
VERIFY_RAISES_ASSERT(dare.compute(Id, Id, Id, rect));
VERIFY_RAISES_ASSERT(dare.compute(Id, Id, Id, Id, rect, Id));
VERIFY_RAISES_ASSERT(dare.compute(Id, Id, Id, Id, Id, MatrixXd::Ones(3, 2)));
// The solver recovers from a failed computation.
VERIFY_IS_EQUAL(dare.compute(Id * 0.5, Id, Id, Id).info(), Success);
}
void dare_no_stabilizing_solution() {
// The unstable mode 2 is not controllable, so no stabilizing solution exists.
MatrixXd A(2, 2), B(2, 1);
A << 2, 0, 0, 0.5;
B << 0, 1;
DiscreteAlgebraicRiccatiSolver<MatrixXd> dare;
for (bool balance : {true, false})
VERIFY_IS_EQUAL(dare.compute(A, B, MatrixXd::Identity(2, 2), MatrixXd::Identity(1, 1), balance).info(),
NumericalIssue);
// A Jordan block at 1 with B = 0 and Q = 0 gives a singular pencil, whose reordering rejects a swap.
A << 1, 1, 0, 1;
B.setZero();
for (bool balance : {true, false})
VERIFY_IS_EQUAL(dare.compute(A, B, MatrixXd::Zero(2, 2), MatrixXd::Identity(1, 1), balance).info(), NumericalIssue);
}
void dare_empty() {
// m = 0: the solution is empty.
DiscreteAlgebraicRiccatiSolver<MatrixXd> dare(MatrixXd(0, 0), MatrixXd(0, 2), MatrixXd(0, 0),
MatrixXd::Identity(2, 2));
VERIFY_IS_EQUAL(dare.info(), Success);
VERIFY_IS_EQUAL(dare.solution().rows(), 0);
dare.compute(MatrixXd(0, 0), MatrixXd(0, 2), MatrixXd(0, 0), MatrixXd::Identity(2, 2), MatrixXd(0, 0),
MatrixXd(0, 2));
VERIFY_IS_EQUAL(dare.info(), Success);
// n = 0: for stable A the equation reduces to the Stein equation A^* X A - X + Q = 0.
const Index m = 4;
MatrixXd A = MatrixXd::Random(m, m);
A *= 0.5 / A.operatorNorm();
const MatrixXd Q = MatrixXd::Identity(m, m);
dare.compute(A, MatrixXd(m, 0), Q, MatrixXd(0, 0));
VERIFY_IS_EQUAL(dare.info(), Success);
const MatrixXd& X = dare.solution();
VERIFY((A.adjoint() * X * A - X + Q).norm() <= 100 * NumTraits<double>::epsilon() * X.norm());
}
#endif
// Random problems with a unique stabilizing solution whose conditioning stays moderate: A = U T U^* with U unitary and
// T upper triangular with eigenvalue moduli in [0.1, 0.8], except for at most n well controllable ones in [1.25, 2]; Q
// and R are positive definite, and ||S||_F <= 1/2 keeps Q - S R^-1 S^* >= 3/4 I in the generalized problems. The sizes
// are m in [1, maxM] (m = maxM for fixed-size MatrixType) and n in [1, min(m, maxN)].
template <typename MatrixType>
void dare_random(Index maxM, Index maxN) {
using Scalar = typename MatrixType::Scalar;
using RealScalar = typename MatrixType::RealScalar;
using DynMatrix = Matrix<Scalar, Dynamic, Dynamic>;
const Index m = MatrixType::RowsAtCompileTime == Dynamic ? internal::random<Index>(1, maxM) : maxM;
const Index n = internal::random<Index>(1, numext::mini(m, maxN));
DynMatrix T = DynMatrix::Random(m, m).template triangularView<StrictlyUpper>();
T *= RealScalar(0.5) / numext::sqrt(RealScalar(m));
const Index unstable = internal::random<Index>(0, n);
for (Index i = 0; i < m; ++i) {
Scalar phase = internal::random<Scalar>();
while (numext::is_exactly_zero(phase)) phase = internal::random<Scalar>();
const RealScalar modulus =
i < unstable ? internal::random<RealScalar>(1.25, 2) : internal::random<RealScalar>(0.1, 0.8);
T(i, i) = phase / numext::abs(phase) * modulus;
}
const DynMatrix U = HouseholderQR<DynMatrix>(DynMatrix::Random(m, m)).householderQ();
const MatrixType A = U * T * U.adjoint();
// B = U Bu with Bu(i, i) = 2 + O(1) for the unstable modes i < unstable <= n keeps them well controllable. A third
// of the problems scale B by 10^-k, for which balancing alone would cost digits.
DynMatrix Bu = DynMatrix::Random(m, n);
for (Index i = 0; i < unstable; ++i) Bu(i, i) += Scalar(2);
const int k = internal::random<int>(0, 2) == 0 ? internal::random<int>(1, NumTraits<RealScalar>::digits10() / 3) : 0;
const DynMatrix B = U * Bu * numext::pow(RealScalar(10), RealScalar(-k));
const DynMatrix C = DynMatrix::Random(m, m);
const DynMatrix D = DynMatrix::Random(n, n);
const MatrixType Q = C.adjoint() * C + DynMatrix::Identity(m, m);
const DynMatrix R = D.adjoint() * D + DynMatrix::Identity(n, n);
const DynMatrix Id = DynMatrix::Identity(m, m), Z = DynMatrix::Zero(m, n);
DiscreteAlgebraicRiccatiSolver<MatrixType> dare(A, B, Q, R);
VERIFY_IS_EQUAL(dare.info(), Success);
const MatrixType& X = dare.solution();
VERIFY_IS_EQUAL(X, X.adjoint());
verify_dare_backward_error<DynMatrix>(A, B, Q, R, Id, Z, X);
verify_stabilizing<DynMatrix>(A, B, R, Id, Z, X);
// X >= Q > 0, since X = Q + A^* (X - X B (R + B^* X B)^-1 B^* X) A.
VERIFY(SelfAdjointEigenSolver<DynMatrix>(X).eigenvalues().minCoeff() > RealScalar(0));
DiscreteAlgebraicRiccatiSolver<MatrixType> unbalanced(A, B, Q, R, /*balance=*/false);
VERIFY_IS_EQUAL(unbalanced.info(), Success);
verify_dare_backward_error<DynMatrix>(A, B, Q, R, Id, Z, unbalanced.solution());
// The generalized equation with E = I and S = 0 is the standard one.
DiscreteAlgebraicRiccatiSolver<MatrixType> generalized;
generalized.compute(A, B, Q, R, MatrixType::Identity(m, m), Z);
VERIFY_IS_EQUAL(generalized.info(), Success);
verify_dare_backward_error<DynMatrix>(A, B, Q, R, Id, Z, generalized.solution());
// The generalized problem (E A, E B, Q, R, E, S) has the closed-loop dynamics of (A, B), and E^* X E is the solution
// of the standard problem with the cross term S.
const MatrixType E =
MatrixType::Identity(m, m) + MatrixType::Random(m, m) * (RealScalar(0.5) / numext::sqrt(RealScalar(m)));
const DynMatrix S = DynMatrix::Random(m, n) * (RealScalar(0.5) / numext::sqrt(RealScalar(m * n)));
const DynMatrix EA = E * A, EB = E * B;
for (bool balance : {true, false}) {
generalized.compute(EA, EB, Q, R, E, S, balance);
VERIFY_IS_EQUAL(generalized.info(), Success);
const DynMatrix Xg = generalized.solution();
VERIFY_IS_EQUAL(Xg, Xg.adjoint());
verify_dare_backward_error<DynMatrix>(EA, EB, Q, R, E, S, Xg);
verify_stabilizing<DynMatrix>(EA, EB, R, E, S, Xg);
}
}
// Checks the reordering of a generalized Schur form directly.
template <typename MatrixType>
void generalized_schur_reordering(Index size) {
using Scalar = typename MatrixType::Scalar;
using RealScalar = typename MatrixType::RealScalar;
const MatrixType A = MatrixType::Random(size, size) * RealScalar(1.5);
const MatrixType B = MatrixType::Random(size, size);
const RealScalar eps = NumTraits<RealScalar>::epsilon();
typename internal::riccati_qz<MatrixType>::type qz(A, B, true);
VERIFY_IS_EQUAL(qz.info(), Success);
MatrixType S = qz.matrixS(), T = qz.matrixT(), Q = qz.matrixQ(), Z = qz.matrixZ().adjoint();
Index inside = 0;
for (Index k = 0; k < size; ++k) {
// Count eigenvalues inside the unit circle through the 1x1 and 2x2 diagonal blocks.
const Index bs = internal::generalized_schur_block_size(S, k);
if (bs == 1) {
if (numext::abs(S(k, k)) < numext::abs(T(k, k))) ++inside;
} else {
if (numext::abs(S.template block<2, 2>(k, k).determinant()) < numext::abs(T(k, k) * T(k + 1, k + 1))) inside += 2;
++k;
}
}
const Index selected = internal::reorder_generalized_schur_inside_unit_circle(S, T, &Q, Z);
VERIFY_IS_EQUAL(selected, inside);
// Each swap is accepted only if it is backward stable to 20 eps ||(S, T)||_F locally, see the strong stability test.
const RealScalar bound = RealScalar(20) * RealScalar(size) * eps;
VERIFY((Q * S * Z.adjoint() - A).norm() <= bound * A.norm());
VERIFY((Q * T * Z.adjoint() - B).norm() <= bound * B.norm());
VERIFY((Q.adjoint() * Q - MatrixType::Identity(size, size)).norm() <= bound);
VERIFY((Z.adjoint() * Z - MatrixType::Identity(size, size)).norm() <= bound);
VERIFY_IS_EQUAL(T.template triangularView<StrictlyLower>().toDenseMatrix(), MatrixType::Zero(size, size));
for (Index i = 0; i + 2 < size; ++i) VERIFY_IS_EQUAL(S(i + 2, i), Scalar(0));
// The selected eigenvalues come first, and the boundary does not split a 2x2 block.
if (selected > 0 && selected < size) VERIFY_IS_EQUAL(S(selected, selected - 1), Scalar(0));
for (Index k = 0; k < size;) {
const Index bs = internal::generalized_schur_block_size(S, k);
RealScalar modulus;
if (bs == 1)
modulus = numext::abs(S(k, k)) / numext::abs(T(k, k));
else
modulus = numext::sqrt(numext::abs(S.template block<2, 2>(k, k).determinant()) /
numext::abs(T(k, k) * T(k + 1, k + 1)));
VERIFY((k < selected) == (modulus < RealScalar(1)));
k += bs;
}
}
// Generalized eigenvalues of the diagonal block of size bs at (k, k) of (S, T), sorted.
template <typename MatrixType>
std::vector<std::complex<typename MatrixType::RealScalar>> block_eigenvalues(const MatrixType& S, const MatrixType& T,
Index k, Index bs) {
using Complex = std::complex<typename MatrixType::RealScalar>;
using ComplexMatrix = Matrix<Complex, Dynamic, Dynamic>;
const ComplexMatrix s = S.block(k, k, bs, bs).template cast<Complex>();
const ComplexMatrix t = T.block(k, k, bs, bs).template cast<Complex>();
const Matrix<Complex, Dynamic, 1> ev = ComplexEigenSolver<ComplexMatrix>(t.inverse() * s).eigenvalues();
std::vector<Complex> v(ev.data(), ev.data() + ev.size());
std::sort(v.begin(), v.end(), [](const Complex& a, const Complex& b) {
return a.imag() < b.imag() || (a.imag() == b.imag() && a.real() < b.real());
});
return v;
}
// Swaps the diagonal blocks of sizes n1 and n2 at row/column j = 1 of a generalized Schur form of size n1 + n2 + 2.
template <typename MatrixType>
void generalized_schur_swap(Index n1, Index n2) {
using Scalar = typename MatrixType::Scalar;
using RealScalar = typename MatrixType::RealScalar;
const Index j = 1, size = n1 + n2 + 2;
MatrixType S = MatrixType::Random(size, size).template triangularView<Upper>();
MatrixType T = MatrixType::Random(size, size).template triangularView<Upper>();
T.diagonal().array() += Scalar(2);
// A 2x2 block (s, t) with t = I and s = [a, 1; -1, a] has the eigenvalues a +- i.
const Index starts[] = {j, j + n1};
const Index sizes[] = {n1, n2};
for (int b = 0; b < 2; ++b) {
if (sizes[b] != 2) continue;
const Index k = starts[b];
const Scalar a = Scalar(b == 0 ? 0.5 : -0.25);
S.template block<2, 2>(k, k) << a, Scalar(1), Scalar(-1), a;
T.template block<2, 2>(k, k).setIdentity();
}
const MatrixType S0 = S, T0 = T;
MatrixType Q = MatrixType::Identity(size, size), Z = MatrixType::Identity(size, size);
const auto upper = block_eigenvalues(S0, T0, j, n1);
const auto lower = block_eigenvalues(S0, T0, j + n1, n2);
VERIFY(internal::swap_generalized_schur_blocks(S, T, &Q, Z, j, n1, n2));
const auto newUpper = block_eigenvalues(S, T, j, n2);
const auto newLower = block_eigenvalues(S, T, j + n2, n1);
const RealScalar eps = NumTraits<RealScalar>::epsilon();
for (Index i = 0; i < n2; ++i)
VERIFY(std::abs(newUpper[i] - lower[i]) <= RealScalar(100) * eps * (RealScalar(1) + std::abs(lower[i])));
for (Index i = 0; i < n1; ++i)
VERIFY(std::abs(newLower[i] - upper[i]) <= RealScalar(100) * eps * (RealScalar(1) + std::abs(upper[i])));
// Exact structure: zero block boundary, T upper triangular, S zero below its 2x2 diagonal blocks.
VERIFY_IS_EQUAL(S.block(j + n2, j, n1, n2), MatrixType::Zero(n1, n2));
VERIFY_IS_EQUAL(T.template triangularView<StrictlyLower>().toDenseMatrix(), MatrixType::Zero(size, size));
for (Index c = 0; c < size; ++c)
for (Index r = c + 1; r < size; ++r)
if (!((r == j + 1 && c == j && n2 == 2) || (r == j + n2 + 1 && c == j + n2 && n1 == 2)))
VERIFY_IS_EQUAL(S(r, c), Scalar(0));
// Backward error and orthogonality.
const RealScalar bound = RealScalar(20) * RealScalar(size) * eps;
VERIFY((Q * S * Z.adjoint() - S0).norm() <= bound * S0.norm());
VERIFY((Q * T * Z.adjoint() - T0).norm() <= bound * T0.norm());
VERIFY((Q.adjoint() * Q - MatrixType::Identity(size, size)).norm() <= bound);
VERIFY((Z.adjoint() * Z - MatrixType::Identity(size, size)).norm() <= bound);
}
#if defined(EIGEN_TEST_PART_6) || defined(EIGEN_TEST_PART_ALL)
// A rejected swap leaves all matrices untouched, and the reordering then reports failure.
void generalized_schur_rejected_swap() {
// Equal 2x2 blocks make the generalized Sylvester equation singular.
MatrixXd S(4, 4), T = MatrixXd::Identity(4, 4);
S << 0.5, 1, 0.3, 0.2, //
-1, 0.5, 0.1, 0.4, //
0, 0, 0.5, 1, //
0, 0, -1, 0.5;
MatrixXd Q = MatrixXd::Identity(4, 4), Z = MatrixXd::Identity(4, 4);
const MatrixXd S0 = S, T0 = T, Q0 = Q, Z0 = Z;
VERIFY(!internal::swap_generalized_schur_blocks(S, T, &Q, Z, 0, 2, 2));
VERIFY_IS_EQUAL(S, S0);
VERIFY_IS_EQUAL(T, T0);
VERIFY_IS_EQUAL(Q, Q0);
VERIFY_IS_EQUAL(Z, Z0);
// The 1x1 blocks 0/0 and 1/2 of a singular pencil cannot be swapped, so the eigenvalue 1/2 cannot be selected.
MatrixXd S2(2, 2), T2(2, 2);
S2 << 0, 0, 0, 0.5;
T2 << 0, 0, 0, 1;
MatrixXd Q2 = MatrixXd::Identity(2, 2), Z2 = MatrixXd::Identity(2, 2);
const MatrixXd S20 = S2, T20 = T2;
VERIFY_IS_EQUAL(internal::reorder_generalized_schur_inside_unit_circle(S2, T2, &Q2, Z2), Index(-1));
VERIFY_IS_EQUAL(S2, S20);
VERIFY_IS_EQUAL(T2, T20);
}
#endif
EIGEN_DECLARE_TEST(riccati) {
CALL_SUBTEST_1(dare_scipy_real_cases<MatrixXd>());
CALL_SUBTEST_1(dare_generalized_scipy_cases<MatrixXd>());
CALL_SUBTEST_1(dare_generalized_balanced());
CALL_SUBTEST_1(dare_invalid_input());
CALL_SUBTEST_1(dare_no_stabilizing_solution());
CALL_SUBTEST_1(dare_unreachable_unstable_modes<MatrixXd>());
CALL_SUBTEST_7(dare_unreachable_unstable_modes<MatrixXcd>());
CALL_SUBTEST_1(dare_empty());
CALL_SUBTEST_6(generalized_schur_rejected_swap());
for (Index n1 = 1; n1 <= 2; ++n1)
for (Index n2 = 1; n2 <= 2; ++n2) CALL_SUBTEST_6(generalized_schur_swap<MatrixXd>(n1, n2));
CALL_SUBTEST_6(generalized_schur_swap<MatrixXcd>(1, 1));
CALL_SUBTEST_4(dare_complex_qz_2x2_block());
CALL_SUBTEST_7(dare_scipy_complex_cases());
CALL_SUBTEST_7(dare_scipy_real_cases<MatrixXcd>());
CALL_SUBTEST_7(dare_generalized_scipy_cases<MatrixXcd>());
for (int i = 0; i < g_repeat; i++) {
CALL_SUBTEST_2(dare_random<MatrixXd>(20, 6));
CALL_SUBTEST_3(dare_random<MatrixXcd>(20, 6));
CALL_SUBTEST_4(dare_random<MatrixXf>(8, 4));
CALL_SUBTEST_4(dare_random<MatrixXcf>(8, 4));
CALL_SUBTEST_5((dare_random<Matrix<double, 4, 4>>(4, 4)));
CALL_SUBTEST_6(generalized_schur_reordering<MatrixXd>(internal::random<Index>(1, 30)));
CALL_SUBTEST_6(generalized_schur_reordering<MatrixXcd>(internal::random<Index>(1, 30)));
CALL_SUBTEST_6(generalized_schur_reordering<MatrixXf>(internal::random<Index>(1, 30)));
}
}