refactor(solver): H2 unify 5 Newton loops into newton_core (+B2/B3/B4/B5)
The five DCE solvers (Euclidean, Spherical, HyperIdeal, CP-Euclidean, Inversive-Distance) carried near-identical Newton loops differing only in the gradient function, the Hessian function, and the spherical concave sign (test-coverage audit H2 — "5 near-identical Newton loops"). Extract one detail::newton_core<GradFn, HessFn>(x, grad, hess, concave, tol, max_iter); each public solver is now a thin wrapper passing two lambdas. This lets the performance fixes land once instead of five times: - B2 (api-perf): line_search now optionally returns the gradient at the accepted point; newton_core reuses it as the next iteration's convergence gradient, removing ~1 redundant full-mesh gradient evaluation per iteration. - B4 (api-perf): NewtonLinearSolver keeps one persistent SimplicialLDLT and runs analyzePattern once (symbolic factorization cached across iterations), re-analyzing only when nnz changes (self-healing for the FD Hessians that prune near-zero triplets). SparseQR fallback preserved verbatim. - B3 (api-perf): the Euclidean has_edge_dof scan is hoisted out of the loop (it is layout-invariant) — now done once before newton_core. - B5 (api-perf): the dead SimplicialLDLT variable in newton_euclidean is gone. Behaviour is unchanged: concave spherical still factors −H and solves (−H)·Δx = G; the merit steepest-descent still uses the true H; HardJava clamp default preserved. Tests (+2): NewtonCore.ReusesLineSearchGradient_B2_Convex asserts exactly 2 gradient evals on a unit-Hessian quadratic (vs 3 pre-B2), and ConcavePathConverges covers the −H branch directly. 298/298 CGAL tests pass, including all Java golden-vector parity tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -579,3 +579,78 @@ TEST(SparseQRFallback, OkFlag_NullPointerIsSafe)
|
||||
EXPECT_NEAR(x[1], 5.0, 1e-12);
|
||||
});
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// H2 / B2 — newton_core reuses the line-search gradient
|
||||
//
|
||||
// All five solvers now share detail::newton_core. B2: the gradient computed at
|
||||
// the accepted line-search point is carried into the next iteration's
|
||||
// convergence check instead of being recomputed at the loop top. On a
|
||||
// unit-Hessian quadratic, exact Newton reaches the minimum in one step, so the
|
||||
// total gradient-eval count is exactly 2 (1 initial + 1 accepted trial); the
|
||||
// pre-B2 loop would have recomputed G at the top of iteration 1 → 3.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
TEST(NewtonCore, ReusesLineSearchGradient_B2_Convex)
|
||||
{
|
||||
const std::vector<double> xstar = {1.0, -2.0, 3.5};
|
||||
int grad_calls = 0;
|
||||
|
||||
auto grad = [&](const std::vector<double>& x) {
|
||||
++grad_calls;
|
||||
std::vector<double> g(x.size());
|
||||
for (std::size_t i = 0; i < x.size(); ++i) g[i] = x[i] - xstar[i];
|
||||
return g; // G(x) = x − x*, H = +I
|
||||
};
|
||||
auto hess = [&](const std::vector<double>&) {
|
||||
Eigen::SparseMatrix<double> H(3, 3);
|
||||
for (int i = 0; i < 3; ++i) H.insert(i, i) = 1.0;
|
||||
H.makeCompressed();
|
||||
return H;
|
||||
};
|
||||
|
||||
auto res = conformallab::detail::newton_core(
|
||||
std::vector<double>{0.0, 0.0, 0.0}, grad, hess,
|
||||
/*concave=*/false, /*tol=*/1e-9, /*max_iter=*/50);
|
||||
|
||||
ASSERT_TRUE(res.converged);
|
||||
EXPECT_EQ(res.iterations, 1);
|
||||
EXPECT_NEAR(res.x[0], 1.0, 1e-9);
|
||||
EXPECT_NEAR(res.x[1], -2.0, 1e-9);
|
||||
EXPECT_NEAR(res.x[2], 3.5, 1e-9);
|
||||
EXPECT_EQ(grad_calls, 2)
|
||||
<< "B2: the loop-top gradient must be reused from the line search";
|
||||
}
|
||||
|
||||
// Concave path (spherical-style): H is NSD, newton_core factors −H and solves
|
||||
// (−H)·Δx = G. G(x) = x* − x with H = −I makes x* a maximiser; the core must
|
||||
// still converge in one step.
|
||||
TEST(NewtonCore, ConcavePathConverges)
|
||||
{
|
||||
const std::vector<double> xstar = {0.5, -1.5, 2.0};
|
||||
int grad_calls = 0;
|
||||
|
||||
auto grad = [&](const std::vector<double>& x) {
|
||||
++grad_calls;
|
||||
std::vector<double> g(x.size());
|
||||
for (std::size_t i = 0; i < x.size(); ++i) g[i] = xstar[i] - x[i];
|
||||
return g; // G(x) = x* − x, H = −I
|
||||
};
|
||||
auto hess = [&](const std::vector<double>&) {
|
||||
Eigen::SparseMatrix<double> H(3, 3);
|
||||
for (int i = 0; i < 3; ++i) H.insert(i, i) = -1.0;
|
||||
H.makeCompressed();
|
||||
return H;
|
||||
};
|
||||
|
||||
auto res = conformallab::detail::newton_core(
|
||||
std::vector<double>{0.0, 0.0, 0.0}, grad, hess,
|
||||
/*concave=*/true, /*tol=*/1e-9, /*max_iter=*/50);
|
||||
|
||||
ASSERT_TRUE(res.converged);
|
||||
EXPECT_EQ(res.iterations, 1);
|
||||
EXPECT_NEAR(res.x[0], 0.5, 1e-9);
|
||||
EXPECT_NEAR(res.x[1], -1.5, 1e-9);
|
||||
EXPECT_NEAR(res.x[2], 2.0, 1e-9);
|
||||
EXPECT_EQ(grad_calls, 2);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user