feat(linear-static-3d-euler-beam): step 20 - mkl-pardiso-solver-review-fix

This commit is contained in:
KOKO\Mimi
2026-08-09 20:20:37 +09:00
parent 4678472326
commit be5f4eb86d
4 changed files with 139 additions and 25 deletions
@@ -55,14 +55,45 @@ TEST(MklPardisoSolver, RejectsInvalidCsrStateAndDimensions) {
static_assert(std::has_virtual_destructor_v<fesa::LinearSolver>);
fesa::MklPardisoSolver solver;
fesa::Vector solution{2U};
expectSolverFailure(solver.solve(fesa::Vector{2U, 1.0}, solution));
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(
solver.solve(fesa::Vector{2U, 1.0}, solution)
.diagnostics()
.front()
.code,
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});
@@ -72,11 +103,6 @@ TEST(MklPardisoSolver, RejectsInvalidCsrStateAndDimensions) {
rectangularStatus.diagnostics().front().code,
"solver-matrix-not-square");
const auto empty = makeDenseCsr(0U, 0U, {});
const auto emptyStatus = solver.factorize(empty);
expectSolverFailure(emptyStatus);
EXPECT_EQ(emptyStatus.diagnostics().front().code, "solver-empty-matrix");
fesa::SparsePattern invalidPattern{{0U, 2U}, {0U}};
auto invalidCsr = fesa::SparseMatrix::fromCoo(
1U,
@@ -92,6 +118,18 @@ TEST(MklPardisoSolver, RejectsInvalidCsrStateAndDimensions) {
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,
@@ -105,9 +143,4 @@ TEST(MklPardisoSolver, RejectsInvalidCsrStateAndDimensions) {
noDiagonalStatus.diagnostics().front().code,
"solver-missing-diagonal");
const auto spd = makeDenseCsr(2U, 2U, {4.0, 1.0, 1.0, 3.0});
ASSERT_TRUE(solver.factorize(spd).isOk());
expectSolverFailure(solver.solve(fesa::Vector{1U, 1.0}, solution));
fesa::Vector wrongSolution{1U};
expectSolverFailure(solver.solve(fesa::Vector{2U, 1.0}, wrongSolution));
}
@@ -5,7 +5,6 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <initializer_list>
#include <limits>
@@ -58,7 +57,16 @@ double normalizedResidual(
const fesa::Vector& rhs) {
auto residual = matrix.multiply(solution);
residual.axpy(-1.0, rhs);
return residual.norm() / (std::max)(1.0, rhs.norm());
const double numerator = residual.norm();
const double denominator = rhs.norm();
if (!std::isfinite(numerator) || !std::isfinite(denominator)) {
return (std::numeric_limits<double>::infinity)();
}
if (denominator == 0.0) {
return numerator == 0.0 ? 0.0 :
(std::numeric_limits<double>::infinity)();
}
return numerator / denominator;
}
double relativeError(
@@ -66,7 +74,16 @@ double relativeError(
const fesa::Vector& expected) {
auto difference = actual;
difference.axpy(-1.0, expected);
return difference.norm() / (std::max)(1.0, expected.norm());
const double numerator = difference.norm();
const double denominator = expected.norm();
if (!std::isfinite(numerator) || !std::isfinite(denominator)) {
return (std::numeric_limits<double>::infinity)();
}
if (denominator == 0.0) {
return numerator == 0.0 ? 0.0 :
(std::numeric_limits<double>::infinity)();
}
return numerator / denominator;
}
void expectStructuredSolverFailure(const fesa::Status& status) {
@@ -174,9 +191,13 @@ TEST(MklPardisoSolver, ClassifiesSingularIndefiniteAndNonfiniteFailures) {
auto rhs = makeVector({1.0, 2.0});
rhs[1U] = (std::numeric_limits<double>::infinity)();
fesa::Vector solution{2U};
solution[0U] = 23.0;
solution[1U] = -9.0;
const auto rhsStatus = solver.solve(rhs, solution);
expectStructuredSolverFailure(rhsStatus);
EXPECT_EQ(rhsStatus.diagnostics()[0U].code, "nonfinite-solver-rhs");
EXPECT_DOUBLE_EQ(solution[0U], 23.0);
EXPECT_DOUBLE_EQ(solution[1U], -9.0);
fesa::SparsePattern pattern{{0U, 1U}, {0U}};
auto nonfiniteMatrix = fesa::SparseMatrix::fromCoo(