// This file is part of Eigen, a lightweight C++ template library // for linear algebra. // // Copyright (C) 2026 Rasmus Munk Larsen // // 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 // Enable Eigen's runtime malloc tracking so bunchkaufman_no_malloc() can assert that compute() // performs no heap allocation when the workspace is pre-allocated. (Malloc stays allowed by default; // only that one subtest toggles it off.) Must be defined before any Eigen header is included. #define EIGEN_RUNTIME_NO_MALLOC #include "main.h" #include #include #include #include "solverbase.h" template typename MatrixType::RealScalar matrix_l1_norm(const MatrixType& m) { if (m.cols() == 0) return typename MatrixType::RealScalar(0); MatrixType symm = m.template selfadjointView(); return symm.cwiseAbs().colwise().sum().maxCoeff(); } // Reconstruct the block-diagonal D from vectorD() / subDiagonal() and check that // P^T L D L^* P == A and that matrixL()/matrixU() are consistent. template void verify_factorization(const MatrixType& A, const BKType& bk) { typedef typename MatrixType::Scalar Scalar; const Index n = A.rows(); VERIFY_IS_APPROX(A, bk.reconstructedMatrix()); // The diagonal of D must be exactly real (not merely up to roundoff): the factorization // re-realifies the trailing diagonal after each Hermitian update, like LAPACK xHETF2/xHETRF. for (Index k = 0; k < n; ++k) { VERIFY(numext::is_exactly_zero(numext::imag(bk.vectorD().coeff(k)))); } // Build D explicitly from vectorD() and subDiagonal() and reconstruct manually. MatrixType D = MatrixType::Zero(n, n); D.diagonal() = bk.vectorD(); for (Index k = 0; k + 1 < n; ++k) { Scalar s = bk.subDiagonal()(k); if (!numext::is_exactly_zero(s)) { D(k + 1, k) = s; D(k, k + 1) = numext::conj(s); } } // matrixU() should be the adjoint of matrixL(). MatrixType L = bk.matrixL(); MatrixType U = bk.matrixU(); VERIFY_IS_APPROX(L.adjoint(), U); // P^T L D L^* P MatrixType PtL = bk.transpositionsP().transpose() * L; MatrixType recon = PtL * D * PtL.adjoint(); VERIFY_IS_APPROX(A, recon); } // Core test on a Hermitian indefinite matrix `symm` (full, self-adjoint). template void bunchkaufman_solve_and_reconstruct(const MatrixType& symm) { typedef typename MatrixType::Scalar Scalar; typedef Matrix SquareMatrixType; typedef Matrix VectorType; const Index rows = symm.rows(); const Index cols = symm.cols(); SquareMatrixType symmLo = symm.template triangularView(); SquareMatrixType symmUp = symm.template triangularView(); BunchKaufman bk_lo(symmLo); VERIFY(bk_lo.info() == Success); verify_factorization(SquareMatrixType(symm), bk_lo); check_solverbase(symm, bk_lo, rows, rows, 1); check_solverbase(symm, bk_lo, rows, cols, rows); BunchKaufman bk_up(symmUp); VERIFY(bk_up.info() == Success); verify_factorization(SquareMatrixType(symm), bk_up); check_solverbase(symm, bk_up, rows, rows, 1); // MatrixBase / SelfAdjointView entry points. verify_factorization(SquareMatrixType(symm), SquareMatrixType(symm).bunchKaufman()); verify_factorization(SquareMatrixType(symm), symm.template selfadjointView().bunchKaufman()); verify_factorization(SquareMatrixType(symm), symm.template selfadjointView().bunchKaufman()); // rcond() within a factor of 10 of the true reciprocal 1-norm condition number. if (rows > 0) { const SquareMatrixType inv = bk_lo.solve(SquareMatrixType::Identity(rows, rows)); using RealScalar = typename MatrixType::RealScalar; RealScalar rcond = (RealScalar(1) / matrix_l1_norm(symmLo)) / matrix_l1_norm(inv); RealScalar rcond_est = bk_lo.rcond(); VERIFY(rcond_est >= rcond / 10 && rcond_est <= rcond * 10); } } // Build a Hermitian matrix with a prescribed (real) eigenvalue spectrum and check inertia + stability. template void bunchkaufman_inertia_and_conditioning(Index n) { typedef typename MatrixType::Scalar Scalar; typedef typename MatrixType::RealScalar RealScalar; typedef Matrix RealVectorType; // Random orthonormal/unitary U via QR. MatrixType R = MatrixType::Random(n, n); HouseholderQR qr(R); MatrixType U = qr.householderQ(); // Eigenvalues spanning a wide magnitude range, with mixed signs (indefinite). const RealScalar s = (std::min)(RealScalar(6), RealScalar(std::numeric_limits::max_exponent10) / 8); RealVectorType d(n); Index expect_pos = 0, expect_neg = 0; for (Index k = 0; k < n; ++k) { RealScalar mag = pow(RealScalar(10), internal::random(-s, s)); RealScalar sign = internal::random() ? RealScalar(1) : RealScalar(-1); d(k) = sign * mag; if (d(k) > 0) ++expect_pos; else ++expect_neg; } MatrixType A = U * d.asDiagonal() * U.adjoint(); // Force exact Hermitian symmetry (kill round-off asymmetry). A = (A + A.adjoint()).eval() * Scalar(RealScalar(0.5)); BunchKaufman bk(A); VERIFY(bk.info() == Success); VERIFY_IS_APPROX(A, bk.reconstructedMatrix()); // isPositive()/isNegative() agree with the true inertia. VERIFY(bk.isPositive() == (expect_neg == 0)); VERIFY(bk.isNegative() == (expect_pos == 0)); // Backward-stable solve: relative residual is small even for ill-conditioned A. Matrix b = Matrix::Random(n); Matrix x = bk.solve(b); RealScalar res = (A * x - b).norm() / b.norm(); RealScalar tol = sqrt(test_precision()); VERIFY(res <= tol); } // Definiteness / 2x2-pivot regression cases. template void bunchkaufman_small_cases() { typedef Matrix Mat2; typedef Matrix Vec2; // Indefinite with zero diagonal -> requires a 2x2 pivot. { Mat2 A; A << Scalar(0), Scalar(1), Scalar(1), Scalar(0); BunchKaufman bk(A); VERIFY(bk.info() == Success); VERIFY_IS_APPROX(A, bk.reconstructedMatrix()); VERIFY(!bk.isPositive()); VERIFY(!bk.isNegative()); Vec2 b(Scalar(3), Scalar(5)); Vec2 x = bk.solve(b); VERIFY_IS_APPROX(A * x, b); } // Diagonal indefinite [[1,0],[0,-1]]. { Mat2 A; A << Scalar(1), Scalar(0), Scalar(0), Scalar(-1); BunchKaufman bk(A); VERIFY(bk.info() == Success); VERIFY_IS_APPROX(A, bk.reconstructedMatrix()); VERIFY(!bk.isPositive()); VERIFY(!bk.isNegative()); } // 1x1. { Matrix A; A << Scalar(-3); BunchKaufman > bk(A); VERIFY(bk.info() == Success); VERIFY_IS_APPROX(A, bk.reconstructedMatrix()); VERIFY(!bk.isPositive()); VERIFY(bk.isNegative()); } } // Exactly-singular matrix: factorization succeeds structurally but reports NumericalIssue, // while still reconstructing the input exactly. Exercised at a small size (unblocked kernel) and a // size larger than the panel width (blocked kernel, where the zero pivot lands inside a panel). template void bunchkaufman_singular(Index n) { typedef Matrix MatrixType; // A zero column/row makes the matrix exactly singular with an exact zero pivot. Place it in the // interior so that, for n > blocksize, it falls inside a panel of the blocked algorithm. const Index z = n / 2; MatrixType M = MatrixType::Random(n, n); MatrixType A = M + M.adjoint(); A.col(z).setZero(); A.row(z).setZero(); BunchKaufman lo(A); VERIFY(lo.info() == NumericalIssue); VERIFY_IS_APPROX(A, lo.reconstructedMatrix()); // matrixL() must be a well-formed unit lower triangular factor even on the singular column. VERIFY((lo.matrixL().toDenseMatrix().diagonal().array() == Scalar(1)).all()); BunchKaufman up(A); VERIFY(up.info() == NumericalIssue); VERIFY_IS_APPROX(A, up.reconstructedMatrix()); } // A matrix containing a NaN must be reported as a numerical failure (matching LAPACK's DISNAN guard), // not silently accepted. template void bunchkaufman_nan() { typedef Matrix MatrixType; for (Index n : {5, 100}) { MatrixType M = MatrixType::Random(n, n); MatrixType A = M + M.adjoint(); A(n / 2, n / 2) = std::numeric_limits::Real>::quiet_NaN(); BunchKaufman bk(A); VERIFY(bk.info() == NumericalIssue); } } // Rank-deficient PSD: A = a a^* with a of rank r < n. Reconstruct must match. template void bunchkaufman_rank_deficient() { typedef Matrix MatrixType; const Index n = 16; const Index r = internal::random(1, n - 1); MatrixType a = MatrixType::Random(n, r); MatrixType A = a * a.adjoint(); BunchKaufman bk(A); VERIFY_IS_APPROX(A, bk.reconstructedMatrix()); VERIFY(!bk.isNegative()); // PSD -> no negative eigenvalues } // Blocking and 2x2-panel-boundary stress across sizes that straddle the panel width. template void bunchkaufman_blocking_boundary() { typedef Matrix MatrixType; typedef typename NumTraits::Real RealScalar; const Index PS = internal::packet_traits::size; const Index sizes[] = {1, 2, 3, PS - 1, PS, PS + 1, 2 * PS, 31, 32, 33, 63, 64, 65, 96, 127, 128, 129, 192, 2 * 64 + 3, 200}; for (Index n : sizes) { if (n <= 0) continue; MatrixType M = MatrixType::Random(n, n); MatrixType A = M + M.adjoint(); // Force several 2x2 pivots by shrinking the diagonal. A.diagonal() *= Scalar(RealScalar(1e-2)); BunchKaufman lo(A); VERIFY(lo.info() == Success); VERIFY_IS_APPROX(A, lo.reconstructedMatrix()); BunchKaufman up(A); VERIFY(up.info() == Success); VERIFY_IS_APPROX(A, up.reconstructedMatrix()); // Lower and Upper must yield the same (Hermitian) decomposition of A. VERIFY_IS_APPROX(lo.reconstructedMatrix(), up.reconstructedMatrix()); Matrix b = Matrix::Random(n); Matrix x = lo.solve(b); const Matrix residual = A * x - b; // The diagonal shrink above intentionally makes the pivot-boundary cases harder than the generic solve tests. VERIFY(residual.norm() <= RealScalar(10) * NumTraits::epsilon() * (A.norm() * x.norm() + b.norm())); } } template void bunchkaufman_verify_assert() { MatrixType tmp; BunchKaufman bk; VERIFY_RAISES_ASSERT(bk.matrixL()) VERIFY_RAISES_ASSERT(bk.matrixU()) VERIFY_RAISES_ASSERT(bk.vectorD()) VERIFY_RAISES_ASSERT(bk.subDiagonal()) VERIFY_RAISES_ASSERT(bk.transpositionsP()) VERIFY_RAISES_ASSERT(bk.isPositive()) VERIFY_RAISES_ASSERT(bk.isNegative()) VERIFY_RAISES_ASSERT(bk.matrixLDLT()) VERIFY_RAISES_ASSERT(bk.reconstructedMatrix()) VERIFY_RAISES_ASSERT(bk.solve(tmp)) } // Build a random Hermitian (real symmetric) indefinite matrix of the same type/size as `m`. template MatrixType make_hermitian_indefinite(const MatrixType& m) { MatrixType a = MatrixType::Random(m.rows(), m.cols()); return MatrixType(a + a.adjoint()); } template void bunchkaufman(const MatrixType& m) { // General Hermitian indefinite. bunchkaufman_solve_and_reconstruct(make_hermitian_indefinite(m)); // Zero-diagonal Hermitian -> forces 2x2 pivots throughout (needs n >= 2 to stay non-singular). if (m.rows() >= 2) { MatrixType A = make_hermitian_indefinite(m); A.diagonal().setZero(); bunchkaufman_solve_and_reconstruct(A); } } // Extreme-scale 2x2 pivot: an off-diagonal-only Hermitian 2x2 forces a single 2x2 pivot whose // determinant is det = -|off|^2. The scaled (det-free) 2x2 formulas must stay finite and correct when // |off| is huge or tiny. Regression for forming det = d11*d22 - |d21|^2 directly, which overflows to // +-inf for off=1e200 (solve then returns 0, residual 1) and underflows to 0 for off=1e-200 (solve // returns NaN/inf), and which also misclassifies the inertia. Requires a type that can hold 1e+-200, // so this is exercised for double / complex only. template void bunchkaufman_extreme_scale() { typedef typename NumTraits::Real RealScalar; typedef Matrix Mat2; typedef Matrix Vec2; const RealScalar tol = sqrt(test_precision()); for (RealScalar mag : {pow(RealScalar(10), RealScalar(200)), pow(RealScalar(10), RealScalar(-200))}) { const Scalar off = Scalar(mag); Mat2 A; A << Scalar(0), numext::conj(off), off, Scalar(0); BunchKaufman bk(A); VERIFY(bk.info() == Success); VERIFY(!bk.isPositive()); // det < 0 => one positive and one negative eigenvalue VERIFY(!bk.isNegative()); // Use the max-abs (infinity) relative norm throughout: the Frobenius norm (.norm()) squares the // ~1e200 entries and would overflow/underflow even for a correct factorization. VERIFY((A - bk.reconstructedMatrix()).cwiseAbs().maxCoeff() <= tol * A.cwiseAbs().maxCoeff()); // A x = b with b = [1,1]; the product A*x stays O(1), so its residual is safe to measure. const Vec2 b(Scalar(1), Scalar(1)); const Vec2 x = bk.solve(b); VERIFY((A * x - b).cwiseAbs().maxCoeff() <= tol); } } // Extreme-scale factorization at the matrix level: a zero-diagonal Hermitian matrix (2x2 pivots // throughout, exercising both the unblocked and -- for n > blocksize -- the blocked trailing update) // scaled to an extreme magnitude. The factorization is scale-equivariant and must remain overflow-free; // the solve must stay backward stable. double / complex only (1e+-175 overflows float). template void bunchkaufman_extreme_scale_large(Index n) { typedef typename NumTraits::Real RealScalar; typedef Matrix MatrixType; typedef Matrix VectorType; const RealScalar tol = sqrt(test_precision()); MatrixType M = MatrixType::Random(n, n); MatrixType A = M + M.adjoint(); A.diagonal().setZero(); // 1e+-175 is past the squaring threshold (|entry|^2 over/underflows double), so the pre-fix code // (which forms det = d11*d22 - |d21|^2) produces NaN/Inf here, while the scaled formulas stay exact. for (RealScalar sigma : {pow(RealScalar(10), RealScalar(175)), pow(RealScalar(10), RealScalar(-175))}) { const MatrixType As = A * Scalar(sigma); BunchKaufman bk(As); VERIFY(bk.info() == Success); // Max-abs relative reconstruction error (avoid .norm(), whose squaring overflows the ~1e175 entries). VERIFY((As - bk.reconstructedMatrix()).cwiseAbs().maxCoeff() <= tol * As.cwiseAbs().maxCoeff()); // As*x stays O(1) for a unit-scale rhs, so its residual norm is safe to form. const VectorType b = VectorType::Random(n); const VectorType x = bk.solve(b); VERIFY((As * x - b).norm() <= tol * b.norm()); } } // Regression: the size constructor must pre-allocate the panel workspace so that a subsequent compute() // on a problem of that size performs no heap allocation. n is chosen above the panel width so the // blocked path (the one that uses the workspace) runs. (Uses the default stack-allocation limit so the // trailing-update GEMM's small blocking buffers stay on the stack rather than the heap.) template void bunchkaufman_no_malloc() { typedef Matrix MatrixType; const Index n = internal::bunch_kaufman_blocksize() + 36; const MatrixType M = MatrixType::Random(n, n); const MatrixType A = M + M.adjoint(); BunchKaufman bk(n); // pre-allocates m_matrix, m_transpositions, m_subdiag, m_workspace internal::set_is_malloc_allowed(false); bk.compute(A); internal::set_is_malloc_allowed(true); VERIFY(bk.info() == Success); } EIGEN_DECLARE_TEST(bunchkaufman) { for (int i = 0; i < g_repeat; i++) { CALL_SUBTEST_1(bunchkaufman(Matrix())); CALL_SUBTEST_2(bunchkaufman(Matrix2d())); CALL_SUBTEST_3(bunchkaufman(Matrix3f())); CALL_SUBTEST_4(bunchkaufman(Matrix4d())); int s = internal::random(1, EIGEN_TEST_MAX_SIZE); CALL_SUBTEST_5(bunchkaufman(MatrixXd(s, s))); TEST_SET_BUT_UNUSED_VARIABLE(s); s = internal::random(1, EIGEN_TEST_MAX_SIZE / 2); CALL_SUBTEST_6(bunchkaufman(MatrixXcd(s, s))); TEST_SET_BUT_UNUSED_VARIABLE(s); s = internal::random(2, EIGEN_TEST_MAX_SIZE); CALL_SUBTEST_5(bunchkaufman_inertia_and_conditioning(s)); s = internal::random(2, EIGEN_TEST_MAX_SIZE / 2); CALL_SUBTEST_6(bunchkaufman_inertia_and_conditioning(s)); CALL_SUBTEST_7(bunchkaufman_small_cases()); CALL_SUBTEST_7(bunchkaufman_small_cases >()); // Singular matrices in both the unblocked (n=8) and blocked (n=100 > blocksize) regimes. CALL_SUBTEST_5(bunchkaufman_singular(8)); CALL_SUBTEST_5(bunchkaufman_singular(100)); CALL_SUBTEST_6(bunchkaufman_singular >(8)); CALL_SUBTEST_6(bunchkaufman_singular >(100)); CALL_SUBTEST_5(bunchkaufman_nan()); CALL_SUBTEST_6(bunchkaufman_nan >()); CALL_SUBTEST_5(bunchkaufman_rank_deficient()); CALL_SUBTEST_6(bunchkaufman_rank_deficient >()); } // Empty-matrix edge case. CALL_SUBTEST_5(bunchkaufman(MatrixXd(0, 0))); // Problem-size constructors. CALL_SUBTEST_8(BunchKaufman(10)); CALL_SUBTEST_8(BunchKaufman(10)); // Assertion checks on an uninitialized decomposition. CALL_SUBTEST_3(bunchkaufman_verify_assert()); CALL_SUBTEST_5(bunchkaufman_verify_assert()); CALL_SUBTEST_6(bunchkaufman_verify_assert()); // Deterministic blocking / panel-boundary tests (outside g_repeat). CALL_SUBTEST_8(bunchkaufman_blocking_boundary()); CALL_SUBTEST_8(bunchkaufman_blocking_boundary()); CALL_SUBTEST_8(bunchkaufman_blocking_boundary >()); // Extreme-scale 2x2 pivots: the scaled (det-free) 2x2 formulas must not over/underflow. CALL_SUBTEST_7(bunchkaufman_extreme_scale()); CALL_SUBTEST_7(bunchkaufman_extreme_scale >()); CALL_SUBTEST_5(bunchkaufman_extreme_scale_large(8)); CALL_SUBTEST_5(bunchkaufman_extreme_scale_large(100)); CALL_SUBTEST_6(bunchkaufman_extreme_scale_large >(8)); CALL_SUBTEST_6(bunchkaufman_extreme_scale_large >(100)); // No-malloc regression: the size constructor pre-allocates the panel workspace. CALL_SUBTEST_8(bunchkaufman_no_malloc()); CALL_SUBTEST_8(bunchkaufman_no_malloc >()); }