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:
@@ -142,6 +142,14 @@ namespace detail { // re-open for the remaining helpers
|
|||||||
// If neither phase satisfies Armijo, return the best (smallest-residual) point
|
// If neither phase satisfies Armijo, return the best (smallest-residual) point
|
||||||
// visited. If nothing beat ‖G(x)‖, return x unchanged and set *improved=false,
|
// visited. If nothing beat ‖G(x)‖, return x unchanged and set *improved=false,
|
||||||
// so the caller can stop cleanly instead of taking the old divergent full step.
|
// so the caller can stop cleanly instead of taking the old divergent full step.
|
||||||
|
//
|
||||||
|
// B2 (api-performance audit): the gradient computed at the accepted trial point
|
||||||
|
// is normally discarded (only its norm is kept), forcing the next Newton
|
||||||
|
// iteration to recompute G(x) from scratch. When `accepted_grad` is non-null it
|
||||||
|
// receives the gradient vector at the returned point, so the caller can reuse it
|
||||||
|
// as the next iteration's convergence gradient. It is only written when the
|
||||||
|
// returned point differs from `x` (i.e. `*improved == true`); on a pure stall
|
||||||
|
// (`*improved == false`) the caller must recompute G(x) itself.
|
||||||
template <typename GradFn>
|
template <typename GradFn>
|
||||||
inline std::vector<double> line_search(
|
inline std::vector<double> line_search(
|
||||||
const std::vector<double>& x,
|
const std::vector<double>& x,
|
||||||
@@ -150,6 +158,7 @@ inline std::vector<double> line_search(
|
|||||||
double norm0,
|
double norm0,
|
||||||
GradFn&& grad_fn,
|
GradFn&& grad_fn,
|
||||||
bool* improved = nullptr,
|
bool* improved = nullptr,
|
||||||
|
std::vector<double>* accepted_grad = nullptr,
|
||||||
int max_halvings = 20,
|
int max_halvings = 20,
|
||||||
double c1 = 1e-4)
|
double c1 = 1e-4)
|
||||||
{
|
{
|
||||||
@@ -160,14 +169,18 @@ inline std::vector<double> line_search(
|
|||||||
std::vector<double> best_x = x;
|
std::vector<double> best_x = x;
|
||||||
double best_norm = norm0;
|
double best_norm = norm0;
|
||||||
|
|
||||||
// Evaluate ‖G(x + α·dir)‖₂, leaving the trial point in `xnew`.
|
std::vector<double> trial_grad; // gradient at the most recent trial point
|
||||||
|
std::vector<double> best_grad; // gradient at best_x (B2)
|
||||||
|
|
||||||
|
// Evaluate ‖G(x + α·dir)‖₂, leaving the trial point in `xnew` and the
|
||||||
|
// gradient there in `trial_grad`.
|
||||||
auto eval = [&](const Eigen::VectorXd& dir, double alpha) -> double {
|
auto eval = [&](const Eigen::VectorXd& dir, double alpha) -> double {
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i)
|
||||||
xnew[static_cast<std::size_t>(i)] =
|
xnew[static_cast<std::size_t>(i)] =
|
||||||
x[static_cast<std::size_t>(i)] + alpha * dir[i];
|
x[static_cast<std::size_t>(i)] + alpha * dir[i];
|
||||||
auto Gnew = grad_fn(xnew);
|
trial_grad = grad_fn(xnew);
|
||||||
double s = 0.0;
|
double s = 0.0;
|
||||||
for (double v : Gnew) s += v * v;
|
for (double v : trial_grad) s += v * v;
|
||||||
return std::sqrt(s);
|
return std::sqrt(s);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -175,9 +188,10 @@ inline std::vector<double> line_search(
|
|||||||
double alpha = 1.0;
|
double alpha = 1.0;
|
||||||
for (int ls = 0; ls < max_halvings; ++ls) {
|
for (int ls = 0; ls < max_halvings; ++ls) {
|
||||||
double norm_new = eval(dx, alpha);
|
double norm_new = eval(dx, alpha);
|
||||||
if (norm_new < best_norm) { best_norm = norm_new; best_x = xnew; }
|
if (norm_new < best_norm) { best_norm = norm_new; best_x = xnew; best_grad = trial_grad; }
|
||||||
if (norm_new * norm_new <= (1.0 - 2.0 * c1 * alpha) * norm0_sq) {
|
if (norm_new * norm_new <= (1.0 - 2.0 * c1 * alpha) * norm0_sq) {
|
||||||
if (improved) *improved = true;
|
if (improved) *improved = true;
|
||||||
|
if (accepted_grad) *accepted_grad = trial_grad;
|
||||||
return xnew;
|
return xnew;
|
||||||
}
|
}
|
||||||
alpha *= 0.5;
|
alpha *= 0.5;
|
||||||
@@ -190,9 +204,10 @@ inline std::vector<double> line_search(
|
|||||||
for (int ls = 0; ls < max_halvings; ++ls) {
|
for (int ls = 0; ls < max_halvings; ++ls) {
|
||||||
double thresh_sq = norm0_sq - 2.0 * c1 * alpha * dsd_sq;
|
double thresh_sq = norm0_sq - 2.0 * c1 * alpha * dsd_sq;
|
||||||
double norm_new = eval(d_sd, alpha);
|
double norm_new = eval(d_sd, alpha);
|
||||||
if (norm_new < best_norm) { best_norm = norm_new; best_x = xnew; }
|
if (norm_new < best_norm) { best_norm = norm_new; best_x = xnew; best_grad = trial_grad; }
|
||||||
if (thresh_sq >= 0.0 && norm_new * norm_new <= thresh_sq) {
|
if (thresh_sq >= 0.0 && norm_new * norm_new <= thresh_sq) {
|
||||||
if (improved) *improved = true;
|
if (improved) *improved = true;
|
||||||
|
if (accepted_grad) *accepted_grad = trial_grad;
|
||||||
return xnew;
|
return xnew;
|
||||||
}
|
}
|
||||||
alpha *= 0.5;
|
alpha *= 0.5;
|
||||||
@@ -201,10 +216,144 @@ inline std::vector<double> line_search(
|
|||||||
|
|
||||||
// ── Both phases failed Armijo — never take the divergent full step. ───────
|
// ── Both phases failed Armijo — never take the divergent full step. ───────
|
||||||
// Return the best point seen; if none improved, stay put and signal stall.
|
// Return the best point seen; if none improved, stay put and signal stall.
|
||||||
if (improved) *improved = (best_norm < norm0);
|
const bool any_improvement = (best_norm < norm0);
|
||||||
|
if (improved) *improved = any_improvement;
|
||||||
|
if (accepted_grad && any_improvement) *accepted_grad = best_grad;
|
||||||
return best_x;
|
return best_x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Cached linear solver (B4) ─────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// solve_with_fallback rebuilds a fresh SimplicialLDLT every call, re-running the
|
||||||
|
// symbolic analysis (fill-reducing reordering / COLAMD) each Newton iteration —
|
||||||
|
// even though the Hessian sparsity pattern is iteration-invariant for a fixed
|
||||||
|
// mesh + DOF layout. This object keeps one persistent LDLT and runs
|
||||||
|
// `analyzePattern` once, then only `factorize` per iteration.
|
||||||
|
//
|
||||||
|
// FD-built Hessians (hyper-ideal, inversive-distance) prune near-zero triplets,
|
||||||
|
// so their pattern can shift slightly between iterations; we detect that via a
|
||||||
|
// change in `nonZeros()` and re-analyze, which keeps the cache correct and
|
||||||
|
// self-healing. The SparseQR fallback for singular/rank-deficient systems is
|
||||||
|
// preserved verbatim (built fresh on the rare path). The fallback semantics are
|
||||||
|
// identical to `solve_with_fallback`, so behaviour is unchanged.
|
||||||
|
struct NewtonLinearSolver {
|
||||||
|
Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>> ldlt;
|
||||||
|
bool analyzed = false;
|
||||||
|
Eigen::Index last_nnz = -1;
|
||||||
|
bool fallback_used = false; ///< true iff the last solve used SparseQR
|
||||||
|
|
||||||
|
Eigen::VectorXd solve(const Eigen::SparseMatrix<double>& A,
|
||||||
|
const Eigen::VectorXd& rhs,
|
||||||
|
bool& ok)
|
||||||
|
{
|
||||||
|
fallback_used = false;
|
||||||
|
if (!analyzed || A.nonZeros() != last_nnz) {
|
||||||
|
ldlt.analyzePattern(A);
|
||||||
|
analyzed = true;
|
||||||
|
last_nnz = A.nonZeros();
|
||||||
|
}
|
||||||
|
ldlt.factorize(A);
|
||||||
|
if (ldlt.info() == Eigen::Success) {
|
||||||
|
Eigen::VectorXd x = ldlt.solve(rhs);
|
||||||
|
if (ldlt.info() == Eigen::Success) { ok = true; return x; }
|
||||||
|
}
|
||||||
|
// Fallback: SparseQR — handles singular/rank-deficient A (gauge modes).
|
||||||
|
fallback_used = true;
|
||||||
|
Eigen::SparseQR<Eigen::SparseMatrix<double>, Eigen::COLAMDOrdering<int>> qr(A);
|
||||||
|
if (qr.info() == Eigen::Success) {
|
||||||
|
Eigen::VectorXd x = qr.solve(rhs);
|
||||||
|
if (qr.info() == Eigen::Success) { ok = true; return x; }
|
||||||
|
}
|
||||||
|
ok = false;
|
||||||
|
return Eigen::VectorXd::Zero(rhs.size());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Unified Newton core (H2) ──────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// All five DCE Newton solvers (Euclidean, Spherical, HyperIdeal, CP-Euclidean,
|
||||||
|
// Inversive-Distance) ran a near-identical loop that differed only in (a) the
|
||||||
|
// gradient function, (b) the Hessian function, and (c) the spherical sign quirk
|
||||||
|
// (the concave spherical energy factorises −H). This template captures that one
|
||||||
|
// loop so a fix lands once instead of five times.
|
||||||
|
//
|
||||||
|
// `grad(x) -> std::vector<double>` : the energy gradient G(x).
|
||||||
|
// `hess(x) -> Eigen::SparseMatrix<double>`: the TRUE Hessian H(x) (un-negated).
|
||||||
|
// `concave` : if true, solve (−H)·Δx = G (spherical);
|
||||||
|
// otherwise H·Δx = −G. Both are the same
|
||||||
|
// Newton system; `concave` only selects
|
||||||
|
// the PSD matrix actually factorised.
|
||||||
|
//
|
||||||
|
// Folds in B2 (reuse the line-search gradient as the next convergence gradient),
|
||||||
|
// B4 (cached symbolic factorization), and B5 (no dead solver variable).
|
||||||
|
template <typename GradFn, typename HessFn>
|
||||||
|
inline NewtonResult newton_core(
|
||||||
|
std::vector<double> x,
|
||||||
|
GradFn&& grad,
|
||||||
|
HessFn&& hess,
|
||||||
|
bool concave,
|
||||||
|
double tol,
|
||||||
|
int max_iter)
|
||||||
|
{
|
||||||
|
const int n = static_cast<int>(x.size());
|
||||||
|
|
||||||
|
NewtonResult res;
|
||||||
|
res.converged = false;
|
||||||
|
res.iterations = 0;
|
||||||
|
res.grad_inf_norm = 0.0;
|
||||||
|
|
||||||
|
NewtonLinearSolver solver;
|
||||||
|
|
||||||
|
// B2: gradient carried across the loop; the first one is the only "extra"
|
||||||
|
// evaluation — every later iteration reuses the line-search gradient.
|
||||||
|
std::vector<double> G_std = grad(x);
|
||||||
|
|
||||||
|
for (int iter = 0; iter < max_iter; ++iter) {
|
||||||
|
Eigen::Map<const Eigen::VectorXd> G(G_std.data(), n);
|
||||||
|
|
||||||
|
const double inf_norm = G.cwiseAbs().maxCoeff();
|
||||||
|
if (inf_norm < tol) {
|
||||||
|
res.converged = true;
|
||||||
|
res.grad_inf_norm = inf_norm;
|
||||||
|
res.iterations = iter;
|
||||||
|
res.x = x;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
Eigen::SparseMatrix<double> H = hess(x);
|
||||||
|
|
||||||
|
// Newton system: factor the PSD matrix (H for convex, −H for concave).
|
||||||
|
Eigen::SparseMatrix<double> A = concave ? Eigen::SparseMatrix<double>(-H) : H;
|
||||||
|
Eigen::VectorXd rhs = concave ? Eigen::VectorXd(G) : Eigen::VectorXd(-G);
|
||||||
|
|
||||||
|
bool ok = false;
|
||||||
|
Eigen::VectorXd dx = solver.solve(A, rhs, ok);
|
||||||
|
if (!ok) break;
|
||||||
|
|
||||||
|
// Globalised line search (Armijo + steepest-descent fallback).
|
||||||
|
// d_sd = −(H·G) is the merit-function steepest descent for f = ½‖G‖²,
|
||||||
|
// using the TRUE (un-negated) Hessian for both signs.
|
||||||
|
const double norm0 = G.norm();
|
||||||
|
Eigen::VectorXd d_sd = -(H * G);
|
||||||
|
bool improved = true;
|
||||||
|
std::vector<double> G_next;
|
||||||
|
x = detail::line_search(x, dx, d_sd, norm0, grad, &improved, &G_next);
|
||||||
|
if (!improved) break; // line search stalled — stop cleanly
|
||||||
|
|
||||||
|
G_std = std::move(G_next); // B2: reuse for the next convergence check
|
||||||
|
res.iterations = iter + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final gradient norm (reached only on max-iter / break — recomputed to be
|
||||||
|
// correct after a stalled or failed step).
|
||||||
|
auto G_final = grad(x);
|
||||||
|
double inf_final = 0.0;
|
||||||
|
for (double v : G_final) inf_final = std::max(inf_final, std::abs(v));
|
||||||
|
res.grad_inf_norm = inf_final;
|
||||||
|
res.x = x;
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
|
|
||||||
// ── Euclidean Newton solver ────────────────────────────────────────────────────
|
// ── Euclidean Newton solver ────────────────────────────────────────────────────
|
||||||
@@ -238,63 +387,21 @@ inline NewtonResult newton_euclidean(
|
|||||||
double tol = 1e-8,
|
double tol = 1e-8,
|
||||||
int max_iter = 200)
|
int max_iter = 200)
|
||||||
{
|
{
|
||||||
std::vector<double> x = x0;
|
// Layout is loop-invariant — decide the Hessian variant once (B3): cyclic
|
||||||
const int n = static_cast<int>(x.size());
|
// layout (edge DOFs present) → analytic cyclic Hessian, which covers the
|
||||||
|
// vertex-edge / edge-edge blocks the vertex-only cotangent Laplacian omits.
|
||||||
NewtonResult res;
|
|
||||||
res.converged = false;
|
|
||||||
res.iterations = 0;
|
|
||||||
res.grad_inf_norm = 0.0;
|
|
||||||
|
|
||||||
Eigen::SimplicialLDLT<Eigen::SparseMatrix<double>> solver;
|
|
||||||
|
|
||||||
for (int iter = 0; iter < max_iter; ++iter) {
|
|
||||||
// ── Gradient ──────────────────────────────────────────────────────────
|
|
||||||
auto G_std = euclidean_gradient(mesh, x, m);
|
|
||||||
Eigen::Map<const Eigen::VectorXd> G(G_std.data(), n);
|
|
||||||
|
|
||||||
double inf_norm = G.cwiseAbs().maxCoeff();
|
|
||||||
if (inf_norm < tol) {
|
|
||||||
res.converged = true;
|
|
||||||
res.grad_inf_norm = inf_norm;
|
|
||||||
res.iterations = iter;
|
|
||||||
res.x = x;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Hessian + solve H·Δx = −G (SparseQR fallback for singular H) ──
|
|
||||||
// Cyclic layout (edge DOFs present) → analytic cyclic Hessian, covering
|
|
||||||
// the vertex-edge / edge-edge blocks the vertex-only cotangent Laplacian
|
|
||||||
// omits. Vertex-only layout → analytic cotangent Laplacian.
|
|
||||||
bool has_edge_dof = false;
|
bool has_edge_dof = false;
|
||||||
for (auto e : mesh.edges())
|
for (auto e : mesh.edges())
|
||||||
if (m.e_idx[e] >= 0) { has_edge_dof = true; break; }
|
if (m.e_idx[e] >= 0) { has_edge_dof = true; break; }
|
||||||
auto H = has_edge_dof ? euclidean_hessian_analytic(mesh, x, m)
|
|
||||||
: euclidean_hessian(mesh, x, m);
|
|
||||||
bool ok = false;
|
|
||||||
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
|
|
||||||
if (!ok) break;
|
|
||||||
|
|
||||||
// ── Globalised line search (Armijo + steepest-descent fallback) ───────
|
return detail::newton_core(
|
||||||
double norm0 = G.norm();
|
std::move(x0),
|
||||||
Eigen::VectorXd d_sd = -(H * G); // merit-function steepest descent
|
[&](const std::vector<double>& xc) { return euclidean_gradient(mesh, xc, m); },
|
||||||
bool improved = true;
|
[&](const std::vector<double>& xc) {
|
||||||
x = detail::line_search(x, dx, d_sd, norm0,
|
return has_edge_dof ? euclidean_hessian_analytic(mesh, xc, m)
|
||||||
[&](const std::vector<double>& xnew) {
|
: euclidean_hessian(mesh, xc, m);
|
||||||
return euclidean_gradient(mesh, xnew, m);
|
},
|
||||||
}, &improved);
|
/*concave=*/false, tol, max_iter);
|
||||||
if (!improved) break; // line search stalled — stop cleanly
|
|
||||||
|
|
||||||
res.iterations = iter + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Report final gradient norm
|
|
||||||
auto G_final = euclidean_gradient(mesh, x, m);
|
|
||||||
double inf_final = 0.0;
|
|
||||||
for (double v : G_final) inf_final = std::max(inf_final, std::abs(v));
|
|
||||||
res.grad_inf_norm = inf_final;
|
|
||||||
res.x = x;
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Spherical Newton solver ───────────────────────────────────────────────────
|
// ── Spherical Newton solver ───────────────────────────────────────────────────
|
||||||
@@ -327,55 +434,14 @@ inline NewtonResult newton_spherical(
|
|||||||
double tol = 1e-8,
|
double tol = 1e-8,
|
||||||
int max_iter = 200)
|
int max_iter = 200)
|
||||||
{
|
{
|
||||||
std::vector<double> x = x0;
|
// Concave energy: the Hessian H is NSD, so newton_core factors −H (PSD) and
|
||||||
const int n = static_cast<int>(x.size());
|
// solves (−H)·Δx = G — the same Newton system, just the sign that keeps LDLT
|
||||||
|
// well-defined. The merit steepest-descent inside still uses the true H.
|
||||||
NewtonResult res;
|
return detail::newton_core(
|
||||||
res.converged = false;
|
std::move(x0),
|
||||||
res.iterations = 0;
|
[&](const std::vector<double>& xc) { return spherical_gradient(mesh, xc, m); },
|
||||||
res.grad_inf_norm = 0.0;
|
[&](const std::vector<double>& xc) { return spherical_hessian(mesh, xc, m); },
|
||||||
|
/*concave=*/true, tol, max_iter);
|
||||||
for (int iter = 0; iter < max_iter; ++iter) {
|
|
||||||
// ── Gradient ──────────────────────────────────────────────────────────
|
|
||||||
auto G_std = spherical_gradient(mesh, x, m);
|
|
||||||
Eigen::Map<const Eigen::VectorXd> G(G_std.data(), n);
|
|
||||||
|
|
||||||
double inf_norm = G.cwiseAbs().maxCoeff();
|
|
||||||
if (inf_norm < tol) {
|
|
||||||
res.converged = true;
|
|
||||||
res.grad_inf_norm = inf_norm;
|
|
||||||
res.iterations = iter;
|
|
||||||
res.x = x;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Hessian: negate to get PSD; solve (−H)·Δx = G ──────────────────
|
|
||||||
auto H = spherical_hessian(mesh, x, m);
|
|
||||||
auto negH = Eigen::SparseMatrix<double>(-H);
|
|
||||||
bool ok = false;
|
|
||||||
Eigen::VectorXd dx = detail::solve_with_fallback(negH, G, ok);
|
|
||||||
if (!ok) break;
|
|
||||||
|
|
||||||
// ── Globalised line search (Armijo + steepest-descent fallback) ───────
|
|
||||||
// d_sd uses the actual (un-negated) Hessian: ∇f = H·G for f = ½‖G‖².
|
|
||||||
double norm0 = G.norm();
|
|
||||||
Eigen::VectorXd d_sd = -(H * G);
|
|
||||||
bool improved = true;
|
|
||||||
x = detail::line_search(x, dx, d_sd, norm0,
|
|
||||||
[&](const std::vector<double>& xnew) {
|
|
||||||
return spherical_gradient(mesh, xnew, m);
|
|
||||||
}, &improved);
|
|
||||||
if (!improved) break;
|
|
||||||
|
|
||||||
res.iterations = iter + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto G_final = spherical_gradient(mesh, x, m);
|
|
||||||
double inf_final = 0.0;
|
|
||||||
for (double v : G_final) inf_final = std::max(inf_final, std::abs(v));
|
|
||||||
res.grad_inf_norm = inf_final;
|
|
||||||
res.x = x;
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── HyperIdeal Newton solver ──────────────────────────────────────────────────
|
// ── HyperIdeal Newton solver ──────────────────────────────────────────────────
|
||||||
@@ -419,53 +485,17 @@ inline NewtonResult newton_hyper_ideal(
|
|||||||
double hess_eps = 1e-5,
|
double hess_eps = 1e-5,
|
||||||
HyperIdealScaleClamp clamp = HyperIdealScaleClamp::HardJava)
|
HyperIdealScaleClamp clamp = HyperIdealScaleClamp::HardJava)
|
||||||
{
|
{
|
||||||
std::vector<double> x = x0;
|
// block-FD Hessian (~33–1166× faster than full-FD); `clamp` selects the
|
||||||
const int n = static_cast<int>(x.size());
|
// vertex-scale floor mode (N3). Convex energy → factor H directly.
|
||||||
|
return detail::newton_core(
|
||||||
NewtonResult res;
|
std::move(x0),
|
||||||
res.converged = false;
|
[&](const std::vector<double>& xc) {
|
||||||
res.iterations = 0;
|
return evaluate_hyper_ideal(mesh, xc, m, /*energy=*/false, /*grad=*/true, clamp).gradient;
|
||||||
res.grad_inf_norm = 0.0;
|
},
|
||||||
|
[&](const std::vector<double>& xc) {
|
||||||
for (int iter = 0; iter < max_iter; ++iter) {
|
return hyper_ideal_hessian_block_fd_sym(mesh, xc, m, hess_eps, clamp);
|
||||||
// ── Gradient ──────────────────────────────────────────────────────────
|
},
|
||||||
auto G_std = evaluate_hyper_ideal(mesh, x, m, /*energy=*/false, /*grad=*/true, clamp).gradient;
|
/*concave=*/false, tol, max_iter);
|
||||||
Eigen::Map<const Eigen::VectorXd> G(G_std.data(), n);
|
|
||||||
|
|
||||||
double inf_norm = G.cwiseAbs().maxCoeff();
|
|
||||||
if (inf_norm < tol) {
|
|
||||||
res.converged = true;
|
|
||||||
res.grad_inf_norm = inf_norm;
|
|
||||||
res.iterations = iter;
|
|
||||||
res.x = x;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Hessian (block-FD, ~33–1166× faster than full-FD) + solve H·Δx = −G
|
|
||||||
auto H = hyper_ideal_hessian_block_fd_sym(mesh, x, m, hess_eps, clamp);
|
|
||||||
bool ok = false;
|
|
||||||
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
|
|
||||||
if (!ok) break;
|
|
||||||
|
|
||||||
// ── Globalised line search (Armijo + steepest-descent fallback) ───────
|
|
||||||
double norm0 = G.norm();
|
|
||||||
Eigen::VectorXd d_sd = -(H * G);
|
|
||||||
bool improved = true;
|
|
||||||
x = detail::line_search(x, dx, d_sd, norm0,
|
|
||||||
[&](const std::vector<double>& xnew) {
|
|
||||||
return evaluate_hyper_ideal(mesh, xnew, m, false, true, clamp).gradient;
|
|
||||||
}, &improved);
|
|
||||||
if (!improved) break;
|
|
||||||
|
|
||||||
res.iterations = iter + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto G_final = evaluate_hyper_ideal(mesh, x, m, false, true, clamp).gradient;
|
|
||||||
double inf_final = 0.0;
|
|
||||||
for (double v : G_final) inf_final = std::max(inf_final, std::abs(v));
|
|
||||||
res.grad_inf_norm = inf_final;
|
|
||||||
res.x = x;
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CP-Euclidean Newton solver (Phase 9a.1) ───────────────────────────────────
|
// ── CP-Euclidean Newton solver (Phase 9a.1) ───────────────────────────────────
|
||||||
@@ -499,50 +529,12 @@ inline NewtonResult newton_cp_euclidean(
|
|||||||
double tol = 1e-8,
|
double tol = 1e-8,
|
||||||
int max_iter = 200)
|
int max_iter = 200)
|
||||||
{
|
{
|
||||||
std::vector<double> x = x0;
|
// Convex energy with an exact analytic Hessian (BPS-2015 2×2-per-edge).
|
||||||
const int n = static_cast<int>(x.size());
|
return detail::newton_core(
|
||||||
|
std::move(x0),
|
||||||
NewtonResult res;
|
[&](const std::vector<double>& xc) { return cp_euclidean_gradient(mesh, xc, m); },
|
||||||
res.converged = false;
|
[&](const std::vector<double>& xc) { return cp_euclidean_hessian(mesh, xc, m); },
|
||||||
res.iterations = 0;
|
/*concave=*/false, tol, max_iter);
|
||||||
res.grad_inf_norm = 0.0;
|
|
||||||
|
|
||||||
for (int iter = 0; iter < max_iter; ++iter) {
|
|
||||||
auto G_std = cp_euclidean_gradient(mesh, x, m);
|
|
||||||
Eigen::Map<const Eigen::VectorXd> G(G_std.data(), n);
|
|
||||||
|
|
||||||
double inf_norm = G.cwiseAbs().maxCoeff();
|
|
||||||
if (inf_norm < tol) {
|
|
||||||
res.converged = true;
|
|
||||||
res.grad_inf_norm = inf_norm;
|
|
||||||
res.iterations = iter;
|
|
||||||
res.x = x;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto H = cp_euclidean_hessian(mesh, x, m);
|
|
||||||
bool ok = false;
|
|
||||||
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
|
|
||||||
if (!ok) break;
|
|
||||||
|
|
||||||
double norm0 = G.norm();
|
|
||||||
Eigen::VectorXd d_sd = -(H * G);
|
|
||||||
bool improved = true;
|
|
||||||
x = detail::line_search(x, dx, d_sd, norm0,
|
|
||||||
[&](const std::vector<double>& xnew) {
|
|
||||||
return cp_euclidean_gradient(mesh, xnew, m);
|
|
||||||
}, &improved);
|
|
||||||
if (!improved) break;
|
|
||||||
|
|
||||||
res.iterations = iter + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto G_final = cp_euclidean_gradient(mesh, x, m);
|
|
||||||
double inf_final = 0.0;
|
|
||||||
for (double v : G_final) inf_final = std::max(inf_final, std::abs(v));
|
|
||||||
res.grad_inf_norm = inf_final;
|
|
||||||
res.x = x;
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Inversive-Distance Newton solver (Phase 9a.2) ─────────────────────────────
|
// ── Inversive-Distance Newton solver (Phase 9a.2) ─────────────────────────────
|
||||||
@@ -581,51 +573,14 @@ inline NewtonResult newton_inversive_distance(
|
|||||||
int max_iter = 200,
|
int max_iter = 200,
|
||||||
double hess_eps = 1e-5)
|
double hess_eps = 1e-5)
|
||||||
{
|
{
|
||||||
std::vector<double> x = x0;
|
// Convex energy; block-FD Hessian (~n/6× faster than full-FD).
|
||||||
const int n = static_cast<int>(x.size());
|
return detail::newton_core(
|
||||||
|
std::move(x0),
|
||||||
NewtonResult res;
|
[&](const std::vector<double>& xc) { return inversive_distance_gradient(mesh, xc, m); },
|
||||||
res.converged = false;
|
[&](const std::vector<double>& xc) {
|
||||||
res.iterations = 0;
|
return inversive_distance_hessian_block_fd_sym(mesh, xc, m, hess_eps);
|
||||||
res.grad_inf_norm = 0.0;
|
},
|
||||||
|
/*concave=*/false, tol, max_iter);
|
||||||
for (int iter = 0; iter < max_iter; ++iter) {
|
|
||||||
auto G_std = inversive_distance_gradient(mesh, x, m);
|
|
||||||
Eigen::Map<const Eigen::VectorXd> G(G_std.data(), n);
|
|
||||||
|
|
||||||
double inf_norm = G.cwiseAbs().maxCoeff();
|
|
||||||
if (inf_norm < tol) {
|
|
||||||
res.converged = true;
|
|
||||||
res.grad_inf_norm = inf_norm;
|
|
||||||
res.iterations = iter;
|
|
||||||
res.x = x;
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hessian (block-FD, ~n/6× faster than full-FD) + solve H·Δx = −G.
|
|
||||||
auto H = inversive_distance_hessian_block_fd_sym(mesh, x, m, hess_eps);
|
|
||||||
bool ok = false;
|
|
||||||
Eigen::VectorXd dx = detail::solve_with_fallback(H, -G, ok);
|
|
||||||
if (!ok) break;
|
|
||||||
|
|
||||||
double norm0 = G.norm();
|
|
||||||
Eigen::VectorXd d_sd = -(H * G);
|
|
||||||
bool improved = true;
|
|
||||||
x = detail::line_search(x, dx, d_sd, norm0,
|
|
||||||
[&](const std::vector<double>& xnew) {
|
|
||||||
return inversive_distance_gradient(mesh, xnew, m);
|
|
||||||
}, &improved);
|
|
||||||
if (!improved) break;
|
|
||||||
|
|
||||||
res.iterations = iter + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto G_final = inversive_distance_gradient(mesh, x, m);
|
|
||||||
double inf_final = 0.0;
|
|
||||||
for (double v : G_final) inf_final = std::max(inf_final, std::abs(v));
|
|
||||||
res.grad_inf_norm = inf_final;
|
|
||||||
res.x = x;
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace conformallab
|
} // namespace conformallab
|
||||||
|
|||||||
@@ -579,3 +579,78 @@ TEST(SparseQRFallback, OkFlag_NullPointerIsSafe)
|
|||||||
EXPECT_NEAR(x[1], 5.0, 1e-12);
|
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