#pragma once // Copyright (c) 2024-2026 Tarik Moussa. // SPDX-License-Identifier: MIT // inversive_distance_functional.hpp // // Phase 9a.2 — Inversive-distance circle-packing functional (Luo 2004). // // VERTEX-based circle packing. Each vertex carries a circle of radius // r_i = exp(u_i). The inversive distance I_ij between two adjacent // circles is a constant of the edge, derived once from the initial // geometry via Bowers-Stephenson 2004. // // This is the FACE-DUAL of CPEuclideanFunctional (Phase 9a.1). The // correspondence is I_ij = cos θ_e (Glickenstein 2011 §5). // // ┌──────────────────────────────────────────────────────────────────────────┐ // │ Mathematical model │ // │ ────────────────── │ // │ │ // │ Variables: u_i = log r_i (per vertex; r_i is the radius) │ // │ Constants: I_ij (per edge; inversive distance) │ // │ Θ_v (per vertex; target cone angle) │ // │ │ // │ Bowers-Stephenson (init from initial geometry): │ // │ I_ij = ( ℓ_ij² − r_i² − r_j² ) / ( 2 r_i r_j ) │ // │ │ // │ Edge length (Luo 2004 §3, Glickenstein 2011 eq. 2.1): │ // │ ℓ_ij(u)² = exp(2 u_i) + exp(2 u_j) + 2 I_ij exp(u_i + u_j) │ // │ = r_i² + r_j² + 2 I_ij r_i r_j │ // │ │ // │ Triangle angles: same half-tangent law of cosines as the │ // │ Euclidean functional (numerically stable). │ // │ │ // │ Gradient (Luo 2004 Lemma 3.1): │ // │ ∂E/∂u_v = Θ_v − Σ_{T ∋ v} α_v(T) │ // │ │ // │ Energy: path integral E(u) = ∫₀¹ ⟨G(tu), u⟩ dt │ // │ (Luo's 1-form is closed; we use 10-point Gauss-Legendre │ // │ quadrature, identical to euclidean_functional.hpp) │ // │ │ // │ Hessian: finite-difference for the MVP port; an analytic form is │ // │ given in Glickenstein 2011 eq. (4.6) and may be added │ // │ later for performance. │ // └──────────────────────────────────────────────────────────────────────────┘ // // Relation to euclidean_functional.hpp // ──────────────────────────────────── // The two are structurally identical in: // • DOF layout (per vertex), DOF index sentinel (−1 = pinned) // • Gradient pattern (Θ − Σ α) // • Energy via path integral (same Gauss-Legendre constants) // • Halfedge convention (h0/h1/h2, source pattern, α opposite-edge) // // They differ ONLY in: // • Per-edge constant: λ°_ij (log²-length) vs I_ij (inversive distance) // • Edge-length formula: // Euclidean: ℓ_ij = exp((λ°_ij + u_i + u_j) / 2) // Inversive distance: ℓ_ij² = exp(2u_i) + exp(2u_j) // + 2 I_ij exp(u_i + u_j) // // In particular at the tangential limit I_ij = 1 the inversive-distance length // reduces to (exp(u_i) + exp(u_j))² ⇒ ℓ_ij = r_i + r_j (tangential circles), // which is *different* from the Euclidean-conformal length even at the same // initial geometry. The two functionals describe distinct geometric objects. // // Property-map name prefix: "iv:" (vertex) and "ie:" (edge). #include "conformal_mesh.hpp" #include "constants.hpp" #include "euclidean_geometry.hpp" // euclidean_angles(λ12, λ23, λ31) #include #include #include #include #include namespace conformallab { // ── Property-map type aliases ──────────────────────────────────────────────── /// Property map vertex → `int` for the Inversive-Distance functional. using IDVMapI = ConformalMesh::Property_map; /// Property map vertex → `double` for the Inversive-Distance functional. using IDVMapD = ConformalMesh::Property_map; /// Property map edge → `double` for the Inversive-Distance functional. using IDEMapD = ConformalMesh::Property_map; // ── Persistent map bundle ───────────────────────────────────────────────────── /// Bundle of the four property maps consumed by the Inversive-Distance /// circle-packing functional (Luo 2004 / Bowers-Stephenson 2004). struct InversiveDistanceMaps { IDVMapI v_idx; ///< DOF index per vertex (−1 = pinned / u_v = 0) IDVMapD theta_v; ///< target cone angle Θ_v (default 2π) IDVMapD r0; ///< initial radius r_i^(0) (default 1) IDEMapD I_e; ///< inversive distance I_ij (per edge, constant) }; /// Attach the four inversive-distance property maps to `mesh` and /// return their handles. /// /// Defaults are intentionally trivial — every real use of this /// functional must call `compute_inversive_distance_init_from_mesh()` /// next to populate `r0` and `I_e` from the input geometry. /// * `v_idx[v] = -1` (all vertices pinned initially) /// * `theta_v[v] = 2π` (regular interior vertex) /// * `r0[v] = 1.0` (placeholder) /// * `I_e[e] = 1.0` (tangential default — overwritten by init step) /// /// The maps use the `"iv:"` / `"ie:"` prefix so they do not collide /// with the Euclidean / Spherical / HyperIdeal / CP-Euclidean maps. inline InversiveDistanceMaps setup_inversive_distance_maps(ConformalMesh& mesh) { InversiveDistanceMaps m; m.v_idx = mesh.add_property_map ("iv:idx", -1 ).first; m.theta_v = mesh.add_property_map("iv:theta", TWO_PI ).first; m.r0 = mesh.add_property_map("iv:r0", 1.0 ).first; m.I_e = mesh.add_property_map("ie:I", 1.0 ).first; return m; } /// Assign sequential DOF indices `0..n-1` to every vertex. /// /// **Note:** this overload does NOT pin a gauge vertex. The caller /// is expected to either: /// 1. set one `m.v_idx[v] = -1` *before* calling this function (then /// the call is a no-op for that vertex) — OR — /// 2. flip one assigned index back to `-1` *after* this function. /// /// For a closed mesh, exactly one pin is required to remove the /// global rotational mode. inline int assign_inversive_distance_vertex_dof_indices(ConformalMesh& mesh, InversiveDistanceMaps& m) { int idx = 0; for (auto v : mesh.vertices()) m.v_idx[v] = idx++; return idx; } /// Count the free DOFs (vertices with `v_idx >= 0`). inline int inversive_distance_dimension(const ConformalMesh& mesh, const InversiveDistanceMaps& m) { int dim = 0; for (auto v : mesh.vertices()) if (m.v_idx[v] >= 0) ++dim; return dim; } /// Two-phase initialisation from initial mesh geometry. Mirrors the /// role of `compute_lambda0_from_mesh` in the Euclidean functional, but /// adapted to Luo's vertex-based radius parametrisation. /// /// **Phase 1.** Pick a positive radius per vertex: /// \code /// r_i^(0) = (1/3) · min{ℓ_e : e adjacent to v_i} /// \endcode /// This is a heuristic — the user may override `m.r0[v]` for any /// vertex between `setup_inversive_distance_maps()` and this call. /// /// **Phase 2.** Compute the per-edge inversive distance via the /// Bowers-Stephenson 2004 identity: /// \code /// I_ij = ( ℓ_ij² − r_i² − r_j² ) / ( 2 r_i r_j ) /// \endcode /// /// \pre Every edge has positive 3-D length. /// \pre Radii produced in Phase 1 are positive (degenerate isolated /// vertices fall back to `r_i = 1`). /// \post Every `I_e[e] > -1` for a valid packing. The chosen /// Phase-1 heuristic keeps `I_e > 0` for most real meshes. inline void compute_inversive_distance_init_from_mesh(ConformalMesh& mesh, InversiveDistanceMaps& m) { // Phase 1: r_i = (1/3) · min adjacent edge length. for (auto v : mesh.vertices()) { double min_len = std::numeric_limits::infinity(); for (auto h : CGAL::halfedges_around_target(v, mesh)) { auto p1 = mesh.point(mesh.source(h)); auto p2 = mesh.point(mesh.target(h)); double dx = p1.x() - p2.x(); double dy = p1.y() - p2.y(); double dz = p1.z() - p2.z(); double len = std::sqrt(dx*dx + dy*dy + dz*dz); if (len < min_len) min_len = len; } m.r0[v] = (std::isfinite(min_len) && min_len > 1e-15) ? min_len / 3.0 : 1.0; } // Phase 2: I_ij from initial geometry. for (auto e : mesh.edges()) { auto h = mesh.halfedge(e); auto vi = mesh.source(h); auto vj = mesh.target(h); auto p1 = mesh.point(vi); auto p2 = mesh.point(vj); double dx = p1.x() - p2.x(); double dy = p1.y() - p2.y(); double dz = p1.z() - p2.z(); double l2 = dx*dx + dy*dy + dz*dz; double ri = m.r0[vi]; double rj = m.r0[vj]; m.I_e[e] = (l2 - ri*ri - rj*rj) / (2.0 * ri * rj); } } // ── Internal helpers ────────────────────────────────────────────────────────── namespace id_detail { inline double dof_val(int idx, const std::vector& x) noexcept { return idx >= 0 ? x[static_cast(idx)] : 0.0; } inline std::size_t hidx(Halfedge_index h) noexcept { return static_cast(static_cast(h)); } // Inversive-distance edge length squared: ℓ² = exp(2u_i) + exp(2u_j) + 2 I r_i r_j // where r_i = exp(u_i), so: ℓ² = r_i² + r_j² + 2 I r_i r_j. // Returns -1 if the result is non-positive (degenerate; the caller skips the face). inline double edge_length_squared(double u_i, double u_j, double I_ij) noexcept { double ri = std::exp(u_i); double rj = std::exp(u_j); double l2 = ri*ri + rj*rj + 2.0 * I_ij * ri * rj; return l2 > 0.0 ? l2 : -1.0; } } // namespace id_detail /// Inversive-Distance gradient `G_v = Θ_v − Σ_faces α_v(face)`. Same /// half-edge corner-angle storage convention as `euclidean_gradient`. inline std::vector inversive_distance_gradient( const ConformalMesh& mesh, const std::vector& x, const InversiveDistanceMaps& m) { const int n = inversive_distance_dimension(mesh, m); std::vector G(static_cast(n), 0.0); const std::size_t nh = mesh.number_of_halfedges(); std::vector h_alpha(nh, 0.0); // Pass 1 — per face, compute corner angles via the law of cosines. // We reuse euclidean_angles(λ12, λ23, λ31) which takes 2·log(ℓ) per edge. 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); double u1 = id_detail::dof_val(m.v_idx[v1], x); double u2 = id_detail::dof_val(m.v_idx[v2], x); double u3 = id_detail::dof_val(m.v_idx[v3], x); double l12sq = id_detail::edge_length_squared(u1, u2, m.I_e[e12]); double l23sq = id_detail::edge_length_squared(u2, u3, m.I_e[e23]); double l31sq = id_detail::edge_length_squared(u3, u1, m.I_e[e31]); if (l12sq <= 0 || l23sq <= 0 || l31sq <= 0) continue; // euclidean_angles expects 2·log(ℓ) per edge — feed log(ℓ²). auto fa = euclidean_angles(std::log(l12sq), std::log(l23sq), std::log(l31sq)); if (!fa.valid) continue; h_alpha[id_detail::hidx(h0)] = fa.alpha3; h_alpha[id_detail::hidx(h1)] = fa.alpha1; h_alpha[id_detail::hidx(h2)] = fa.alpha2; } // Pass 2 — accumulate vertex gradient. for (auto v : mesh.vertices()) { int iv = m.v_idx[v]; if (iv < 0) continue; double sum_alpha = 0.0; for (auto h : CGAL::halfedges_around_target(v, mesh)) { if (mesh.is_border(h)) continue; sum_alpha += h_alpha[id_detail::hidx(mesh.prev(h))]; } G[static_cast(iv)] = m.theta_v[v] - sum_alpha; } return G; } /// 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( const ConformalMesh& mesh, const std::vector& x, const InversiveDistanceMaps& m) { static const double gl_s[10] = { -0.9739065285171717, -0.8650633666889845, -0.6794095682990244, -0.4333953941292472, -0.1488743389816312, 0.1488743389816312, 0.4333953941292472, 0.6794095682990244, 0.8650633666889845, 0.9739065285171717 }; static const double gl_w[10] = { 0.0666713443086881, 0.1494513491505806, 0.2190863625159820, 0.2692667193099963, 0.2955242247147529, 0.2955242247147529, 0.2692667193099963, 0.2190863625159820, 0.1494513491505806, 0.0666713443086881 }; const std::size_t n = x.size(); double E = 0.0; std::vector tx(n); for (int k = 0; k < 10; ++k) { double t = (1.0 + gl_s[k]) * 0.5; double wt = gl_w[k] * 0.5; for (std::size_t i = 0; i < n; ++i) tx[i] = t * x[i]; auto G = inversive_distance_gradient(mesh, tx, m); double dot = 0.0; for (std::size_t i = 0; i < n; ++i) dot += G[i] * x[i]; E += wt * dot; } return E; } /// FD gradient check for the Inversive-Distance functional (central diff). inline bool gradient_check_inversive_distance( const ConformalMesh& mesh, const std::vector& x, const InversiveDistanceMaps& m, double eps = 1e-5, double tol = 1e-6) { auto G = inversive_distance_gradient(mesh, x, m); const std::size_t n = G.size(); for (std::size_t i = 0; i < n; ++i) { std::vector xp = x, xm = x; xp[i] += eps; xm[i] -= eps; double Ep = inversive_distance_energy(mesh, xp, m); double Em = inversive_distance_energy(mesh, xm, m); double fd = (Ep - Em) / (2.0 * eps); if (std::abs(G[i] - fd) > tol) { std::cerr << "[inversive-distance] FD gradient mismatch at DOF " << i << ": analytic=" << G[i] << " FD=" << fd << " diff=" << (G[i] - fd) << "\n"; return false; } } return true; } /// Newton equilibrium check: returns `true` iff the gradient at `x` /// is below `tol` in infinity norm (Σ adj-face angles equal Θ_v). inline bool is_inversive_distance_equilibrium( const ConformalMesh& mesh, const std::vector& x, const InversiveDistanceMaps& m, double tol = 1e-8) { auto G = inversive_distance_gradient(mesh, x, m); for (double g : G) if (std::abs(g) > tol) return false; return true; } } // namespace conformallab