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>
188 lines
8.4 KiB
C++
188 lines
8.4 KiB
C++
#pragma once
|
||
// Copyright (c) 2024-2026 Tarik Moussa.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
// inversive_distance_hessian.hpp
|
||
//
|
||
// Phase 9a.2 — Hessian of the inversive-distance circle-packing functional
|
||
// (Luo 2004 / Bowers-Stephenson 2004).
|
||
//
|
||
// ┌──────────────────────────────────────────────────────────────────────────┐
|
||
// │ Implementation strategy │
|
||
// │ │
|
||
// │ TWO finite-difference Hessian implementations are provided here, │
|
||
// │ mirroring the HyperIdeal pair in `hyper_ideal_hessian.hpp`: │
|
||
// │ │
|
||
// │ 1. `inversive_distance_hessian` — full finite-difference baseline. │
|
||
// │ Cost ≈ n × (cost of a full gradient evaluation) = O(n · F). │
|
||
// │ Kept as the cross-validation reference for the block-FD variant. │
|
||
// │ │
|
||
// │ 2. `inversive_distance_hessian_block_fd` — per-face block-FD. │
|
||
// │ Each face contributes to the gradient through exactly 3 vertex │
|
||
// │ DOFs (u₁,u₂,u₃); we FD the 3×3 local Jacobian of that face's │
|
||
// │ gradient contribution and scatter it. Cost ≈ F × 6 face-angle │
|
||
// │ evaluations = O(F). Speed-up factor ≈ n/6 over full-FD. │
|
||
// │ │
|
||
// │ Why the block-FD is correct (locality lemma): │
|
||
// │ G_v = Θ_v − Σ_{f ∋ v} α_v(f), and α_v(f) depends ONLY on the 3 │
|
||
// │ vertex DOFs of face f. Hence ∂G_x/∂y = Σ_{f: x,y ∈ {v1,v2,v3}(f)} │
|
||
// │ ∂(−α_x)/∂y at f, so accumulating per-face 3×3 blocks reproduces the │
|
||
// │ full Hessian (identical to O(ε²)). │
|
||
// │ │
|
||
// │ An analytic Hessian via Glickenstein 2011 eq. (4.6) is tracked in │
|
||
// │ `doc/roadmap/research-track.md` as Phase 9a.2-analytic; it would take │
|
||
// │ the cost from O(F)·(FD constant) to a single O(F) analytic pass. │
|
||
// └──────────────────────────────────────────────────────────────────────────┘
|
||
|
||
#include "inversive_distance_functional.hpp"
|
||
#include <Eigen/Sparse>
|
||
#include <vector>
|
||
#include <cmath>
|
||
|
||
namespace conformallab {
|
||
|
||
/// Full finite-difference Inversive-Distance Hessian (baseline).
|
||
/// Cost: `n` full-gradient evaluations ≈ `O(n·F)`. Use for small meshes
|
||
/// or as a correctness reference for the block-FD variant.
|
||
inline Eigen::SparseMatrix<double> inversive_distance_hessian(
|
||
const ConformalMesh& mesh,
|
||
const std::vector<double>& x,
|
||
const InversiveDistanceMaps& m,
|
||
double eps = 1e-5)
|
||
{
|
||
const int n = inversive_distance_dimension(mesh, m);
|
||
std::vector<Eigen::Triplet<double>> trips;
|
||
trips.reserve(static_cast<std::size_t>(n) * 16);
|
||
|
||
std::vector<double> xp = x, xm = x;
|
||
for (int j = 0; j < n; ++j) {
|
||
const std::size_t sj = static_cast<std::size_t>(j);
|
||
xp[sj] = x[sj] + eps;
|
||
xm[sj] = x[sj] - eps;
|
||
|
||
auto Gp = inversive_distance_gradient(mesh, xp, m);
|
||
auto Gm = inversive_distance_gradient(mesh, xm, m);
|
||
|
||
xp[sj] = xm[sj] = x[sj]; // restore
|
||
|
||
for (int i = 0; i < n; ++i) {
|
||
double val = (Gp[static_cast<std::size_t>(i)]
|
||
- Gm[static_cast<std::size_t>(i)]) / (2.0 * eps);
|
||
if (std::abs(val) > 1e-15)
|
||
trips.emplace_back(i, j, val);
|
||
}
|
||
}
|
||
|
||
Eigen::SparseMatrix<double> H(n, n);
|
||
H.setFromTriplets(trips.begin(), trips.end());
|
||
return H;
|
||
}
|
||
|
||
/// Symmetrised full-FD Inversive-Distance Hessian: `(H + Hᵀ)/2`.
|
||
inline Eigen::SparseMatrix<double> inversive_distance_hessian_sym(
|
||
const ConformalMesh& mesh,
|
||
const std::vector<double>& x,
|
||
const InversiveDistanceMaps& m,
|
||
double eps = 1e-5)
|
||
{
|
||
auto H = inversive_distance_hessian(mesh, x, m, eps);
|
||
Eigen::SparseMatrix<double> Ht = H.transpose();
|
||
return (H + Ht) * 0.5;
|
||
}
|
||
|
||
// ── Block-FD Hessian ──────────────────────────────────────────────────────────
|
||
//
|
||
// The 3 local DOFs of a face f are (u_{v1}, u_{v2}, u_{v3}). For each free
|
||
// local DOF we recompute the face's gradient contribution (−α₁,−α₂,−α₃) at
|
||
// x ± ε along that axis and read off the 3×3 Jacobian. The result scatters
|
||
// into the global Hessian via the DOF-index lookup.
|
||
//
|
||
// Cost: F × 6 face-angle evaluations (3 DOFs × 2 directions) vs n×F for
|
||
// full-FD — a speed-up of ≈ n/6, i.e. ~hundreds× on large closed meshes.
|
||
/// Per-face block-FD Inversive-Distance Hessian. Uses the locality lemma
|
||
/// `∂G_x/∂y = Σ_{f: x,y ∈ {v1,v2,v3}(f)} ∂(−α_x)/∂y` to perturb only the 3
|
||
/// face-local DOFs at a time, giving an `F·6` face-evaluation budget vs `n·F`
|
||
/// for full-FD. Mathematically equivalent to `inversive_distance_hessian`
|
||
/// up to O(ε²) FD rounding.
|
||
inline Eigen::SparseMatrix<double> inversive_distance_hessian_block_fd(
|
||
const ConformalMesh& mesh,
|
||
const std::vector<double>& x,
|
||
const InversiveDistanceMaps& m,
|
||
double eps = 1e-5)
|
||
{
|
||
const int n = inversive_distance_dimension(mesh, m);
|
||
std::vector<Eigen::Triplet<double>> trips;
|
||
trips.reserve(9 * mesh.number_of_faces());
|
||
|
||
for (auto f : mesh.faces()) {
|
||
Halfedge_index h0 = mesh.halfedge(f);
|
||
Halfedge_index h1 = mesh.next(h0);
|
||
Halfedge_index h2 = mesh.next(h1);
|
||
|
||
Vertex_index v1 = mesh.source(h0);
|
||
Vertex_index v2 = mesh.source(h1);
|
||
Vertex_index v3 = mesh.source(h2);
|
||
Edge_index e12 = mesh.edge(h0);
|
||
Edge_index e23 = mesh.edge(h1);
|
||
Edge_index e31 = mesh.edge(h2);
|
||
|
||
// Local DOF indices: (u1, u2, u3). Pinned slots = -1.
|
||
const int idx[3] = { m.v_idx[v1], m.v_idx[v2], m.v_idx[v3] };
|
||
|
||
const double I12 = m.I_e[e12];
|
||
const double I23 = m.I_e[e23];
|
||
const double I31 = m.I_e[e31];
|
||
|
||
// Local DOF values (0 for pinned).
|
||
const double vals[3] = {
|
||
id_detail::dof_val(idx[0], x),
|
||
id_detail::dof_val(idx[1], x),
|
||
id_detail::dof_val(idx[2], x)
|
||
};
|
||
|
||
for (int j = 0; j < 3; ++j) {
|
||
if (idx[j] < 0) continue; // never perturb a pinned DOF
|
||
|
||
double vp[3], vm[3];
|
||
for (int k = 0; k < 3; ++k) { vp[k] = vm[k] = vals[k]; }
|
||
vp[j] += eps;
|
||
vm[j] -= eps;
|
||
|
||
auto Cp = inversive_distance_face_grad_contribs(
|
||
vp[0], vp[1], vp[2], I12, I23, I31);
|
||
auto Cm = inversive_distance_face_grad_contribs(
|
||
vm[0], vm[1], vm[2], I12, I23, I31);
|
||
|
||
const double Gp[3] = { Cp.g1, Cp.g2, Cp.g3 };
|
||
const double Gm[3] = { Cm.g1, Cm.g2, Cm.g3 };
|
||
|
||
for (int i = 0; i < 3; ++i) {
|
||
if (idx[i] < 0) continue; // pinned: contributes nothing
|
||
const double val = (Gp[i] - Gm[i]) / (2.0 * eps);
|
||
if (std::abs(val) > 1e-15)
|
||
trips.emplace_back(idx[i], idx[j], val);
|
||
}
|
||
}
|
||
}
|
||
|
||
Eigen::SparseMatrix<double> H(n, n);
|
||
H.setFromTriplets(trips.begin(), trips.end());
|
||
return H;
|
||
}
|
||
|
||
/// Symmetrised block-FD Inversive-Distance Hessian: `(H + Hᵀ)/2` of
|
||
/// `inversive_distance_hessian_block_fd(...)` for solvers requiring strict
|
||
/// symmetry.
|
||
inline Eigen::SparseMatrix<double> inversive_distance_hessian_block_fd_sym(
|
||
const ConformalMesh& mesh,
|
||
const std::vector<double>& x,
|
||
const InversiveDistanceMaps& m,
|
||
double eps = 1e-5)
|
||
{
|
||
auto H = inversive_distance_hessian_block_fd(mesh, x, m, eps);
|
||
Eigen::SparseMatrix<double> Ht = H.transpose();
|
||
return (H + Ht) * 0.5;
|
||
}
|
||
|
||
} // namespace conformallab
|