perf(inv-dist): B1 port — block-FD Hessian for newton_inversive_distance

The Inversive-Distance solver built its Hessian inline by full finite
differences: n perturbations × a full O(F) gradient eval = O(n·F) per Newton
iteration (quadratic in mesh size), with no fast path at all (api-performance
audit B1, second half).

Port the per-face block-FD scheme already used by HyperIdeal (Phase 9b):
the gradient decomposes by face (G_v = Θ_v − Σ_{f∋v} α_v, and each face's
angles depend only on its 3 vertex DOFs), so the Hessian decomposes into
per-face 3×3 blocks.  Cost drops to O(F) face evaluations, a ≈ n/6 speed-up.

- inversive_distance_functional.hpp: add the pure 3→3 kernel
  inversive_distance_face_grad_contribs (returns the per-face contribution
  −α to G; mirrors the gradient's face-skip on ℓ²≤0 exactly).
- inversive_distance_hessian.hpp (new): full-FD baseline + block-FD + sym
  variants, mirroring hyper_ideal_hessian.hpp.
- newton_solver.hpp: drop the inline full-FD lambda; call
  inversive_distance_hessian_block_fd_sym.
- test: InversiveDistance_BlockFDHessianMatchesFullFD cross-validates the
  two Hessians entry-wise on a perturbed (off-equilibrium) config.

291/291 CGAL tests pass; all Inversive-Distance convergence tests unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-31 16:03:12 +02:00
parent a5718c0326
commit 2dc4ddcc32
4 changed files with 276 additions and 36 deletions

View File

@@ -16,6 +16,7 @@
#include "newton_solver.hpp"
#include "cp_euclidean_functional.hpp"
#include "inversive_distance_functional.hpp"
#include "inversive_distance_hessian.hpp"
#include "mesh_builder.hpp"
#include "conformal_mesh.hpp"
@@ -232,6 +233,49 @@ TEST(NewtonPhase9a, InversiveDistance_PerturbedTetrahedron_Converges)
EXPECT_LT(res.grad_inf_norm, 1e-8);
}
// ════════════════════════════════════════════════════════════════════════════
// 6b. Inversive-Distance block-FD Hessian == full-FD Hessian (B1 port)
//
// The solver was switched from the O(n·F) full-FD Hessian to the O(F)
// per-face block-FD Hessian. The two must agree to FD rounding on any
// configuration — including a perturbed (non-equilibrium) one, where the
// off-diagonal coupling is non-trivial. This is the locality-lemma
// cross-validation that licenses the faster path.
// ════════════════════════════════════════════════════════════════════════════
TEST(NewtonPhase9a, InversiveDistance_BlockFDHessianMatchesFullFD)
{
auto mesh = make_tetrahedron();
auto m = setup_inversive_distance_maps(mesh);
compute_inversive_distance_init_from_mesh(mesh, m);
// Pin one vertex; index the rest.
auto vit = mesh.vertices().begin();
m.v_idx[*vit++] = -1;
int n = 0;
for (; vit != mesh.vertices().end(); ++vit) m.v_idx[*vit] = n++;
// Evaluate the two Hessians away from equilibrium so off-diagonals are live.
std::vector<double> x(static_cast<std::size_t>(n), 0.0);
for (int i = 0; i < n; ++i) x[static_cast<std::size_t>(i)] = 0.07 * (i + 1);
auto H_full = inversive_distance_hessian_sym(mesh, x, m);
auto H_block = inversive_distance_hessian_block_fd_sym(mesh, x, m);
ASSERT_EQ(H_full.rows(), H_block.rows());
ASSERT_EQ(H_full.cols(), H_block.cols());
Eigen::MatrixXd Df = Eigen::MatrixXd(H_full);
Eigen::MatrixXd Db = Eigen::MatrixXd(H_block);
double max_abs_diff = (Df - Db).cwiseAbs().maxCoeff();
EXPECT_LT(max_abs_diff, 1e-7)
<< "block-FD and full-FD Hessians must agree to FD rounding;\n"
<< "max |Δ| = " << max_abs_diff;
// Sanity: the matrices are non-trivial (not both accidentally zero).
EXPECT_GT(Df.cwiseAbs().maxCoeff(), 1e-3);
}
// ════════════════════════════════════════════════════════════════════════════
// 7. CP-Euclidean Newton — uses analytic Hessian (NOT FD)
//