fix(solver+io): B1 block-FD Hessian, V3 NaN/Inf guard, C1 ok-flag (audit quick-wins)

B1 (api-performance): wire hyper_ideal_hessian_block_fd_sym into newton_hyper_ideal
instead of the full-FD path — 33×/1166× faster on cathead/brezel, also fixes the
FD-step vs Newton-tol accuracy floor (N1 cross-fix).

V3 (input-validation): add isfinite check on all vertex coordinates in load_mesh;
NaN/Inf now throws runtime_error at the I/O boundary before poisoning the solver.

C1 (test-coverage): expose the internal ok flag via an optional bool* parameter
on solve_linear_system so double-solver failure is no longer invisible to callers.

+5 new tests (LoadMeshThrowsOnNaN/Inf, OkFlag_True*, OkFlag_NullPointerIsSafe).
282/282 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-31 14:03:45 +02:00
parent 505dd4b0a5
commit 101f3138ce
4 changed files with 134 additions and 5 deletions

View File

@@ -501,3 +501,81 @@ TEST(SparseQRFallback, Euclidean_ClosedMeshNoPinConverges)
"grad_inf_norm = " << res.grad_inf_norm;
EXPECT_LT(res.grad_inf_norm, 1e-8);
}
// ════════════════════════════════════════════════════════════════════════════
// C1: solve_linear_system exposes the ok flag via the new out-parameter
//
// The C1 audit finding was that solve_linear_system silently dropped the
// internal ok flag, making double-solver failure invisible to callers.
// These tests verify that the new optional bool* ok parameter is correctly
// wired: ok=true for successful solves, and the parameter is optional (null
// is safe).
//
// Note: constructing a matrix where Eigen's SparseQR hard-fails is not
// practical — SparseQR reports Eigen::Success even for rank-0 inputs,
// returning a zero min-norm solution. The observable contract for callers is
// therefore: ok=true whenever a solver reports success; ok=false only when
// both report failure (an edge case Eigen essentially never produces). The
// fix in C1 ensures that if that edge case ever fires it propagates to the
// caller — previously it was silently swallowed.
// ════════════════════════════════════════════════════════════════════════════
TEST(SparseQRFallback, OkFlag_TrueOnFullRankSystem)
{
// 3×3 diagonal PD matrix: LDLT succeeds → ok must be true.
Eigen::SparseMatrix<double> A(3, 3);
A.insert(0, 0) = 1.0;
A.insert(1, 1) = 2.0;
A.insert(2, 2) = 3.0;
A.makeCompressed();
Eigen::VectorXd rhs(3);
rhs << 1.0, 4.0, 9.0;
bool fallback = true;
bool ok = false;
Eigen::VectorXd x = conformallab::solve_linear_system(A, rhs, &fallback, &ok);
EXPECT_TRUE(ok) << "Full-rank system: ok must be true";
EXPECT_FALSE(fallback) << "Full-rank system: LDLT path, no fallback expected";
EXPECT_NEAR(x[0], 1.0, 1e-12);
EXPECT_NEAR(x[1], 2.0, 1e-12);
EXPECT_NEAR(x[2], 3.0, 1e-12);
}
TEST(SparseQRFallback, OkFlag_TrueOnSingularSystemViaFallback)
{
// Singular matrix (zero row/col 1) — LDLT fails, SparseQR succeeds → ok=true.
Eigen::SparseMatrix<double> A(3, 3);
A.insert(0, 0) = 2.0;
A.insert(2, 2) = 3.0;
A.makeCompressed();
Eigen::VectorXd rhs(3);
rhs << 2.0, 0.0, 3.0;
bool fallback = false;
bool ok = false;
Eigen::VectorXd x = conformallab::solve_linear_system(A, rhs, &fallback, &ok);
EXPECT_TRUE(ok) << "Singular-but-consistent system: SparseQR succeeds → ok must be true";
EXPECT_TRUE(fallback) << "Singular system: fallback must have been triggered";
}
TEST(SparseQRFallback, OkFlag_NullPointerIsSafe)
{
// Calling with ok=nullptr must not crash (backward-compatibility).
Eigen::SparseMatrix<double> A(2, 2);
A.insert(0, 0) = 1.0;
A.insert(1, 1) = 1.0;
A.makeCompressed();
Eigen::VectorXd rhs(2);
rhs << 3.0, 5.0;
EXPECT_NO_THROW({
Eigen::VectorXd x = conformallab::solve_linear_system(A, rhs, nullptr, nullptr);
EXPECT_NEAR(x[0], 3.0, 1e-12);
EXPECT_NEAR(x[1], 5.0, 1e-12);
});
}