147 lines
5.3 KiB
C++
147 lines
5.3 KiB
C++
#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 <gtest/gtest.h>
|
|
|
|
#include <type_traits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
fesa::SparseMatrix makeDenseCsr(
|
|
const std::size_t rows,
|
|
const std::size_t columns,
|
|
const std::vector<double>& values) {
|
|
EXPECT_EQ(values.size(), rows * columns);
|
|
|
|
fesa::SparsePattern pattern;
|
|
std::vector<fesa::CooContribution> 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<fesa::LinearSolver, fesa::MklPardisoSolver>);
|
|
static_assert(std::has_virtual_destructor_v<fesa::LinearSolver>);
|
|
|
|
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");
|
|
|
|
}
|