#include "fesa/solvers/linear/linear_solver.hpp" #include "fesa/solvers/linear/mkl_pardiso_solver.hpp" #include "fesa/fem/dof_manager.hpp" #include "fesa/math/sparse_matrix.hpp" #include #include #include #include namespace { fesa::SparseMatrix makeDenseCsr( const std::size_t rows, const std::size_t columns, const std::vector& values) { EXPECT_EQ(values.size(), rows * columns); fesa::SparsePattern pattern; std::vector contributions; pattern.rowOffsets.reserve(rows + 1U); pattern.rowOffsets.push_back(0U); for (std::size_t row = 0U; row < rows; ++row) { for (std::size_t column = 0U; column < columns; ++column) { pattern.columnIndices.push_back(column); contributions.push_back({ row, column, values[row * columns + column], row, column}); } pattern.rowOffsets.push_back(pattern.columnIndices.size()); } auto matrix = fesa::SparseMatrix::fromCoo( rows, columns, std::move(contributions), pattern); EXPECT_TRUE(matrix.hasValue()); return std::move(matrix.value()); } void expectSolverFailure(const fesa::Status& status) { EXPECT_FALSE(status.isOk()); EXPECT_EQ(status.failureCategory(), fesa::FailureCategory::solver); ASSERT_FALSE(status.diagnostics().empty()); EXPECT_EQ(status.diagnostics().front().severity, fesa::Severity::error); } } // namespace TEST(MklPardisoSolver, RejectsInvalidCsrStateAndDimensions) { static_assert(std::is_base_of_v); static_assert(std::has_virtual_destructor_v); fesa::MklPardisoSolver solver; fesa::Vector untouched{2U}; untouched[0U] = 17.0; untouched[1U] = -4.0; const auto beforeFactorize = solver.solve(fesa::Vector{2U, 1.0}, untouched); expectSolverFailure(beforeFactorize); EXPECT_EQ( beforeFactorize.diagnostics().front().code, "solver-not-factorized"); EXPECT_DOUBLE_EQ(untouched[0U], 17.0); EXPECT_DOUBLE_EQ(untouched[1U], -4.0); // A fully constrained model has a valid 0x0 Kff. It still observes the // factorize-then-solve lifecycle without invoking a numerical backend. const auto empty = makeDenseCsr(0U, 0U, {}); ASSERT_TRUE(solver.factorize(empty).isOk()); fesa::Vector emptySolution{0U}; EXPECT_TRUE(solver.solve(fesa::Vector{0U}, emptySolution).isOk()); EXPECT_EQ(emptySolution.size(), 0U); // Refactorization from the trivial state must establish ordinary PARDISO // state rather than retaining a zero-equation shortcut. const auto spd = makeDenseCsr(2U, 2U, {4.0, 1.0, 1.0, 3.0}); ASSERT_TRUE(solver.factorize(spd).isOk()); fesa::Vector solution{2U}; ASSERT_TRUE(solver.solve(fesa::Vector{2U, 1.0}, solution).isOk()); EXPECT_NEAR(solution[0U], 2.0 / 11.0, 1.0e-14); EXPECT_NEAR(solution[1U], 3.0 / 11.0, 1.0e-14); const double solvedFirst = solution[0U]; const double solvedSecond = solution[1U]; expectSolverFailure(solver.solve(fesa::Vector{1U, 1.0}, solution)); EXPECT_DOUBLE_EQ(solution[0U], solvedFirst); EXPECT_DOUBLE_EQ(solution[1U], solvedSecond); fesa::Vector wrongSolution{1U}; wrongSolution[0U] = 41.0; expectSolverFailure(solver.solve(fesa::Vector{2U, 1.0}, wrongSolution)); EXPECT_DOUBLE_EQ(wrongSolution[0U], 41.0); const auto rectangular = makeDenseCsr( 2U, 3U, {2.0, 0.0, 0.0, 0.0, 3.0, 0.0}); const auto rectangularStatus = solver.factorize(rectangular); expectSolverFailure(rectangularStatus); EXPECT_EQ( rectangularStatus.diagnostics().front().code, "solver-matrix-not-square"); fesa::SparsePattern invalidPattern{{0U, 2U}, {0U}}; auto invalidCsr = fesa::SparseMatrix::fromCoo( 1U, 1U, {{0U, 0U, 1.0, 0U, 0U}}, invalidPattern); EXPECT_FALSE(invalidCsr.hasValue()); const auto nonsymmetric = makeDenseCsr(2U, 2U, {2.0, 1.0, 0.0, 3.0}); const auto nonsymmetricStatus = solver.factorize(nonsymmetric); expectSolverFailure(nonsymmetricStatus); EXPECT_EQ( nonsymmetricStatus.diagnostics().front().code, "solver-matrix-not-symmetric"); const auto scaledNonsymmetric = makeDenseCsr( 2U, 2U, {2.0e-20, 1.0e-20, 1.1e-20, 3.0e-20}); const auto scaledNonsymmetricStatus = solver.factorize(scaledNonsymmetric); // Stop this case before inspecting diagnostics when the production code // incorrectly accepts the matrix; this keeps the RED failure deterministic. ASSERT_FALSE(scaledNonsymmetricStatus.isOk()); expectSolverFailure(scaledNonsymmetricStatus); EXPECT_EQ( scaledNonsymmetricStatus.diagnostics().front().code, "solver-matrix-not-symmetric"); fesa::SparsePattern noDiagonalPattern{{0U, 1U, 2U}, {1U, 0U}}; auto noDiagonal = fesa::SparseMatrix::fromCoo( 2U, 2U, {{0U, 1U, 1.0, 0U, 0U}, {1U, 0U, 1.0, 1U, 0U}}, noDiagonalPattern); ASSERT_TRUE(noDiagonal.hasValue()); const auto noDiagonalStatus = solver.factorize(noDiagonal.value()); expectSolverFailure(noDiagonalStatus); EXPECT_EQ( noDiagonalStatus.diagnostics().front().code, "solver-missing-diagonal"); }