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:
@@ -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)
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user