blob: 1b06ea51a91535945ee3fbbadeee52484314241b [file] [edit]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Copyright (C) 2026 Rasmus Munk Larsen <rmlarsen@gmail.com>
//
// This Source Code Form is subject to the terms of the Mozilla
// Public License v. 2.0. If a copy of the MPL was not distributed
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// SPDX-License-Identifier: MPL-2.0
#include "main.h"
// Substitution is backward stable: the computed inverse X of a triangular T satisfies
// |T X - I| <= c*n*eps*|T|*|X| entrywise, for a small constant c (Higham, Accuracy and Stability of
// Numerical Algorithms, 2nd ed., Thm 8.5). The factors below absorb c and the entrywise-to-norm step;
// the largest ratio measured over the families tested here is 2, at n = 1.
static constexpr int kResidualFactor = 16;
// Forward error against the reference solve, bounded by the residual bound times cond(T) <= |T|*|X|.
static constexpr int kForwardFactor = 16;
// The part of the storage that a triangularView<mode>() does not own, and that an in-place kernel must
// therefore leave bit for bit unchanged: the opposite triangle, plus the diagonal under UnitDiag.
constexpr unsigned int untouched_mode(unsigned int mode) {
return ((mode & Lower) != 0 ? unsigned(Upper) : unsigned(Lower)) | ((mode & UnitDiag) != 0 ? 0u : unsigned(ZeroDiag));
}
template <unsigned int Mode, typename MatrixType, typename OperandType>
void check_triangular_inverse(const MatrixType& t, OperandType& x, bool well_conditioned) {
using Scalar = typename MatrixType::Scalar;
using RealScalar = typename NumTraits<Scalar>::Real;
using DenseType = Matrix<Scalar, Dynamic, Dynamic>;
const Index n = t.rows();
const RealScalar eps = NumTraits<RealScalar>::epsilon();
x = t;
x.template triangularView<Mode>().inverseInPlace();
constexpr unsigned int kUntouched = untouched_mode(Mode);
VERIFY_IS_CWISE_EQUAL(x.template triangularView<kUntouched>().toDenseMatrix(),
t.template triangularView<kUntouched>().toDenseMatrix());
const DenseType td = t.template triangularView<Mode>().toDenseMatrix();
const DenseType xd = x.template triangularView<Mode>().toDenseMatrix();
const RealScalar residual = (td * xd - DenseType::Identity(n, n)).norm();
const RealScalar residual_bound = RealScalar(kResidualFactor * n) * eps * td.norm() * xd.norm();
VERIFY((numext::isfinite)(residual_bound));
VERIFY(residual <= residual_bound);
if (!well_conditioned) return;
MatrixType reference = MatrixType::Identity(n, n);
t.template triangularView<Mode>().solveInPlace(reference);
const DenseType referenced = reference.template triangularView<Mode>().toDenseMatrix();
const RealScalar forward_bound = RealScalar(kForwardFactor * n) * eps * td.norm() * xd.norm() * xd.norm();
VERIFY((numext::isfinite)(forward_bound));
VERIFY((xd - referenced).norm() <= forward_bound);
}
// Strictly diagonally dominant, so cond(T) stays O(1) and the forward comparison is meaningful. The
// scaling also holds with a unit diagonal, where the stored diagonal is ignored.
template <typename MatrixType>
MatrixType well_conditioned_triangular(Index n) {
using Scalar = typename MatrixType::Scalar;
using RealScalar = typename NumTraits<Scalar>::Real;
MatrixType t = MatrixType::Random(n, n) / RealScalar(n + 1);
t.diagonal().array() += Scalar(1);
return t;
}
// Unscaled off-diagonals, so |T^-1| can grow exponentially in n; only the residual bound applies.
template <typename MatrixType>
MatrixType ill_conditioned_triangular(Index n) {
using Scalar = typename MatrixType::Scalar;
MatrixType t = MatrixType::Random(n, n);
t.diagonal().array() += Scalar(2);
return t;
}
template <typename MatrixType, typename OperandType>
void check_inverse_all_modes(const MatrixType& t, OperandType& x, bool well_conditioned) {
check_triangular_inverse<Lower>(t, x, well_conditioned);
check_triangular_inverse<Upper>(t, x, well_conditioned);
check_triangular_inverse<UnitLower>(t, x, well_conditioned);
check_triangular_inverse<UnitUpper>(t, x, well_conditioned);
}
template <typename MatrixType>
void triangular_inverse_all_modes(Index n) {
MatrixType x(n, n);
check_inverse_all_modes(well_conditioned_triangular<MatrixType>(n), x, /*well_conditioned=*/true);
check_inverse_all_modes(ill_conditioned_triangular<MatrixType>(n), x, /*well_conditioned=*/false);
}
// The destination of LLT::inverse() need not be a plain object, so run the kernel through a block of a
// larger matrix: a non-zero offset and an outer stride that does not match the block width.
template <typename MatrixType>
void triangular_inverse_on_block(Index n) {
const Index row_offset = 2, col_offset = 3;
MatrixType host = MatrixType::Random(n + 3, n + 5);
MatrixType expected = host;
Block<MatrixType> x(host, row_offset, col_offset, n, n);
check_triangular_inverse<Lower>(well_conditioned_triangular<MatrixType>(n), x, /*well_conditioned=*/true);
// Nothing outside the block moved.
expected.block(row_offset, col_offset, n, n) = x;
VERIFY_IS_CWISE_EQUAL(host, expected);
}
// An operand whose inner stride is a run-time value, which is what a Map or Ref with a dynamic inner
// stride hands the blocked kernels: their TRSM and TRMM steps must reach each panel through that
// value, since the compile-time InnerStrideAtCompileTime they also carry is Dynamic.
template <typename MatrixType>
void triangular_inverse_on_strided_map(Index n) {
using Scalar = typename MatrixType::Scalar;
using VectorType = Matrix<Scalar, Dynamic, 1>;
using StrideType = Stride<Dynamic, Dynamic>;
const Index inner = 2, outer = 2 * n + 3;
const Index buffer_size = numext::maxi(Index(1), (n - 1) * (outer + inner) + 1);
VectorType buffer = VectorType::Random(buffer_size);
const VectorType before = buffer;
Map<MatrixType, 0, StrideType> x(buffer.data(), n, n, StrideType(outer, inner));
check_triangular_inverse<Lower>(well_conditioned_triangular<MatrixType>(n), x, /*well_conditioned=*/true);
check_triangular_inverse<Upper>(well_conditioned_triangular<MatrixType>(n), x, /*well_conditioned=*/true);
// Nothing in the gaps between the mapped coefficients moved.
std::vector<bool> mapped(buffer_size, false);
for (Index j = 0; j < n; ++j)
for (Index i = 0; i < n; ++i) mapped[static_cast<std::size_t>(&x.coeffRef(i, j) - buffer.data())] = true;
for (Index k = 0; k < buffer_size; ++k)
if (!mapped[k]) VERIFY_IS_EQUAL(buffer[k], before[k]);
}
// internal::triangular_adjoint_square_in_place() is what LLT::inverse() uses to turn L^-1 into
// L^-* L^-1. It is not public API, but it needs coverage of its own: a Cholesky factor always has a
// real diagonal, so the conjugations in the kernel are unobservable through LLT.
template <int UpLo, typename MatrixType>
void adjoint_square_in_place(Index n) {
using Scalar = typename MatrixType::Scalar;
using RealScalar = typename NumTraits<Scalar>::Real;
using DenseType = Matrix<Scalar, Dynamic, Dynamic>;
const RealScalar eps = NumTraits<RealScalar>::epsilon();
const MatrixType t = ill_conditioned_triangular<MatrixType>(n);
MatrixType a = t;
internal::triangular_adjoint_square_in_place<UpLo>(a);
const DenseType td = t.template triangularView<UpLo>().toDenseMatrix();
const DenseType expected = UpLo == Lower ? DenseType(td.adjoint() * td) : DenseType(td * td.adjoint());
const DenseType computed = a.template triangularView<UpLo>().toDenseMatrix();
// n accumulations of products of entries of T, so the rounding error is bounded by ~n*eps*|T|^2.
const RealScalar bound = RealScalar(kResidualFactor * n) * eps * td.norm() * td.norm();
VERIFY((numext::isfinite)(bound));
VERIFY((computed - expected.template triangularView<UpLo>().toDenseMatrix()).norm() <= bound);
constexpr unsigned int kUntouched = untouched_mode(UpLo);
VERIFY_IS_CWISE_EQUAL(a.template triangularView<kUntouched>().toDenseMatrix(),
t.template triangularView<kUntouched>().toDenseMatrix());
}
template <typename MatrixType>
void adjoint_square_both_triangles(Index n) {
adjoint_square_in_place<Lower, MatrixType>(n);
adjoint_square_in_place<Upper, MatrixType>(n);
}
EIGEN_DECLARE_TEST(triangular_in_place) {
// 31/32 straddles the unblocked-to-blocked switch, 40 and 65 are not multiples of the block size,
// and at 256 the heuristic first picks a block big enough for the nested kernels to block in turn.
const Index sizes[] = {0, 1, 2, 3, 7, 16, 31, 32, 33, 40, 64, 65, 128, 256};
for (int i = 0; i < g_repeat; i++) {
for (Index n : sizes) {
EIGEN_UNUSED_VARIABLE(n); // the part-6 build compiles no subtest that takes n
CALL_SUBTEST_1(triangular_inverse_all_modes<MatrixXd>(n));
CALL_SUBTEST_2(triangular_inverse_all_modes<MatrixXcd>(n));
CALL_SUBTEST_3(triangular_inverse_all_modes<MatrixXf>(n));
CALL_SUBTEST_4((triangular_inverse_all_modes<Matrix<double, Dynamic, Dynamic, RowMajor>>(n)));
CALL_SUBTEST_5(triangular_inverse_on_block<MatrixXd>(n));
CALL_SUBTEST_5(triangular_inverse_on_strided_map<MatrixXd>(n));
CALL_SUBTEST_7(adjoint_square_both_triangles<MatrixXd>(n));
CALL_SUBTEST_8(adjoint_square_both_triangles<MatrixXcd>(n));
CALL_SUBTEST_9(adjoint_square_both_triangles<MatrixXf>(n));
CALL_SUBTEST_10((adjoint_square_both_triangles<Matrix<double, Dynamic, Dynamic, RowMajor>>(n)));
}
CALL_SUBTEST_6((triangular_inverse_all_modes<Matrix<double, 1, 1>>(1)));
CALL_SUBTEST_6(triangular_inverse_all_modes<Matrix4d>(4));
CALL_SUBTEST_6((triangular_inverse_all_modes<Matrix<std::complex<float>, 8, 8>>(8)));
}
}