blob: 3f604c76996faba253559eda26bcb49771c4bed6 [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
#define EIGEN_TEST_ANNOYING_SCALAR_DONT_THROW
#include "sparse_solver.h"
#include "AnnoyingScalar.h"
#include <Eigen/Eigenvalues>
#include <Eigen/SparseCholesky>
namespace {
// The residual checks allow n epsilon / u for the pivot threshold u, which is 0.01 unless a test sets it: a heuristic
// calibrated on these tests. The pivot tests bound |L| by 1/u but not the growth in D, so this isn't a bound on the
// normwise backward error.
struct Inertia {
Index positive = 0;
Index negative = 0;
Index zero = 0;
void add(int sign) {
Index& count = sign > 0 ? positive : (sign < 0 ? negative : zero);
++count;
}
bool operator==(const Inertia& other) const {
return positive == other.positive && negative == other.negative && zero == other.zero;
}
};
// Counts the eigenvalues of each sign from a factorization's D.
template <typename Solver>
Inertia inertiaOfD(const Solver& solver) {
using RealScalar = typename Solver::RealScalar;
const auto sign = [](const RealScalar& value) {
return value > RealScalar(0) ? 1 : (value < RealScalar(0) ? -1 : 0);
};
Inertia inertia;
const auto& D = solver.vectorD();
const auto& sub = solver.subDiagonal();
for (Index k = 0; k < D.size(); ++k) {
if (sub[k] != typename Solver::Scalar(0)) {
// [a b*; b c] has eigenvalues mid +- r. Take the larger magnitude one directly and the other from det / large, so
// the rounding of a difference can't flip its sign.
const RealScalar a = D[k];
const RealScalar c = D[k + 1];
const RealScalar b = numext::abs(sub[k]);
const RealScalar mid = (a + c) / RealScalar(2);
const RealScalar r = numext::hypot(RealScalar((a - c) / RealScalar(2)), b);
const RealScalar large = mid >= RealScalar(0) ? RealScalar(mid + r) : RealScalar(mid - r);
inertia.add(sign(large));
inertia.add(sign((a / large) * c - (b / large) * b));
++k;
} else {
inertia.add(sign(D[k]));
}
}
return inertia;
}
// Normwise backward error: ||A x - b||_inf / (||A||_inf ||x||_inf + ||b||_inf)
template <typename Mat, typename Rhs, typename Sol>
typename NumTraits<typename Mat::Scalar>::Real relativeResidual(const Mat& A, const Sol& x, const Rhs& b) {
using RealScalar = typename NumTraits<typename Mat::Scalar>::Real;
const Matrix<typename Mat::Scalar, Dynamic, Dynamic> dA = A;
const RealScalar normA = dA.cwiseAbs().rowwise().sum().maxCoeff();
const RealScalar scale = normA * x.template lpNorm<Infinity>() + b.template lpNorm<Infinity>();
return (dA * x - b).template lpNorm<Infinity>() / scale;
}
template <typename Mat>
Mat fromTriplets(Index size, const std::vector<Triplet<typename Mat::Scalar, typename Mat::StorageIndex>>& triplets) {
Mat matrix(size, size);
matrix.setFromTriplets(triplets.begin(), triplets.end());
matrix.makeCompressed();
return matrix;
}
template <typename Mat>
using Triplets = std::vector<Triplet<typename Mat::Scalar, typename Mat::StorageIndex>>;
template <typename Scalar>
Scalar randomUnit() {
Scalar value;
do {
value = internal::random<Scalar>();
} while (numext::abs(value) < typename NumTraits<Scalar>::Real(0.1));
return value / numext::abs(value);
}
// Random symmetric or Hermitian matrix with about offDiagonals entries in each triangle and a zero diagonal in every
// second row if zeroDiagonals, which forces 2x2 pivots. Its diagonal is otherwise nonzero with random signs. The
// magnitudes vary so that entries can't cancel into an exactly singular matrix.
template <typename Mat>
Mat randomHermitian(Index size, Index offDiagonals, bool zeroDiagonals) {
using Scalar = typename Mat::Scalar;
using RealScalar = typename NumTraits<Scalar>::Real;
const auto randomNonZero = []() {
return Scalar(randomUnit<Scalar>() * internal::random<RealScalar>(RealScalar(0.5), RealScalar(1)));
};
Triplets<Mat> triplets;
for (Index i = 0; i < size; ++i) {
if (!zeroDiagonals || i % 2 == 0) triplets.emplace_back(i, i, Scalar(numext::real(randomNonZero())));
}
if (zeroDiagonals) {
for (Index i = 0; i + 1 < size; ++i) {
const Scalar value = randomNonZero();
triplets.emplace_back(i + 1, i, value);
triplets.emplace_back(i, i + 1, numext::conj(value));
}
}
for (Index k = 0; k < offDiagonals; ++k) {
const Index i = internal::random<Index>(0, size - 1);
const Index j = internal::random<Index>(0, size - 1);
if (i != j) {
const Scalar value = internal::random<Scalar>();
triplets.emplace_back(i, j, value);
triplets.emplace_back(j, i, numext::conj(value));
}
}
return fromTriplets<Mat>(size, triplets);
}
// The KKT matrix [H A^*; A 0] with H = diagonal * I and A = phase * B, where B is the (n-1) x n bidiagonal with rows
// e_r^T - e_{r+1}^T, the structure of a chain of dynamics constraints.
template <typename Mat>
Mat kktChain(Index numVariables, const typename Mat::Scalar& diagonal,
const typename Mat::Scalar& phase = typename Mat::Scalar(1)) {
using Scalar = typename Mat::Scalar;
Triplets<Mat> triplets;
for (Index i = 0; i < numVariables; ++i) {
if (diagonal != Scalar(0)) triplets.emplace_back(i, i, diagonal);
}
for (Index r = 0; r + 1 < numVariables; ++r) {
const Index row = numVariables + r;
triplets.emplace_back(row, r, phase);
triplets.emplace_back(r, row, numext::conj(phase));
triplets.emplace_back(row, r + 1, Scalar(-phase));
triplets.emplace_back(r + 1, row, Scalar(-numext::conj(phase)));
}
return fromTriplets<Mat>(2 * numVariables - 1, triplets);
}
// L is unit lower triangular, so each leading principal minor of P A P^T that ends on a block boundary of D is the
// product of the determinants of D's blocks up to there. A wrong row in P changes the minors.
template <typename Solver>
void checkPermutation(const Solver& solver, const typename Solver::MatrixType& full) {
using Scalar = typename Solver::Scalar;
using RealScalar = typename Solver::RealScalar;
using DenseMatrix = Matrix<Scalar, Dynamic, Dynamic>;
const Index size = full.rows();
const auto& P = solver.permutationP();
const auto& Pinv = solver.permutationPinv();
VERIFY_IS_EQUAL(P.size(), size);
VERIFY_IS_EQUAL(Pinv.size(), size);
for (Index k = 0; k < size; ++k) VERIFY_IS_EQUAL(Index(P.indices()[Pinv.indices()[k]]), k);
const DenseMatrix permuted = P * DenseMatrix(full) * Pinv;
const auto& D = solver.vectorD();
const auto& sub = solver.subDiagonal();
// |L| |D| |L^*|, whose leading blocks scale the backward error of the factorization of each leading block.
const DenseMatrix absL = DenseMatrix(solver.matrixL()).cwiseAbs();
DenseMatrix absD = DenseMatrix::Zero(size, size);
for (Index k = 0; k < size; ++k) {
absD(k, k) = numext::abs(D[k]);
if (sub[k] != Scalar(0)) absD(k + 1, k) = absD(k, k + 1) = numext::abs(sub[k]);
}
const DenseMatrix growth = absL * absD * absL.adjoint();
RealScalar product(1);
for (Index k = 0; k < size; ++k) {
if (sub[k] != Scalar(0)) {
product *= D[k] * D[k + 1] - numext::abs2(sub[k]);
++k;
} else {
product *= D[k];
}
// The pivots factor M + E exactly, where M is the leading block and ||E||_F <= c (k + 1) epsilon ||G||_F for the
// leading block G of |L| |D| |L^*| (Higham, Theorem 11.3). To first order, det(M + E) = det(M) (1 + tr(M^-1 E)).
const DenseMatrix minor = permuted.topLeftCorner(k + 1, k + 1);
const Scalar leadingMinor = minor.determinant();
const RealScalar tolerance = RealScalar(8 * (k + 1)) * NumTraits<RealScalar>::epsilon() *
growth.topLeftCorner(k + 1, k + 1).norm() * minor.inverse().norm();
VERIFY(numext::abs(leadingMinor - Scalar(product)) <= tolerance * numext::abs(product));
}
}
// The block diagonal D of a factorization, as a dense matrix.
template <typename Solver>
Matrix<typename Solver::Scalar, Dynamic, Dynamic> denseD(const Solver& solver) {
using Scalar = typename Solver::Scalar;
const Index size = solver.rows();
Matrix<Scalar, Dynamic, Dynamic> D = Matrix<Scalar, Dynamic, Dynamic>::Zero(size, size);
D.diagonal() = solver.vectorD().template cast<Scalar>();
for (Index k = 0; k + 1 < size; ++k) {
D(k + 1, k) = solver.subDiagonal()[k];
D(k, k + 1) = numext::conj(solver.subDiagonal()[k]);
}
return D;
}
template <typename Solver>
void checkIndefiniteFactorization(Solver& solver, const typename Solver::MatrixType& input,
const typename Solver::MatrixType& full) {
using Scalar = typename Solver::Scalar;
using RealScalar = typename Solver::RealScalar;
using DenseMatrix = Matrix<Scalar, Dynamic, Dynamic>;
using DenseVector = Matrix<Scalar, Dynamic, 1>;
solver.compute(input);
VERIFY_IS_EQUAL(solver.info(), Success);
const Index size = full.rows();
const RealScalar tolerance = RealScalar(100 * size) * NumTraits<RealScalar>::epsilon();
// ||P A P^T - L D L^*||_F <= 64 n epsilon (||A||_F + || |L| |D| |L^*| ||_F), the normwise form of the backward error
// bound of block LDL^T (N. J. Higham, "Accuracy and Stability of Numerical Algorithms", 2nd ed., SIAM, 2002,
// Theorem 11.3).
const typename Solver::FactorType sparseL = solver.matrixL();
VERIFY_IS_EQUAL(sparseL.nonZeros(), solver.nonZeros() + size);
const DenseMatrix L = sparseL;
VERIFY((L.array() == DenseMatrix(L.template triangularView<UnitLower>()).array()).all());
VERIFY(DenseMatrix(solver.matrixU()) == L.adjoint());
const DenseMatrix D = denseD(solver);
for (Index k = 0; k + 1 < size; ++k) {
if (solver.subDiagonal()[k] != Scalar(0)) VERIFY_IS_EQUAL(L(k + 1, k), Scalar(0));
}
const DenseMatrix permuted = solver.permutationP() * DenseMatrix(full) * solver.permutationPinv();
const RealScalar scale = (L.cwiseAbs() * D.cwiseAbs() * L.adjoint().cwiseAbs()).norm();
const RealScalar factorTolerance = RealScalar(64 * size) * NumTraits<RealScalar>::epsilon();
VERIFY((permuted - L * D * L.adjoint()).norm() <= factorTolerance * (permuted.norm() + scale));
// The bound on |L| the pivot tests enforce, with a margin for the rounding in the tests and in the 2x2 formula.
VERIFY(L.cwiseAbs().maxCoeff() <= (1 + 16 * NumTraits<RealScalar>::epsilon()) / solver.pivotThreshold());
// The solves' backward error has the same form (Higham, Theorem 11.3):
// ||A x - b||_2 <= c n epsilon (||A||_F + || |L| |D| |L^*| ||_F) ||x||_2.
const DenseMatrix dense = full;
const RealScalar solveScale = factorTolerance * (permuted.norm() + scale);
const auto verifySolution = [&](const DenseVector& xCol, const DenseVector& bCol) {
VERIFY((dense * xCol - bCol).norm() <= solveScale * xCol.norm());
};
const DenseVector b = DenseVector::Random(size);
verifySolution(solver.solve(b), b);
const DenseMatrix B = DenseMatrix::Random(size, 3);
const DenseMatrix X = solver.solve(B);
for (Index col = 0; col < B.cols(); ++col) {
const DenseVector bCol = B.col(col);
verifySolution(X.col(col), bCol);
verifySolution(solver.solve(bCol), bCol);
}
// An independent reference, skipped near a sign change that the working precision can't resolve.
const Matrix<RealScalar, Dynamic, 1> eigenvalues =
SelfAdjointEigenSolver<DenseMatrix>(dense, EigenvaluesOnly).eigenvalues();
const RealScalar gap = eigenvalues.cwiseAbs().minCoeff();
if (gap > tolerance * eigenvalues.cwiseAbs().maxCoeff()) {
Inertia reference;
for (Index i = 0; i < size; ++i) reference.add(eigenvalues[i] > RealScalar(0) ? 1 : -1);
VERIFY(inertiaOfD(solver) == reference);
VERIFY_IS_EQUAL(solver.numPositive(), reference.positive);
VERIFY_IS_EQUAL(solver.numNegative(), reference.negative);
VERIFY_IS_EQUAL(solver.numZero(), Index(0));
VERIFY_IS_EQUAL(solver.isPositive(), reference.negative == 0);
VERIFY_IS_EQUAL(solver.isNegative(), reference.positive == 0);
}
}
} // namespace
// The harness shared with the other sparse solvers, on the Hermitian positive definite matrices it generates.
template <typename Scalar, typename StorageIndex, int Options>
void test_positive_definite() {
using Mat = SparseMatrix<Scalar, Options, StorageIndex>;
MultifrontalLDLT<Mat, Lower> lowerAmd;
MultifrontalLDLT<Mat, Upper> upperAmd;
MultifrontalLDLT<Mat, Lower, NaturalOrdering<StorageIndex>> lowerNatural;
MultifrontalLDLT<Mat, Upper, NaturalOrdering<StorageIndex>> upperNatural;
check_sparse_spd_solving(lowerAmd);
check_sparse_spd_solving(upperAmd);
check_sparse_spd_solving(lowerNatural, (std::min)(100, EIGEN_TEST_MAX_SIZE));
check_sparse_spd_solving(upperNatural, (std::min)(100, EIGEN_TEST_MAX_SIZE));
check_sparse_zero_matrix(lowerAmd);
}
// Random indefinite matrices over both triangles and the kinds of ordering, from both a full and a half input.
template <typename Scalar, typename StorageIndex, int Options>
void test_random_indefinite() {
using Mat = SparseMatrix<Scalar, Options, StorageIndex>;
MultifrontalLDLT<Mat, Lower> lowerAmd;
MultifrontalLDLT<Mat, Upper> upperAmd;
MultifrontalLDLT<Mat, Lower, NaturalOrdering<StorageIndex>> lowerNatural;
MultifrontalLDLT<Mat, Upper, COLAMDOrdering<StorageIndex>> upperColamd;
for (int repeat = 0; repeat < g_repeat; ++repeat) {
const Index size = internal::random<Index>(1, (std::min)(100, EIGEN_TEST_MAX_SIZE));
const Index density = internal::random<Index>(0, 3);
const bool zeroDiagonals = internal::random<bool>() && size > 1;
const Mat full = randomHermitian<Mat>(size, size * density, zeroDiagonals);
const Mat lower = full.template triangularView<Lower>();
const Mat upper = full.template triangularView<Upper>();
CALL_SUBTEST(checkIndefiniteFactorization(lowerAmd, full, full));
CALL_SUBTEST(checkIndefiniteFactorization(lowerAmd, lower, full));
CALL_SUBTEST(checkIndefiniteFactorization(upperAmd, full, full));
CALL_SUBTEST(checkIndefiniteFactorization(upperAmd, upper, full));
CALL_SUBTEST(checkIndefiniteFactorization(lowerNatural, full, full));
CALL_SUBTEST(checkIndefiniteFactorization(lowerNatural, lower, full));
CALL_SUBTEST(checkIndefiniteFactorization(upperColamd, upper, full));
}
}
// One dense front with a zero diagonal: the first pivot is 2x2, and the pivots swap columns that aren't neighbors.
template <typename Scalar>
void test_dense_front_with_2x2_pivots() {
using Mat = SparseMatrix<Scalar>;
using RealScalar = typename NumTraits<Scalar>::Real;
using DenseVector = Matrix<Scalar, Dynamic, 1>;
for (int repeat = 0; repeat < g_repeat; ++repeat) {
// Sizes on both sides of a panel's width, and of a dense solve.
const Index size = 2 * internal::random<Index>(2, 8) * internal::random<Index>(1, 8);
Triplets<Mat> triplets;
for (Index col = 0; col < size; ++col) {
for (Index row = col + 1; row < size; ++row) {
const Scalar value = internal::random<Scalar>();
triplets.emplace_back(row, col, value);
triplets.emplace_back(col, row, numext::conj(value));
}
}
const Mat matrix = fromTriplets<Mat>(size, triplets);
MultifrontalLDLT<Mat> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY(solver.subDiagonal()[0] != Scalar(0));
const DenseVector b = DenseVector::Random(size);
const DenseVector x = solver.solve(b);
VERIFY(relativeResidual(matrix, x, b) <= RealScalar(100 * size) * NumTraits<RealScalar>::epsilon());
}
}
// Three dense blocks that share a dense separator, ordered blocks first. The separator's front absorbs one block, which
// adds no explicit zeros, and that leaves two fronts wider than a panel with rows below their pivots and a root that
// assembles both of their contribution blocks. With zero diagonals, the blocks' pivots are 2x2 and can pair columns on
// either side of a panel boundary, or be delayed to the root.
template <typename Scalar, typename StorageIndex, int Options>
void test_large_fronts() {
using Mat = SparseMatrix<Scalar, Options, StorageIndex>;
using Natural = NaturalOrdering<StorageIndex>;
for (int repeat = 0; repeat < g_repeat; ++repeat) {
// The first repeat is wide enough to apply each front's factor through dense products in solve().
const Index blockSize = repeat == 0 ? 80 : internal::random<Index>(1, 80);
const Index separatorSize = repeat == 0 ? 80 : internal::random<Index>(1, 80);
const Index numBlocks = 3;
const Index separator = numBlocks * blockSize;
const Index size = separator + separatorSize;
// Blocks of one zero column are coupled only to the separator, which makes the matrix singular if the separator
// has fewer rows than there are blocks.
const bool zeroDiagonals = internal::random<bool>() && (blockSize > 1 || separatorSize >= numBlocks);
Triplets<Mat> triplets;
for (Index col = 0; col < size; ++col) {
const bool inSeparator = col >= separator;
if (!zeroDiagonals || inSeparator) triplets.emplace_back(col, col, Scalar(numext::real(randomUnit<Scalar>())));
for (Index row = col + 1; row < size; ++row) {
if (row / blockSize == col / blockSize || row >= separator || inSeparator) {
const Scalar value = internal::random<Scalar>();
triplets.emplace_back(row, col, value);
triplets.emplace_back(col, row, numext::conj(value));
}
}
}
const Mat full = fromTriplets<Mat>(size, triplets);
const Mat lower = full.template triangularView<Lower>();
MultifrontalLDLT<Mat, Lower, Natural> natural;
CALL_SUBTEST(checkIndefiniteFactorization(natural, lower, full));
MultifrontalLDLT<Mat> amd;
CALL_SUBTEST(checkIndefiniteFactorization(amd, full, full));
}
}
// The KKT matrix [delta I A^*; A h I] with h / |A| above 1 / threshold. Constraint columns fail both pivot tests until
// enough variables are eliminated, while each variable passes its 1x1 test. The dense blocks keep every column in one
// front, up to several panels wide, with the columns that fail first. An optional dense block at the end adds rows
// below that front: half its columns are coupled to every fully summed column, and the other half are too many explicit
// zeros for the front to merge into the block's front. So it's a non-root front that pivots on variables and delays the
// constraint columns that still fail.
template <typename Scalar, typename StorageIndex, int Options>
void test_failing_columns_before_pivots() {
using Mat = SparseMatrix<Scalar, Options, StorageIndex>;
using RealScalar = typename NumTraits<Scalar>::Real;
using Natural = NaturalOrdering<StorageIndex>;
const RealScalar eps = NumTraits<RealScalar>::epsilon();
for (const Index numConstraints : {Index(5), Index(40), Index(70)}) {
for (const Index numVariables : {Index(5), Index(40), Index(70)}) {
for (const Index numTail : {Index(0), numConstraints}) {
for (const RealScalar h : {RealScalar(1e3), RealScalar(1) / numext::sqrt(eps)}) {
const Index numFully = numConstraints + numVariables;
const Index numRoot = 2 * numTail;
const Index size = numFully + numRoot;
Triplets<Mat> triplets;
const auto addOffDiagonal = [&](Index row, Index col, const Scalar& value) {
triplets.emplace_back(row, col, value);
triplets.emplace_back(col, row, numext::conj(value));
};
// The magnitudes vary so that entries can't cancel into an exactly singular matrix.
const auto randomNonZero = []() {
return Scalar(randomUnit<Scalar>() * internal::random<RealScalar>(0.5, 1));
};
for (Index i = 0; i < numConstraints; ++i) {
triplets.emplace_back(i, i, Scalar(-eps));
for (Index j = 0; j < i; ++j) addOffDiagonal(i, j, Scalar(0));
for (Index j = numConstraints; j < numFully; ++j) {
addOffDiagonal(j, i, randomNonZero());
}
}
for (Index i = numConstraints; i < numFully; ++i) {
triplets.emplace_back(i, i, Scalar(h));
for (Index j = numConstraints; j < i; ++j) addOffDiagonal(i, j, Scalar(0));
}
for (Index i = 0; i < numRoot; ++i) {
triplets.emplace_back(numFully + i, numFully + i, Scalar(RealScalar(i % 2 == 0 ? 1 : -1)));
for (Index j = 0; j < i; ++j) addOffDiagonal(numFully + i, numFully + j, randomNonZero());
}
for (Index i = 0; i < numTail; ++i) {
for (Index j = 0; j < numFully; ++j) addOffDiagonal(numFully + i, j, randomNonZero());
}
const Mat full = fromTriplets<Mat>(size, triplets);
MultifrontalLDLT<Mat, Lower, Natural> natural;
CALL_SUBTEST(checkIndefiniteFactorization(natural, full, full));
MultifrontalLDLT<Mat> amd;
CALL_SUBTEST(checkIndefiniteFactorization(amd, full, full));
}
}
}
}
}
// Merged fronts on diagonally dominant matrices, where no pivot is delayed, so nonZeros() counts the analysis's stored
// entries. SimplicialLDLT gives the fill without explicit zeros.
template <typename Scalar, typename StorageIndex, int Options>
void test_relaxed_fronts() {
using Mat = SparseMatrix<Scalar, Options, StorageIndex>;
using RealScalar = typename NumTraits<Scalar>::Real;
using DenseVector = Matrix<Scalar, Dynamic, 1>;
using Natural = NaturalOrdering<StorageIndex>;
const auto addOffDiagonal = [](Triplets<Mat>& triplets, Index row, Index col) {
const Scalar value = randomUnit<Scalar>();
triplets.emplace_back(row, col, value);
triplets.emplace_back(col, row, numext::conj(value));
};
const auto addDiagonal = [](Triplets<Mat>& triplets, Index size, Index value) {
for (Index i = 0; i < size; ++i) triplets.emplace_back(i, i, Scalar(RealScalar(value)));
};
// Returns the number of explicit zeros in L, which no front's limit lets exceed 30% of its entries.
const auto verifyAgainstExactFill = [](const auto& solver, const auto& exact, const Mat& matrix) {
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY_IS_EQUAL(exact.info(), Success);
VERIFY_IS_EQUAL(solver.numPositive(), matrix.rows());
const Index size = matrix.rows();
const Index exactNonZeros = exact.matrixL().nestedExpression().nonZeros();
const Index stored = solver.nonZeros() + size;
VERIFY(solver.nonZeros() >= exactNonZeros);
VERIFY(RealScalar(0.7) * RealScalar(stored) <= RealScalar(exactNonZeros + size));
const DenseVector b = DenseVector::Random(size);
const DenseVector x = solver.solve(b);
VERIFY(relativeResidual(matrix, x, b) <= RealScalar(100 * size) * NumTraits<RealScalar>::epsilon());
return solver.nonZeros() - exactNonZeros;
};
{
// A tridiagonal matrix's fundamental fronts have one pivot and one row below. Merging two adds one explicit zero.
const Index size = 30;
Triplets<Mat> triplets;
addDiagonal(triplets, size, 4);
for (Index i = 0; i + 1 < size; ++i) addOffDiagonal(triplets, i + 1, i);
const Mat matrix = fromTriplets<Mat>(size, triplets);
MultifrontalLDLT<Mat, Lower, Natural> solver(matrix);
SimplicialLDLT<Mat, Lower, Natural> exact(matrix);
VERIFY(verifyAgainstExactFill(solver, exact, matrix) > 0);
CALL_SUBTEST(checkPermutation(solver, matrix));
}
{
// A separator with two children: a dense block coupled to one of its rows, then one column coupled to all of them.
// The tree's postorder puts the block between the column and the separator. The column's merge adds no explicit
// zeros, so it moves past the block to be contiguous with the separator. The block's merge would add too many.
const Index blockSize = 8;
const Index separatorSize = 8;
const Index column = blockSize;
const Index separator = blockSize + 1;
const Index size = separator + separatorSize;
Triplets<Mat> triplets;
addDiagonal(triplets, size, 2 * size);
for (Index j = 0; j < blockSize; ++j) {
for (Index i = j + 1; i < blockSize; ++i) addOffDiagonal(triplets, i, j);
addOffDiagonal(triplets, separator, j);
}
for (Index j = separator; j < size; ++j) {
addOffDiagonal(triplets, j, column);
for (Index i = j + 1; i < size; ++i) addOffDiagonal(triplets, i, j);
}
const Mat matrix = fromTriplets<Mat>(size, triplets);
MultifrontalLDLT<Mat, Lower, Natural> solver(matrix);
SimplicialLDLT<Mat, Lower, Natural> exact(matrix);
VERIFY_IS_EQUAL(verifyAgainstExactFill(solver, exact, matrix), Index(0));
VERIFY_IS_EQUAL(Index(solver.permutationP().indices()[column]), blockSize);
CALL_SUBTEST(checkPermutation(solver, matrix));
}
for (int repeat = 0; repeat < g_repeat; ++repeat) {
// Random patterns over the front sizes that the limits distinguish.
const Index size = internal::random<Index>(2, 150);
Triplets<Mat> triplets;
// Repeated entries are summed, so a row's off-diagonal entries can add up to the 4 * size drawn below.
addDiagonal(triplets, size, 4 * size + 1);
const Index offDiagonals = size * internal::random<Index>(0, 4);
for (Index k = 0; k < offDiagonals; ++k) {
const Index i = internal::random<Index>(0, size - 1);
const Index j = internal::random<Index>(0, size - 1);
if (i != j) addOffDiagonal(triplets, (std::max)(i, j), (std::min)(i, j));
}
const Mat matrix = fromTriplets<Mat>(size, triplets);
MultifrontalLDLT<Mat, Lower, Natural> natural(matrix);
SimplicialLDLT<Mat, Lower, Natural> exactNatural(matrix);
verifyAgainstExactFill(natural, exactNatural, matrix);
MultifrontalLDLT<Mat> amd(matrix);
SimplicialLDLT<Mat> exactAmd(matrix);
verifyAgainstExactFill(amd, exactAmd, matrix);
}
}
// Reference determinants come from dense LU, whose error is bounded by the conditioning.
template <typename Scalar, typename StorageIndex, int Options>
void test_determinant() {
using Mat = SparseMatrix<Scalar, Options, StorageIndex>;
using RealScalar = typename NumTraits<Scalar>::Real;
using DenseMatrix = Matrix<Scalar, Dynamic, Dynamic>;
MultifrontalLDLT<Mat, Lower> lowerSolver;
MultifrontalLDLT<Mat, Upper> upperSolver;
// The SPD problems' determinants can be past float's range: one drawn with n = 27 was 9e44.
if (std::numeric_limits<RealScalar>::max_exponent10 > 44) {
check_sparse_spd_determinant(lowerSolver);
check_sparse_spd_determinant(upperSolver);
check_sparse_spd_log_abs_determinant(lowerSolver);
check_sparse_spd_log_abs_determinant(upperSolver);
}
for (int repeat = 0; repeat < g_repeat; ++repeat) {
const Index size = internal::random<Index>(1, 30);
const Mat full = randomHermitian<Mat>(size, 2 * size, internal::random<bool>() && size > 1);
const DenseMatrix dense = full;
lowerSolver.compute(full);
VERIFY_IS_EQUAL(lowerSolver.info(), Success);
check_sparse_determinant(lowerSolver, full, dense);
// A Hermitian matrix has a real determinant.
const Scalar det = lowerSolver.determinant();
VERIFY_IS_EQUAL(numext::imag(det), RealScalar(0));
VERIFY_IS_EQUAL(lowerSolver.absDeterminant(), numext::abs(numext::real(det)));
VERIFY_IS_EQUAL(lowerSolver.signDeterminant(), Scalar(numext::real(det) > RealScalar(0) ? 1 : -1));
VERIFY_IS_APPROX(numext::exp(lowerSolver.logAbsDeterminant()), lowerSolver.absDeterminant());
}
// [0 s; s 0] has determinant -s^2, which must not lose its sign to a 2x2 block's scaling.
const RealScalar s(4);
const Mat block = fromTriplets<Mat>(2, {{1, 0, Scalar(s)}, {0, 1, Scalar(s)}});
lowerSolver.compute(block);
VERIFY_IS_APPROX(lowerSolver.determinant(), Scalar(-16));
VERIFY_IS_APPROX(lowerSolver.absDeterminant(), RealScalar(16));
VERIFY_IS_EQUAL(lowerSolver.signDeterminant(), Scalar(-1));
VERIFY_IS_APPROX(lowerSolver.logAbsDeterminant(), RealScalar(2) * numext::log(s));
// -big^2 overflows, but its log and sign don't.
const RealScalar big = RealScalar(2) * numext::sqrt(NumTraits<RealScalar>::highest());
lowerSolver.compute(fromTriplets<Mat>(2, {{1, 0, Scalar(big)}, {0, 1, Scalar(big)}}));
VERIFY_IS_EQUAL(lowerSolver.info(), Success);
VERIFY(!(numext::isfinite)(numext::real(lowerSolver.determinant())));
VERIFY_IS_EQUAL(lowerSolver.signDeterminant(), Scalar(-1));
VERIFY_IS_APPROX(lowerSolver.logAbsDeterminant(), RealScalar(2) * numext::log(big));
const Mat singular = kktChain<Mat>(3, Scalar(0));
lowerSolver.compute(singular);
VERIFY_IS_EQUAL(lowerSolver.info(), NumericalIssue);
VERIFY_IS_EQUAL(lowerSolver.determinant(), Scalar(0));
VERIFY_IS_EQUAL(lowerSolver.absDeterminant(), RealScalar(0));
VERIFY_IS_EQUAL(lowerSolver.signDeterminant(), Scalar(0));
VERIFY_IS_EQUAL(lowerSolver.logAbsDeterminant(), -NumTraits<RealScalar>::infinity());
// The zero pivot comes last, after the product of the other three has overflowed.
const RealScalar huge = numext::sqrt(NumTraits<RealScalar>::highest());
const MultifrontalLDLT<Mat, Lower, NaturalOrdering<StorageIndex>> overflowing(
fromTriplets<Mat>(4, {{1, 1, Scalar(huge)}, {2, 2, Scalar(huge)}, {3, 3, Scalar(huge)}}));
VERIFY_IS_EQUAL(overflowing.info(), NumericalIssue);
VERIFY_IS_EQUAL(overflowing.vectorD()[3], RealScalar(0));
VERIFY_IS_EQUAL(overflowing.determinant(), Scalar(0));
VERIFY_IS_EQUAL(overflowing.absDeterminant(), RealScalar(0));
MultifrontalLDLT<Mat> unfactorized;
VERIFY_RAISES_ASSERT(unfactorized.determinant());
unfactorized.analyzePattern(block);
VERIFY_RAISES_ASSERT(unfactorized.determinant());
VERIFY_RAISES_ASSERT(unfactorized.logAbsDeterminant());
VERIFY_RAISES_ASSERT(unfactorized.signDeterminant());
}
// Which pivots get delayed depends on the values, so the same analysis must handle fronts of different sizes, and the
// workspace must stop growing at the largest of them.
template <typename Scalar, typename StorageIndex, int Options>
void test_delayed_pivots() {
using Mat = SparseMatrix<Scalar, Options, StorageIndex>;
using RealScalar = typename NumTraits<Scalar>::Real;
using DenseVector = Matrix<Scalar, Dynamic, 1>;
using Natural = NaturalOrdering<StorageIndex>;
const Index numVariables = 20;
const Index size = 2 * numVariables - 1;
const Scalar phase = randomUnit<Scalar>();
const Mat small = kktChain<Mat>(numVariables, Scalar(RealScalar(1e-6)), phase);
const Mat large = kktChain<Mat>(numVariables, Scalar(10), phase);
const DenseVector b = DenseVector::Random(size);
Inertia reference;
reference.positive = numVariables;
reference.negative = numVariables - 1;
// Each variable's small diagonal fails the pivot test until the column reaches a constraint row it can pair with.
MultifrontalLDLT<Mat, Lower, Natural> delaying(small);
VERIFY_IS_EQUAL(delaying.info(), Success);
VERIFY_IS_EQUAL(delaying.numPositive(), numVariables);
VERIFY_IS_EQUAL(delaying.numNegative(), numVariables - 1);
VERIFY_IS_EQUAL(delaying.numZero(), Index(0));
VERIFY(inertiaOfD(delaying) == reference);
// Delays add fill that the symbolic analysis doesn't predict.
const MultifrontalLDLT<Mat, Lower, Natural> notDelaying(large);
VERIFY(delaying.nonZeros() > notDelaying.nonZeros());
CALL_SUBTEST(checkPermutation(delaying, small));
const DenseVector delayedX = delaying.solve(b);
VERIFY(relativeResidual(small, delayedX, b) <= RealScalar(100 * size) * NumTraits<RealScalar>::epsilon());
MultifrontalLDLT<Mat> solver;
solver.analyzePattern(small);
VERIFY_IS_EQUAL(solver.info(), InvalidInput);
DenseVector unfactorizedX(size);
VERIFY_RAISES_ASSERT(unfactorizedX = solver.solve(b));
solver.factorize(small);
solver.factorize(large);
for (const Mat* matrix : {&small, &large, &small}) {
solver.factorize(*matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY(inertiaOfD(solver) == reference);
const DenseVector x = solver.solve(b);
VERIFY(relativeResidual(*matrix, x, b) <= RealScalar(100 * size) * NumTraits<RealScalar>::epsilon());
}
}
// Pivot rows and D must describe the same matrix, including when 2x2 pivots and delays reorder the pivots.
template <typename Scalar, typename StorageIndex, int Options>
void test_permutation() {
using Mat = SparseMatrix<Scalar, Options, StorageIndex>;
using Natural = NaturalOrdering<StorageIndex>;
for (int repeat = 0; repeat < g_repeat; ++repeat) {
const Index size = internal::random<Index>(2, 20);
for (const bool zeroDiagonals : {false, true}) {
const Mat full = randomHermitian<Mat>(size, 2 * size, zeroDiagonals);
MultifrontalLDLT<Mat> amd(full);
VERIFY_IS_EQUAL(amd.info(), Success);
CALL_SUBTEST(checkPermutation(amd, full));
MultifrontalLDLT<Mat, Lower, Natural> natural(full);
VERIFY_IS_EQUAL(natural.info(), Success);
CALL_SUBTEST(checkPermutation(natural, full));
}
}
}
// The threshold trades fill against the bound on |L|, but the factorization must be accurate over its whole range.
template <typename Scalar>
void test_pivot_threshold() {
using Mat = SparseMatrix<Scalar>;
using RealScalar = typename NumTraits<Scalar>::Real;
using DenseVector = Matrix<Scalar, Dynamic, 1>;
MultifrontalLDLT<Mat> solver;
VERIFY_IS_EQUAL(solver.pivotThreshold(), RealScalar(0.01));
VERIFY_RAISES_ASSERT(solver.setPivotThreshold(RealScalar(0)));
VERIFY_RAISES_ASSERT(solver.setPivotThreshold(RealScalar(0.75)));
const Mat matrix = kktChain<Mat>(10, Scalar(RealScalar(1e-2)), randomUnit<Scalar>());
const DenseVector b = DenseVector::Random(matrix.rows());
for (const RealScalar threshold : {RealScalar(1e-4), RealScalar(1e-1), RealScalar(0.5)}) {
VERIFY_IS_EQUAL(solver.setPivotThreshold(threshold).pivotThreshold(), threshold);
solver.compute(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
const DenseVector x = solver.solve(b);
VERIFY(relativeResidual(matrix, x, b) <= RealScalar(matrix.rows()) * NumTraits<RealScalar>::epsilon() / threshold);
}
}
// Inputs on either side of each pivot test's acceptance boundary, which every computation here reaches exactly.
template <typename Scalar>
void test_pivot_acceptance() {
using Mat = SparseMatrix<Scalar>;
using RealScalar = typename NumTraits<Scalar>::Real;
using DenseVector = Matrix<Scalar, Dynamic, 1>;
using Natural = NaturalOrdering<typename Mat::StorageIndex>;
MultifrontalLDLT<Mat, Lower, Natural> solver;
const auto factor = [&solver](const RealScalar& threshold, Index size, const Triplets<Mat>& triplets) {
solver.setPivotThreshold(threshold).compute(fromTriplets<Mat>(size, triplets));
VERIFY_IS_EQUAL(solver.info(), Success);
};
// 1x1: [d 1; 1 0] with u = 1/4 takes d as a pivot if |d| >= 1/4, and the 2x2 otherwise.
const RealScalar quarter(0.25);
for (const RealScalar d : {quarter, numext::nextafter(quarter, RealScalar(0))}) {
factor(quarter, 2, {{0, 0, Scalar(d)}, {1, 0, Scalar(1)}, {0, 1, Scalar(1)}});
VERIFY_IS_EQUAL(solver.subDiagonal()[0], d == quarter ? Scalar(0) : Scalar(1));
}
// 2x2 against the rows below it: [0 1 0; 1 0 w; 0 w 1] with u = 1/2 takes the pair {0, 1} if |w| <= 1/(u |det|) = 2,
// and the pair {1, 2} otherwise.
const RealScalar two(2);
for (const RealScalar w : {two, numext::nextafter(two, RealScalar(3))}) {
factor(RealScalar(0.5), 3,
{{1, 0, Scalar(1)}, {0, 1, Scalar(1)}, {2, 1, Scalar(w)}, {1, 2, Scalar(w)}, {2, 2, Scalar(1)}});
VERIFY_IS_EQUAL(numext::abs(solver.subDiagonal()[0]), w == two ? RealScalar(1) : w);
}
// 2x2 determinant: [a 1/2; 1/2 1] with u = 1/2 fails the 1x1 test on a, and takes the 2x2 if |a - 1/4| >= 1/8.
// Otherwise, it takes 1 as a 1x1 pivot.
const RealScalar eighth(0.125);
for (const RealScalar a : {eighth, numext::nextafter(eighth, quarter)}) {
factor(RealScalar(0.5), 2,
{{0, 0, Scalar(a)}, {1, 0, Scalar(RealScalar(0.5))}, {0, 1, Scalar(RealScalar(0.5))}, {1, 1, Scalar(1)}});
VERIFY_IS_EQUAL(solver.subDiagonal()[0], a == eighth ? Scalar(RealScalar(0.5)) : Scalar(0));
}
// A nearly singular 2x2 candidate next to an acceptable 1x1 pivot: [b^2 (1 + 4 eps) b; b 1] with b = 2^-10. The
// solve is checked with a consistent right-hand side, since a random one hides the residual along the near-null
// direction.
const RealScalar b = numext::ldexp(RealScalar(1), -10);
const RealScalar a = b * b * (1 + 4 * NumTraits<RealScalar>::epsilon());
const Mat nearlySingular =
fromTriplets<Mat>(2, {{0, 0, Scalar(a)}, {1, 0, Scalar(b)}, {0, 1, Scalar(b)}, {1, 1, Scalar(1)}});
solver.setPivotThreshold(RealScalar(0.01)).compute(nearlySingular);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY_IS_EQUAL(solver.subDiagonal()[0], Scalar(0));
for (int repeat = 0; repeat < g_repeat; ++repeat) {
const DenseVector x0 = DenseVector::Random(2);
const DenseVector rhs = nearlySingular * x0;
const DenseVector x = solver.solve(rhs);
VERIFY(relativeResidual(nearlySingular, x, rhs) <= RealScalar(200) * NumTraits<RealScalar>::epsilon());
}
}
// With the same ordering, every input form is the same matrix. A solver of the same type computes the same
// factorization exactly, and each solver below allocates its own workspace, so the factorization mustn't depend on
// where that is. A solver of another type is compiled separately, and the compiler can contract floating-point
// operations differently in it (as GCC's default -ffp-contract=fast does), so its pivots match and its D differs only
// by rounding.
template <typename Scalar>
void test_input_forms() {
using RealScalar = typename NumTraits<Scalar>::Real;
using ColMat = SparseMatrix<Scalar, ColMajor>;
using RowMat = SparseMatrix<Scalar, RowMajor>;
using DenseMatrix = Matrix<Scalar, Dynamic, Dynamic>;
using Natural = NaturalOrdering<typename ColMat::StorageIndex>;
for (int repeat = 0; repeat < g_repeat; ++repeat) {
const Index size = 30;
const ColMat full = randomHermitian<ColMat>(size, 2 * size, internal::random<bool>());
const ColMat lower = full.template triangularView<Lower>();
const ColMat upper = full.template triangularView<Upper>();
const RowMat rowFull = full;
const RowMat rowUpper = upper;
ColMat uncompressed = lower;
uncompressed.uncompress();
VERIFY(!uncompressed.isCompressed());
// Spare room between the inner vectors moves each entry away from its position in the compressed matrix.
ColMat uncompressedFull = full;
uncompressedFull.reserve(VectorXi::Constant(full.outerSize(), 2));
VERIFY(!uncompressedFull.isCompressed());
MultifrontalLDLT<ColMat, Lower, Natural> fromFull(full);
MultifrontalLDLT<ColMat, Lower, Natural> fromLower(lower);
MultifrontalLDLT<ColMat, Upper, Natural> fromUpper(upper);
MultifrontalLDLT<ColMat, Lower, Natural> fromUncompressed(uncompressed);
MultifrontalLDLT<ColMat, Lower, Natural> fromUncompressedFull(uncompressedFull);
MultifrontalLDLT<RowMat, Lower, Natural> fromRowFull(rowFull);
MultifrontalLDLT<RowMat, Upper, Natural> fromRowUpper(rowUpper);
VERIFY_IS_EQUAL(fromFull.info(), Success);
// Each factorization is exact for P A P^T + E_i with ||E_i||_F <= 64 n eps (||A||_F + || |L| |D| |L^*| ||_F)
// (Higham, Theorem 11.3). To first order, E changes D by the block diagonal of L^-1 E L^-*, since L^-1 dL is
// strictly block lower triangular. So the D of two such factorizations differ entrywise by at most
// ||L^-1||_F^2 ||E_1 - E_2||_F.
const DenseMatrix L = fromFull.matrixL();
const DenseMatrix D = denseD(fromFull);
const DenseMatrix permuted = fromFull.permutationP() * DenseMatrix(full) * fromFull.permutationPinv();
const DenseMatrix Linv = L.template triangularView<UnitLower>().solve(DenseMatrix::Identity(size, size));
const RealScalar backwardError = RealScalar(64 * size) * NumTraits<RealScalar>::epsilon() *
(permuted.norm() + (L.cwiseAbs() * D.cwiseAbs() * L.adjoint().cwiseAbs()).norm());
const RealScalar tolerance = 2 * Linv.squaredNorm() * backwardError;
VERIFY((numext::isfinite)(tolerance));
const auto verifySamePivots = [&](const auto& other) {
VERIFY_IS_EQUAL(other.info(), Success);
VERIFY_IS_EQUAL(other.permutationP().indices(), fromFull.permutationP().indices());
VERIFY(((other.subDiagonal().array() == Scalar(0)) == (fromFull.subDiagonal().array() == Scalar(0))).all());
};
const auto verifySameFactorization = [&](const auto& other) {
verifySamePivots(other);
VERIFY_IS_EQUAL(other.vectorD(), fromFull.vectorD());
VERIFY_IS_EQUAL(other.subDiagonal(), fromFull.subDiagonal());
};
const auto verifyRoundedFactorization = [&](const auto& other) {
verifySamePivots(other);
VERIFY((other.vectorD() - fromFull.vectorD()).cwiseAbs().maxCoeff() <= tolerance);
VERIFY((other.subDiagonal() - fromFull.subDiagonal()).cwiseAbs().maxCoeff() <= tolerance);
};
verifySameFactorization(fromLower);
verifySameFactorization(fromUncompressed);
verifySameFactorization(fromUncompressedFull);
// The analysis holds for the pattern in either storage mode.
MultifrontalLDLT<ColMat, Lower, Natural> acrossModes;
acrossModes.analyzePattern(full);
acrossModes.factorize(uncompressedFull);
verifySameFactorization(acrossModes);
acrossModes.analyzePattern(uncompressedFull);
acrossModes.factorize(full);
verifySameFactorization(acrossModes);
verifyRoundedFactorization(fromUpper);
verifyRoundedFactorization(fromRowFull);
verifyRoundedFactorization(fromRowUpper);
}
}
template <typename Scalar>
void test_small_and_singular() {
using Mat = SparseMatrix<Scalar>;
using RealScalar = typename NumTraits<Scalar>::Real;
using DenseVector = Matrix<Scalar, Dynamic, 1>;
using Natural = NaturalOrdering<typename Mat::StorageIndex>;
const auto inertia = [](Index positive, Index negative, Index zero) {
Inertia result;
result.positive = positive;
result.negative = negative;
result.zero = zero;
return result;
};
{
MultifrontalLDLT<Mat> solver;
VERIFY_RAISES_ASSERT(solver.info());
VERIFY_RAISES_ASSERT(solver.factorize(Mat(1, 1)));
VERIFY_RAISES_ASSERT(solver.numPositive());
VERIFY_RAISES_ASSERT(solver.numNegative());
VERIFY_RAISES_ASSERT(solver.numZero());
VERIFY_RAISES_ASSERT(solver.isPositive());
VERIFY_RAISES_ASSERT(solver.isNegative());
VERIFY_RAISES_ASSERT(solver.vectorD());
VERIFY_RAISES_ASSERT(solver.subDiagonal());
VERIFY_RAISES_ASSERT(solver.permutationP());
VERIFY_RAISES_ASSERT(solver.permutationPinv());
VERIFY_RAISES_ASSERT(solver.nonZeros());
}
{
// analyzePattern() leaves no factorization to read, and factorize() checks the analyzed pattern's entry count.
const Mat diagonal = fromTriplets<Mat>(2, {{0, 0, Scalar(1)}, {1, 1, Scalar(1)}});
const Mat full = fromTriplets<Mat>(2, {{0, 0, Scalar(1)}, {1, 0, Scalar(1)}, {0, 1, Scalar(1)}, {1, 1, Scalar(2)}});
MultifrontalLDLT<Mat> solver(diagonal);
VERIFY_IS_EQUAL(solver.nonZeros(), 0);
solver.analyzePattern(full);
VERIFY_RAISES_ASSERT(solver.vectorD());
VERIFY_RAISES_ASSERT(solver.nonZeros());
VERIFY_RAISES_ASSERT(solver.factorize(diagonal));
solver.analyzePattern(diagonal);
VERIFY_RAISES_ASSERT(solver.factorize(full));
}
{
const Mat matrix(0, 0);
MultifrontalLDLT<Mat> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY_IS_EQUAL(solver.rows(), 0);
VERIFY_IS_EQUAL(solver.vectorD().size(), 0);
const DenseVector x = solver.solve(DenseVector(0));
VERIFY_IS_EQUAL(x.size(), 0);
}
{
const Mat matrix = fromTriplets<Mat>(1, {{0, 0, Scalar(2)}});
MultifrontalLDLT<Mat> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY(solver.isPositive());
VERIFY(!solver.isNegative());
const DenseVector b = DenseVector::Constant(1, Scalar(3));
const DenseVector x = solver.solve(b);
VERIFY_IS_EQUAL(x[0], Scalar(RealScalar(1.5)));
}
{
const Mat matrix = fromTriplets<Mat>(1, {{0, 0, Scalar(-2)}});
MultifrontalLDLT<Mat> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY(!solver.isPositive());
VERIFY(solver.isNegative());
}
{
const Mat matrix = fromTriplets<Mat>(1, {{0, 0, Scalar(0)}});
MultifrontalLDLT<Mat> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), NumericalIssue);
VERIFY(inertiaOfD(solver) == inertia(0, 0, 1));
const DenseVector b = DenseVector::Constant(1, Scalar(3));
const DenseVector x = solver.solve(b);
VERIFY_IS_EQUAL(x[0], Scalar(0));
}
{
// Structurally empty diagonal
const Mat matrix(2, 2);
MultifrontalLDLT<Mat> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), NumericalIssue);
VERIFY(inertiaOfD(solver) == inertia(0, 0, 2));
VERIFY_IS_EQUAL(solver.numZero(), Index(2));
// The zero matrix is semidefinite both ways.
VERIFY(solver.isPositive());
VERIFY(solver.isNegative());
}
for (Index numVariables : {2, 3, 10, 50}) {
// [0 A^*; A 0] is singular because A has fewer rows than columns.
const Mat matrix = kktChain<Mat>(numVariables, Scalar(0));
MultifrontalLDLT<Mat> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), NumericalIssue);
VERIFY(inertiaOfD(solver) == inertia(numVariables - 1, numVariables - 1, 1));
VERIFY_IS_EQUAL(solver.numPositive(), numVariables - 1);
VERIFY_IS_EQUAL(solver.numNegative(), numVariables - 1);
VERIFY_IS_EQUAL(solver.numZero(), Index(1));
VERIFY(!solver.isPositive());
VERIFY(!solver.isNegative());
}
{
// [I A^*; A 0] where A's two rows are equal. The pivots and updates are small integers, so the zero eigenvalue's
// pivot is exactly zero.
Triplets<Mat> triplets;
for (Index i = 0; i < 3; ++i) triplets.emplace_back(i, i, Scalar(1));
for (Index row : {3, 4}) {
for (Index col : {0, 1, 2}) {
triplets.emplace_back(row, col, Scalar(1));
triplets.emplace_back(col, row, Scalar(1));
}
}
const Mat matrix = fromTriplets<Mat>(5, triplets);
MultifrontalLDLT<Mat> solver(matrix);
const Inertia counts = inertiaOfD(solver);
VERIFY_IS_EQUAL(counts.positive, 3);
VERIFY_IS_EQUAL(counts.negative, 1);
VERIFY(solver.vectorD().allFinite());
VERIFY_IS_EQUAL(solver.vectorD().cwiseAbs().minCoeff(), RealScalar(0));
}
{
// A 2x2 pivot with det > 0: the tiny diagonal fails the 1x1 test, and both eigenvalues have the sign of the trace.
const RealScalar tiny(1e-4);
const RealScalar large(2e5);
Mat matrix =
fromTriplets<Mat>(2, {{0, 0, Scalar(tiny)}, {1, 0, Scalar(1)}, {0, 1, Scalar(1)}, {1, 1, Scalar(large)}});
MultifrontalLDLT<Mat, Lower, Natural> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY(numext::abs(solver.subDiagonal()[0]) != RealScalar(0));
VERIFY(solver.isPositive());
VERIFY(!solver.isNegative());
matrix = -matrix;
solver.compute(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY(!solver.isPositive());
VERIFY(solver.isNegative());
}
{
// Disconnected blocks with a zero diagonal, so each root must take a 2x2 pivot.
Triplets<Mat> triplets;
for (Index block = 0; block < 4; ++block) {
triplets.emplace_back(2 * block + 1, 2 * block, Scalar(block + 1));
triplets.emplace_back(2 * block, 2 * block + 1, Scalar(block + 1));
}
const Mat matrix = fromTriplets<Mat>(8, triplets);
MultifrontalLDLT<Mat, Lower, Natural> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY(inertiaOfD(solver) == inertia(4, 4, 0));
// L = I and each x_i = fl(b_j / s), so |A x - b| <= eps |b|, counting the rounding of the residual's product.
const DenseVector b = DenseVector::Random(8);
const DenseVector x = solver.solve(b);
VERIFY(relativeResidual(matrix, x, b) <= NumTraits<RealScalar>::epsilon());
}
}
// A non-finite pivot has no inertia, so the factorization fails and the matrix is neither semidefinite nor counted in
// full, whichever of the 1x1 and 2x2 paths it took.
template <typename Mat>
void verifyNonFiniteFactorization(const Mat& matrix) {
using Natural = NaturalOrdering<typename Mat::StorageIndex>;
const auto verify = [&matrix](const auto& solver) {
VERIFY_IS_EQUAL(solver.info(), NumericalIssue);
VERIFY(!solver.isPositive());
VERIFY(!solver.isNegative());
VERIFY(solver.numPositive() + solver.numNegative() + solver.numZero() < matrix.rows());
VERIFY(!(numext::isfinite)(numext::real(solver.determinant())));
};
verify(MultifrontalLDLT<Mat, Lower, Natural>(matrix));
verify(MultifrontalLDLT<Mat, Upper, Natural>(matrix));
}
template <typename Scalar>
void test_extreme_values() {
using Mat = SparseMatrix<Scalar>;
using RealScalar = typename NumTraits<Scalar>::Real;
using DenseVector = Matrix<Scalar, Dynamic, 1>;
{
// s^2 overflows, but the block scaled by its largest magnitude s has determinant -1.
const RealScalar s = numext::pow(NumTraits<RealScalar>::highest(), RealScalar(0.75));
const Mat matrix = fromTriplets<Mat>(2, {{1, 0, Scalar(s)}, {0, 1, Scalar(s)}});
MultifrontalLDLT<Mat> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY(!solver.isPositive());
VERIFY(!solver.isNegative());
DenseVector b(2);
b << Scalar(s), Scalar(RealScalar(2) * s);
const DenseVector x = solver.solve(b);
VERIFY_IS_EQUAL(x[0], Scalar(2));
VERIFY_IS_EQUAL(x[1], Scalar(1));
}
{
// A 2x2 block of zero diagonals with the smallest normal u, which the 2x2 test divides by. u and s are normal, so
// flush-to-zero gives the same result.
const RealScalar u = (std::numeric_limits<RealScalar>::min)();
const RealScalar s = u / NumTraits<RealScalar>::epsilon();
const Mat matrix = fromTriplets<Mat>(2, {{1, 0, Scalar(s)}, {0, 1, Scalar(s)}});
MultifrontalLDLT<Mat> solver;
solver.setPivotThreshold(u).compute(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY_IS_EQUAL(solver.numPositive(), 1);
VERIFY_IS_EQUAL(solver.numNegative(), 1);
VERIFY_IS_EQUAL(solver.numZero(), 0);
DenseVector b(2);
b << Scalar(s), Scalar(RealScalar(2) * s);
const DenseVector x = solver.solve(b);
VERIFY_IS_EQUAL(x[0], Scalar(2));
VERIFY_IS_EQUAL(x[1], Scalar(1));
}
{
// A 2x2 candidate whose off-diagonal is far below sqrt(|d11 d22|): scaled by d22 = 1, |b|^2 underflows and
// det = d11. Its bound on column 0 is about |d11|/u, so the row below rejects it and the column waits for that row.
const Scalar tinyOffDiagonal =
randomUnit<Scalar>() * RealScalar(0.1) *
numext::sqrt(RealScalar(NumTraits<RealScalar>::epsilon() / NumTraits<RealScalar>::highest()));
const Scalar d11(NumTraits<RealScalar>::epsilon());
const Mat matrix = fromTriplets<Mat>(3, {{0, 0, d11},
{1, 0, tinyOffDiagonal},
{0, 1, Scalar(numext::conj(tinyOffDiagonal))},
{1, 1, Scalar(1)},
{2, 0, Scalar(1)},
{0, 2, Scalar(1)},
{2, 1, Scalar(1)},
{1, 2, Scalar(1)},
{2, 2, Scalar(1)}});
MultifrontalLDLT<Mat, Lower, NaturalOrdering<typename Mat::StorageIndex>> solver;
CALL_SUBTEST(checkIndefiniteFactorization(solver, matrix, matrix));
VERIFY_IS_EQUAL(solver.numPositive(), 2);
VERIFY_IS_EQUAL(solver.numNegative(), 1);
}
{
// Scaled by d22, d11/d22 and |d21|/d22 underflow. The tiny diagonal fails the 1x1 test against d21, and the block's
// scaled determinant underflows, so the 2x2 is rejected in favor of d22 as a 1x1 pivot. det > 0, so both
// eigenvalues are positive.
const RealScalar sqrtHighest = numext::sqrt(NumTraits<RealScalar>::highest());
const RealScalar d21 = RealScalar(1) / sqrtHighest;
const RealScalar d11 = d21 * RealScalar(1e-4);
const RealScalar d22 = numext::pow(NumTraits<RealScalar>::highest(), RealScalar(0.75));
const Scalar offDiagonal = randomUnit<Scalar>() * d21;
const Mat matrix = fromTriplets<Mat>(
2, {{0, 0, Scalar(d11)}, {1, 0, offDiagonal}, {0, 1, Scalar(numext::conj(offDiagonal))}, {1, 1, Scalar(d22)}});
MultifrontalLDLT<Mat, Lower, NaturalOrdering<typename Mat::StorageIndex>> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY(solver.isPositive());
VERIFY(!solver.isNegative());
VERIFY(numext::real(solver.determinant()) > RealScalar(0));
DenseVector b(2);
b << Scalar(d11 + d21), Scalar(d22 + d21);
const DenseVector x = solver.solve(b);
VERIFY(x.allFinite());
VERIFY(relativeResidual(matrix, x, b) <= RealScalar(200) * NumTraits<RealScalar>::epsilon());
}
// Definite 2x2 pivots: d11 fails the 1x1 test against d21, and nothing else bounds the block.
for (const RealScalar sign : {RealScalar(1), RealScalar(-1)}) {
const Scalar offDiagonal = randomUnit<Scalar>() * RealScalar(1e-3);
const Mat matrix = fromTriplets<Mat>(2, {{0, 0, Scalar(sign * RealScalar(1e-7))},
{1, 0, offDiagonal},
{0, 1, Scalar(numext::conj(offDiagonal))},
{1, 1, Scalar(sign * RealScalar(100))}});
MultifrontalLDLT<Mat, Lower, NaturalOrdering<typename Mat::StorageIndex>> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
VERIFY(numext::abs(solver.subDiagonal()[0]) > RealScalar(0));
VERIFY_IS_EQUAL(solver.numPositive(), sign > RealScalar(0) ? Index(2) : Index(0));
VERIFY_IS_EQUAL(solver.numNegative(), sign > RealScalar(0) ? Index(0) : Index(2));
VERIFY_IS_EQUAL(solver.isPositive(), sign > RealScalar(0));
VERIFY_IS_EQUAL(solver.isNegative(), sign < RealScalar(0));
VERIFY(numext::real(solver.determinant()) > RealScalar(0));
}
for (const RealScalar value : {NumTraits<RealScalar>::infinity(), NumTraits<RealScalar>::quiet_NaN()}) {
verifyNonFiniteFactorization(
fromTriplets<Mat>(2, {{0, 0, Scalar(value)}, {1, 0, Scalar(1)}, {0, 1, Scalar(1)}, {1, 1, Scalar(1)}}));
verifyNonFiniteFactorization(
fromTriplets<Mat>(2, {{0, 0, Scalar(1)}, {1, 0, Scalar(value)}, {0, 1, Scalar(value)}, {1, 1, Scalar(1)}}));
// No pivot test can pass on a non-finite column, so the root falls back to taking its largest diagonal.
Triplets<Mat> triplets;
for (Index col = 0; col < 4; ++col) {
for (Index row = 0; row < 4; ++row) triplets.emplace_back(row, col, Scalar(value));
}
verifyNonFiniteFactorization(fromTriplets<Mat>(4, triplets));
// A zero diagonal's non-finite column, in the root's own front and in a column delayed to it.
verifyNonFiniteFactorization(fromTriplets<Mat>(2, {{1, 0, Scalar(value)}, {0, 1, Scalar(value)}}));
verifyNonFiniteFactorization(
fromTriplets<Mat>(3, {{1, 0, Scalar(value)}, {0, 1, Scalar(value)}, {2, 1, Scalar(1)}, {1, 2, Scalar(1)}}));
const Mat single = fromTriplets<Mat>(1, {{0, 0, Scalar(value)}});
verifyNonFiniteFactorization(single);
verifyNonFiniteFactorization(Mat(-single));
// Dense blocks on columns [0, 20) and [20, 40), both coupled to the separator [40, 42). The second block merges
// with the separator into the root, and the first stays a front of order 22 below it, whose columns are long
// enough to take the vectorized column maximum. The non-finite entry is in that front's pivot columns, then in a
// row below them.
for (const Index row : {Index(1), Index(40)}) {
Triplets<Mat> separated;
const auto add = [&](Index i, Index j) {
const Scalar entry = i == j ? Scalar(100) : (i == row && j == 0 ? Scalar(value) : Scalar(1));
separated.emplace_back(i, j, entry);
if (i != j) separated.emplace_back(j, i, entry);
};
for (const Index first : {Index(0), Index(20)}) {
for (Index j = first; j < first + 20; ++j) {
for (Index i = j; i < first + 20; ++i) add(i, j);
for (Index i = 40; i < 42; ++i) add(i, j);
}
}
add(40, 40);
add(41, 40);
add(41, 41);
verifyNonFiniteFactorization(fromTriplets<Mat>(42, separated));
}
}
// Updates can overflow on finite input. M = 0.6 * highest is in range, and so is every entry below.
const RealScalar M = RealScalar(0.6) * NumTraits<RealScalar>::highest();
// The pivot M leaves the Schur complement -2M on the diagonal.
verifyNonFiniteFactorization(
fromTriplets<Mat>(2, {{0, 0, Scalar(M)}, {1, 0, Scalar(M)}, {0, 1, Scalar(M)}, {1, 1, Scalar(-M)}}));
// The pivot M leaves -2M off the diagonal, between the finite pivots 0.5 M. Eliminating either one carries the
// overflow on to the other.
const RealScalar d = RealScalar(1.5) * M;
verifyNonFiniteFactorization(fromTriplets<Mat>(3, {{0, 0, Scalar(M)},
{1, 0, Scalar(M)},
{2, 0, Scalar(M)},
{0, 1, Scalar(M)},
{1, 1, Scalar(d)},
{2, 1, Scalar(-M)},
{0, 2, Scalar(M)},
{1, 2, Scalar(-M)},
{2, 2, Scalar(d)}}));
}
// A zero diagonal with finite off-diagonal entries whose magnitude overflows.
template <typename RealScalar>
void test_overflowing_complex_magnitude() {
using Scalar = std::complex<RealScalar>;
using Mat = SparseMatrix<Scalar>;
const RealScalar m = RealScalar(0.8) * NumTraits<RealScalar>::highest();
const Scalar z(m, m);
VERIFY(!(numext::isfinite)(numext::abs(z)));
verifyNonFiniteFactorization(fromTriplets<Mat>(2, {{1, 0, z}, {0, 1, numext::conj(z)}}));
}
// Only the real part of a diagonal entry is read, whether its imaginary part is finite or not.
template <typename RealScalar>
void test_non_real_diagonal() {
using Scalar = std::complex<RealScalar>;
using Mat = SparseMatrix<Scalar>;
using RealVector = Matrix<RealScalar, Dynamic, 1>;
using Natural = NaturalOrdering<typename Mat::StorageIndex>;
const Scalar offDiagonal(1, 0.5);
const Mat real = fromTriplets<Mat>(
2, {{0, 0, Scalar(2)}, {1, 0, numext::conj(offDiagonal)}, {0, 1, offDiagonal}, {1, 1, Scalar(-3)}});
// D = (2, -3 - |1 + 0.5i|^2 / 2).
const RealVector expectedD = (RealVector(2) << RealScalar(2), RealScalar(-3.625)).finished();
for (const RealScalar imag : {RealScalar(1), NumTraits<RealScalar>::infinity(), NumTraits<RealScalar>::quiet_NaN()}) {
Mat matrix = real;
matrix.coeffRef(0, 0) = Scalar(2, imag);
matrix.coeffRef(1, 1) = Scalar(-3, -imag);
MultifrontalLDLT<Mat, Lower, Natural> lower(matrix);
MultifrontalLDLT<Mat, Upper, Natural> upper(matrix);
VERIFY_IS_EQUAL(lower.info(), Success);
VERIFY_IS_EQUAL(upper.info(), Success);
VERIFY_IS_EQUAL(lower.vectorD(), expectedD);
VERIFY_IS_EQUAL(upper.vectorD(), expectedD);
VERIFY_IS_EQUAL(lower.numPositive(), 1);
VERIFY_IS_EQUAL(lower.numNegative(), 1);
}
}
// A scalar with no vectorization, non-trivial construction, and only the operations NumTraits documents.
template <typename Scalar>
void test_custom_scalar() {
using Mat = SparseMatrix<Scalar>;
using DenseVector = Matrix<Scalar, Dynamic, 1>;
{
const Mat matrix = kktChain<Mat>(5, Scalar(0.25f));
MultifrontalLDLT<Mat> solver(matrix);
VERIFY_IS_EQUAL(solver.info(), Success);
const DenseVector b = DenseVector::Random(matrix.rows());
const DenseVector x = solver.solve(b);
VERIFY(relativeResidual(matrix, x, b) <= Scalar(100 * matrix.rows()) * NumTraits<Scalar>::epsilon());
}
VERIFY_IS_EQUAL(AnnoyingScalar::instances, 0);
}
EIGEN_DECLARE_TEST(multifrontal_ldlt) {
CALL_SUBTEST_1((test_positive_definite<double, int, ColMajor>()));
CALL_SUBTEST_2((test_positive_definite<std::complex<double>, int, ColMajor>()));
CALL_SUBTEST_3((test_positive_definite<double, long int, RowMajor>()));
CALL_SUBTEST_4((test_random_indefinite<float, int, ColMajor>()));
CALL_SUBTEST_5((test_random_indefinite<double, int, ColMajor>()));
CALL_SUBTEST_6((test_random_indefinite<std::complex<float>, int, ColMajor>()));
CALL_SUBTEST_7((test_random_indefinite<std::complex<double>, long int, ColMajor>()));
CALL_SUBTEST_8((test_random_indefinite<double, long int, RowMajor>()));
CALL_SUBTEST_9((test_random_indefinite<std::complex<double>, int, RowMajor>()));
CALL_SUBTEST_10((test_delayed_pivots<float, int, ColMajor>()));
CALL_SUBTEST_10((test_delayed_pivots<double, long int, RowMajor>()));
CALL_SUBTEST_10((test_delayed_pivots<std::complex<double>, int, ColMajor>()));
CALL_SUBTEST_11(test_dense_front_with_2x2_pivots<float>());
CALL_SUBTEST_11(test_dense_front_with_2x2_pivots<double>());
CALL_SUBTEST_11(test_dense_front_with_2x2_pivots<std::complex<float>>());
CALL_SUBTEST_11(test_dense_front_with_2x2_pivots<std::complex<double>>());
CALL_SUBTEST_12(test_pivot_threshold<double>());
CALL_SUBTEST_12(test_pivot_threshold<std::complex<double>>());
CALL_SUBTEST_13(test_input_forms<double>());
CALL_SUBTEST_13(test_input_forms<std::complex<double>>());
CALL_SUBTEST_14(test_small_and_singular<float>());
CALL_SUBTEST_14(test_small_and_singular<double>());
CALL_SUBTEST_14(test_small_and_singular<std::complex<double>>());
CALL_SUBTEST_15(test_extreme_values<float>());
CALL_SUBTEST_15(test_extreme_values<double>());
CALL_SUBTEST_15(test_extreme_values<std::complex<double>>());
CALL_SUBTEST_15(test_overflowing_complex_magnitude<float>());
CALL_SUBTEST_15(test_overflowing_complex_magnitude<double>());
CALL_SUBTEST_15(test_non_real_diagonal<float>());
CALL_SUBTEST_15(test_non_real_diagonal<double>());
CALL_SUBTEST_16(test_custom_scalar<AnnoyingScalar>());
CALL_SUBTEST_17((test_large_fronts<float, int, ColMajor>()));
CALL_SUBTEST_18((test_large_fronts<double, int, ColMajor>()));
CALL_SUBTEST_19((test_large_fronts<std::complex<double>, int, ColMajor>()));
CALL_SUBTEST_20((test_large_fronts<std::complex<float>, long int, RowMajor>()));
CALL_SUBTEST_21((test_determinant<float, int, ColMajor>()));
CALL_SUBTEST_21((test_determinant<double, int, ColMajor>()));
CALL_SUBTEST_21((test_determinant<std::complex<double>, int, RowMajor>()));
CALL_SUBTEST_22((test_permutation<float, int, ColMajor>()));
CALL_SUBTEST_22((test_permutation<double, int, ColMajor>()));
CALL_SUBTEST_22((test_permutation<std::complex<double>, long int, RowMajor>()));
CALL_SUBTEST_23((test_failing_columns_before_pivots<float, int, ColMajor>()));
CALL_SUBTEST_23((test_failing_columns_before_pivots<double, long int, RowMajor>()));
CALL_SUBTEST_23((test_failing_columns_before_pivots<std::complex<double>, int, ColMajor>()));
CALL_SUBTEST_24((test_relaxed_fronts<float, int, ColMajor>()));
CALL_SUBTEST_24((test_relaxed_fronts<double, long int, RowMajor>()));
CALL_SUBTEST_24((test_relaxed_fronts<std::complex<double>, int, ColMajor>()));
CALL_SUBTEST_25(test_pivot_acceptance<float>());
CALL_SUBTEST_25(test_pivot_acceptance<double>());
CALL_SUBTEST_25(test_pivot_acceptance<std::complex<double>>());
}