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

@@ -315,6 +315,43 @@ inline std::vector<double> inversive_distance_gradient(
return G;
}
/// Per-face contribution to the Inversive-Distance gradient, as a pure
/// 3→3 kernel of the three local vertex DOFs `(u1,u2,u3)` and the three
/// edge inversive distances `(I12,I23,I31)`. No mesh, no property maps —
/// used by the per-face block-FD Hessian in `inversive_distance_hessian.hpp`.
///
/// Returns the three values this face *adds* to the global gradient at
/// `(v1,v2,v3)`. Since `G_v = Θ_v Σ_faces α_v`, the per-face contribution
/// is the **negative** corner angles `(−α₁,−α₂,−α₃)`. A non-real circle
/// configuration (any `ℓ² ≤ 0`) contributes nothing — exactly mirroring the
/// face-skip (`continue`) in `inversive_distance_gradient`, so the block-FD
/// Hessian and the full-FD Hessian see the same per-face support.
struct IDFaceGradContribs {
double g1; ///< contribution to G at v₁ (= −α₁)
double g2; ///< contribution to G at v₂ (= −α₂)
double g3; ///< contribution to G at v₃ (= −α₃)
};
inline IDFaceGradContribs inversive_distance_face_grad_contribs(
double u1, double u2, double u3,
double I12, double I23, double I31)
{
double l12sq = id_detail::edge_length_squared(u1, u2, I12);
double l23sq = id_detail::edge_length_squared(u2, u3, I23);
double l31sq = id_detail::edge_length_squared(u3, u1, I31);
// Same face-skip as inversive_distance_gradient: a non-real circle
// configuration has no limiting angle, so the face contributes 0.
if (l12sq <= 0.0 || l23sq <= 0.0 || l31sq <= 0.0)
return {0.0, 0.0, 0.0};
// euclidean_angles deliberately keeps its limiting (valid=false) angles
// here — the convex C¹ extension — matching the gradient's choice not to
// skip on !fa.valid. Corner at v_k = fa.alpha_k (see gradient Pass-2 trace).
auto fa = euclidean_angles(std::log(l12sq), std::log(l23sq), std::log(l31sq));
return {-fa.alpha1, -fa.alpha2, -fa.alpha3};
}
/// Inversive-Distance energy `E(u) = ∫₀¹ ⟨G(t·u), u⟩ dt`, evaluated
/// with 10-point Gauss-Legendre (constants shared with `euclidean_energy`).
inline double inversive_distance_energy(