#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 #include #include 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 inversive_distance_hessian( const ConformalMesh& mesh, const std::vector& x, const InversiveDistanceMaps& m, double eps = 1e-5) { const int n = inversive_distance_dimension(mesh, m); std::vector> trips; trips.reserve(static_cast(n) * 16); std::vector xp = x, xm = x; for (int j = 0; j < n; ++j) { const std::size_t sj = static_cast(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(i)] - Gm[static_cast(i)]) / (2.0 * eps); if (std::abs(val) > 1e-15) trips.emplace_back(i, j, val); } } Eigen::SparseMatrix H(n, n); H.setFromTriplets(trips.begin(), trips.end()); return H; } /// Symmetrised full-FD Inversive-Distance Hessian: `(H + Hᵀ)/2`. inline Eigen::SparseMatrix inversive_distance_hessian_sym( const ConformalMesh& mesh, const std::vector& x, const InversiveDistanceMaps& m, double eps = 1e-5) { auto H = inversive_distance_hessian(mesh, x, m, eps); Eigen::SparseMatrix 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 inversive_distance_hessian_block_fd( const ConformalMesh& mesh, const std::vector& x, const InversiveDistanceMaps& m, double eps = 1e-5) { const int n = inversive_distance_dimension(mesh, m); std::vector> 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 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 inversive_distance_hessian_block_fd_sym( const ConformalMesh& mesh, const std::vector& x, const InversiveDistanceMaps& m, double eps = 1e-5) { auto H = inversive_distance_hessian_block_fd(mesh, x, m, eps); Eigen::SparseMatrix Ht = H.transpose(); return (H + Ht) * 0.5; } } // namespace conformallab