Files
ConformalLabpp/code/include/inversive_distance_hessian.hpp
Tarik Moussa b29eb4e0c4 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>
2026-05-31 16:03:12 +02:00

188 lines
8.4 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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