blob: b9c16796a930ff5867afd94578d3cdfd6c0386a3 [file]
// This file is part of Eigen, a lightweight C++ template library
// for linear algebra.
//
// Copyright (C) 2025 Johannes Zipfel <johzip1010@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/.
#include "sparse_solver.h"
#include <Eigen/IterativeLinearSolvers>
template <typename T, typename I_>
void test_incompleteLUT_T() {
IncompleteLUT<T, I_> ilut;
ilut.setDroptol(NumTraits<T>::epsilon() * 4);
}
template <typename T>
void test_extract_LU() {
typedef Eigen::SparseMatrix<T> SparseMatrix;
SparseMatrix A(5, 5);
std::vector<Eigen::Triplet<T>> triplets;
triplets.push_back({0, 0, 4});
triplets.push_back({0, 1, -1});
triplets.push_back({0, 4, -1});
triplets.push_back({1, 0, -1});
triplets.push_back({1, 1, 4});
triplets.push_back({1, 2, -1});
triplets.push_back({2, 1, -1});
triplets.push_back({2, 2, 4});
triplets.push_back({2, 3, -1});
triplets.push_back({3, 2, -1});
triplets.push_back({3, 3, 4});
triplets.push_back({3, 4, -1});
triplets.push_back({4, 0, -1});
triplets.push_back({4, 3, -1});
triplets.push_back({4, 4, 4});
A.setFromTriplets(triplets.begin(), triplets.end());
IncompleteLUT<T> ilut;
ilut.compute(A);
Eigen::SparseMatrix<T> matL = ilut.matrixL(); // Extract L
Eigen::SparseMatrix<T> matU = ilut.matrixU(); // Extract U
Eigen::SparseMatrix<T> expectedMatL(5, 5);
std::vector<Eigen::Triplet<T>> tripletsExL;
tripletsExL.emplace_back(0, 0, 1);
tripletsExL.emplace_back(1, 0, -0.25);
tripletsExL.emplace_back(1, 1, 1);
tripletsExL.emplace_back(2, 0, -0.25);
tripletsExL.emplace_back(2, 1, -0.0666667);
tripletsExL.emplace_back(2, 2, 1);
tripletsExL.emplace_back(3, 2, -0.25);
tripletsExL.emplace_back(3, 3, 1);
tripletsExL.emplace_back(4, 1, -0.266667);
tripletsExL.emplace_back(4, 3, -0.266667);
tripletsExL.emplace_back(4, 4, 1);
expectedMatL.setFromTriplets(tripletsExL.begin(), tripletsExL.end());
Eigen::SparseMatrix<T> expectedMatU(5, 5);
std::vector<Eigen::Triplet<T>> tripletsExU;
tripletsExU.emplace_back(0, 0, 4);
tripletsExU.emplace_back(0, 1, -1);
tripletsExU.emplace_back(1, 1, 3.75);
tripletsExU.emplace_back(1, 4, -1);
tripletsExU.emplace_back(2, 2, 4);
tripletsExU.emplace_back(2, 3, -1);
tripletsExU.emplace_back(3, 3, 3.75);
tripletsExU.emplace_back(3, 4, -1);
tripletsExU.emplace_back(4, 4, 3.46667);
expectedMatU.setFromTriplets(tripletsExU.begin(), tripletsExU.end());
VERIFY_IS_APPROX(expectedMatL, matL);
VERIFY_IS_APPROX(expectedMatU, matU);
}
// https://gitlab.com/libeigen/eigen/-/issues/2626
// IncompleteLUT must apply a static row permutation so that matrices whose
// natural diagonal has zeros (but admit a full bipartite matching on the
// sparsity pattern) can still be factorized. Without it, ILUT silently
// produces a useless preconditioner.
template <typename T>
void test_zero_diagonal_2626() {
typedef Eigen::SparseMatrix<T> SparseMatrixT;
typedef Eigen::Matrix<T, Eigen::Dynamic, 1> Vector;
const int n = 50;
// 1D Laplacian: SPD, fully nonzero diagonal.
std::vector<Eigen::Triplet<T>> triplets;
for (int i = 0; i < n; ++i) {
triplets.emplace_back(i, i, T(2));
if (i > 0) triplets.emplace_back(i, i - 1, T(-1));
if (i + 1 < n) triplets.emplace_back(i, i + 1, T(-1));
}
SparseMatrixT base(n, n);
base.setFromTriplets(triplets.begin(), triplets.end());
// Cyclic-shift row permutation by 2: separates the natural diagonal from
// the tridiagonal nonzero band, so all diagonal entries of (Pshift * base)
// are zero. A valid row matching must find the inverse shift.
Eigen::PermutationMatrix<Eigen::Dynamic> Pshift(n);
for (int i = 0; i < n; ++i) Pshift.indices()(i) = (i + 2) % n;
SparseMatrixT A = Pshift * base;
// Verify the test setup: every diagonal is zero.
int nz_diag = 0;
for (int i = 0; i < n; ++i)
if (A.coeff(i, i) != T(0)) ++nz_diag;
VERIFY(nz_diag == 0);
// BiCGSTAB + IncompleteLUT must converge to a reasonable residual.
Vector b = Vector::Random(n);
Eigen::BiCGSTAB<SparseMatrixT, Eigen::IncompleteLUT<T>> solver;
solver.setTolerance(typename Eigen::NumTraits<T>::Real(16) * Eigen::NumTraits<T>::epsilon());
solver.compute(A);
VERIFY(solver.preconditioner().info() == Eigen::Success);
Vector x = solver.solve(b);
VERIFY(solver.info() == Eigen::Success);
Vector residual = b - A * x;
// Solver was set to tol = 16*eps; allow some slack for the residual check.
typename Eigen::NumTraits<T>::Real residual_bound =
typename Eigen::NumTraits<T>::Real(1024) * Eigen::NumTraits<T>::epsilon() * b.norm();
VERIFY(residual.norm() < residual_bound);
}
// A structurally singular matrix (empty row) cannot be made diagonal-nonzero
// by any row permutation; the factorization must report NumericalIssue.
// This covers the rownorm == 0 early-return path.
template <typename T>
void test_structurally_singular() {
typedef Eigen::SparseMatrix<T> SparseMatrixT;
std::vector<Eigen::Triplet<T>> triplets;
triplets.emplace_back(0, 0, T(2));
triplets.emplace_back(0, 1, T(1));
triplets.emplace_back(1, 0, T(1));
triplets.emplace_back(1, 1, T(2));
// row 2 is intentionally empty
triplets.emplace_back(3, 0, T(1));
triplets.emplace_back(3, 3, T(2));
SparseMatrixT A(4, 4);
A.setFromTriplets(triplets.begin(), triplets.end());
Eigen::IncompleteLUT<T> ilut;
ilut.compute(A);
VERIFY(ilut.info() == Eigen::NumericalIssue);
}
// A matrix where every row is structurally non-empty (so rownorm != 0) but
// no full bipartite matching exists, forcing at least one pivot to be shifted.
// Rows 0 and 1 only have entries in column 0, so columns 1 and 2 cannot both
// be matched. This exercises the shifted-pivot path that flips info() to
// NumericalIssue.
template <typename T>
void test_zero_pivot_numerical_issue() {
typedef Eigen::SparseMatrix<T> SparseMatrixT;
SparseMatrixT A(3, 3);
std::vector<Eigen::Triplet<T>> triplets;
triplets.emplace_back(0, 0, T(1));
triplets.emplace_back(1, 0, T(2)); // row 1 competes with row 0 for column 0
triplets.emplace_back(2, 2, T(3));
A.setFromTriplets(triplets.begin(), triplets.end());
Eigen::IncompleteLUT<T> ilut;
ilut.compute(A);
VERIFY(ilut.info() == Eigen::NumericalIssue);
}
// analyzePattern() must depend only on the stored sparsity pattern and not on
// numerical values. Otherwise, the two-step API contract breaks: the same
// analysis would no longer be reusable for any matrix sharing this stored
// pattern. Repro: analyze a pattern with all-zero placeholder values, then
// factorize a numerically nonzero matrix sharing that pattern.
template <typename T>
void test_pattern_value_separation() {
typedef Eigen::SparseMatrix<T> SparseMatrixT;
SparseMatrixT pattern(2, 2);
pattern.insert(0, 1) = T(0);
pattern.insert(1, 0) = T(0);
pattern.makeCompressed();
SparseMatrixT A(2, 2);
A.insert(0, 1) = T(1);
A.insert(1, 0) = T(1);
A.makeCompressed();
Eigen::IncompleteLUT<T> ilut;
ilut.analyzePattern(pattern);
ilut.factorize(A);
VERIFY(ilut.info() == Eigen::Success);
}
EIGEN_DECLARE_TEST(incomplete_LUT) {
CALL_SUBTEST_1((test_incompleteLUT_T<double, int>()));
CALL_SUBTEST_1((test_incompleteLUT_T<float, int>()));
CALL_SUBTEST_2((test_incompleteLUT_T<std::complex<double>, int>()));
CALL_SUBTEST_3((test_incompleteLUT_T<double, long int>()));
CALL_SUBTEST_4(test_extract_LU<double>());
CALL_SUBTEST_4(test_extract_LU<float>());
CALL_SUBTEST_5(test_zero_diagonal_2626<double>());
CALL_SUBTEST_5(test_zero_diagonal_2626<float>());
CALL_SUBTEST_5(test_structurally_singular<double>());
CALL_SUBTEST_5(test_zero_pivot_numerical_issue<double>());
CALL_SUBTEST_5(test_pattern_value_separation<double>());
}