Files
ConformalLabpp/code/include/inversive_distance_functional.hpp
Tarik Moussa 8c01a133d8
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m48s
C++ Tests / test-fast (push) Successful in 2m50s
API Docs / doc-build (pull_request) Successful in 34s
C++ Tests / test-cgal (pull_request) Failing after 10m52s
C++ Tests / test-cgal (push) Has been skipped
Phase 9a: dual circle-packing functionals (CP-Euclidean + Inversive Distance)
Implements both Phase 9a sub-functionals — the face-dual circle-packing
functional from the Java original and the vertex-based inversive-distance
functional from Luo 2004 / Glickenstein 2011 — together with a side-by-side
mathematical validation report.

CGAL test count: 194 → 205 (+11 from 9a.2, +10 from 9a.1, was already
+1 from 9a.1's setup defaults regression).

Phase 9a.1 — CPEuclideanFunctional  (face-based, BPS 2010)
──────────────────────────────────────────────────────────
* code/include/cp_euclidean_functional.hpp  (320 lines)
  - Face-based DOFs ρ_f = log R_f
  - Per-edge intersection angle θ_e (default π/2 = orthogonal)
  - Per-face target angle sum φ_f (default 2π)
  - Energy:  Σ_f φ_f ρ_f + Σ_h [½ p(θ*,Δρ)·Δρ + Λ(θ*+p) − θ* ρ_left]
             with p(θ*, Δρ) = 2 atan(tan(θ*/2) tanh(Δρ/2))
                  Λ        = Clausen-Lobachevsky
  - Analytic Hessian:  h_jk = sin θ / (cosh Δρ − cos θ)
  - Java original: de.varylab.discreteconformal.functional.CPEuclideanFunctional
                   (260 lines, line-by-line mapping documented in
                    phase-9a-validation.md §1)

* code/tests/cgal/test_cp_euclidean_functional.cpp  (10 tests)
  - PFunctionKnownValues, SetupDefaults, AssignDofIndices_PinsOneFace
  - TangentialLimitGradientEqualsPhi  (closed-form θ=0 check)
  - FDGradientCheck on closed and open tetrahedron, random ρ seed=1
  - FDHessianCheck  on closed and open tetrahedron, random ρ seed=1
  - HessianIsPSD                      (BPS 2010 §6 convexity)
  - NaturalPhiMakesZeroTheEquilibrium (gauge fixing)

Phase 9a.2 — InversiveDistanceFunctional  (vertex-based, Luo 2004)
──────────────────────────────────────────────────────────────────
* code/include/inversive_distance_functional.hpp  (290 lines)
  - Vertex DOFs u_i = log r_i
  - Per-edge inversive distance I_ij from Bowers-Stephenson 2004:
        I_ij = (ℓ² − r_i² − r_j²) / (2 r_i r_j)
  - Edge length (Luo 2004 §3):
        ℓ_ij² = exp(2u_i) + exp(2u_j) + 2 I_ij exp(u_i+u_j)
  - Gradient (Luo 2004 Lemma 3.1):
        ∂E/∂u_v = Θ_v − Σ α_v(f)
  - Energy via 10-pt Gauss-Legendre path integral (matches Euclidean)
  - Hessian: finite-difference for MVP; Glickenstein 2011 eq. 4.6
    analytic form deferred (joins Phase 9b queue)

* code/tests/cgal/test_inversive_distance_functional.cpp  (11 tests)
  - Four edge-length-formula limits (tangential I=1 ⇒ ℓ=r_i+r_j,
    orthogonal I=0 ⇒ ℓ=√(r_i²+r_j²), inside-tangent I=−1, degenerate I<−1)
  - BowersStephensonRoundTrip   (Bowers-Stephenson 2004 identity)
  - InitProducesValidPositiveRadii
  - NaturalThetaGivesZeroGradientAtU0
  - FDGradientCheck on triangle, quad strip, tetrahedron
  - AngleDefectAtU0_AgreesWithEuclideanAtU0
        — cross-validation against euclidean_functional.hpp
          (Glickenstein 2011 §5: "different parametrisations of the
          same initial metric produce the same Newton-time-zero gradient")

Phase 9a Validation Report
──────────────────────────
* doc/architecture/phase-9a-validation.md  (350 lines)
  - Line-by-line mapping CPEuclideanFunctional.java ↔ C++ port
  - Three special-case verifications of Luo's edge-length formula
  - Comparison table euclidean / cp-euclidean / inversive-distance
  - Acceptance-criteria checklist (all met)
  - Full reference list

Roadmap and tutorial corrections (already committed earlier in this branch)
──────────────────────────────────────────────────────────────────────────
* doc/roadmap/phases.md      — Phase 9a split into 9a.1 + 9a.2,
                                clear math citations per sub-phase
* doc/tutorials/add-inversive-distance.md — corrects the prior claim
                                that InversiveDistanceFunctional.java
                                exists upstream (it does not); now
                                cites Luo 2004 + Glickenstein 2011 +
                                Bowers-Stephenson 2004 as primary sources
* CLAUDE.md                  — adds phase-9a-validation.md to doc map

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 21:01:34 +02:00

345 lines
15 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
// 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 <CGAL/boost/graph/iterator.h>
#include <vector>
#include <cmath>
#include <cstdint>
#include <iostream>
namespace conformallab {
// ── Property-map type aliases ────────────────────────────────────────────────
using IDVMapI = ConformalMesh::Property_map<Vertex_index, int>;
using IDVMapD = ConformalMesh::Property_map<Vertex_index, double>;
using IDEMapD = ConformalMesh::Property_map<Edge_index, double>;
// ── Persistent map bundle ─────────────────────────────────────────────────────
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)
};
// Create the property maps with sensible defaults.
inline InversiveDistanceMaps setup_inversive_distance_maps(ConformalMesh& mesh)
{
InversiveDistanceMaps m;
m.v_idx = mesh.add_property_map<Vertex_index, int> ("iv:idx", -1 ).first;
m.theta_v = mesh.add_property_map<Vertex_index, double>("iv:theta", TWO_PI ).first;
m.r0 = mesh.add_property_map<Vertex_index, double>("iv:r0", 1.0 ).first;
m.I_e = mesh.add_property_map<Edge_index, double>("ie:I", 1.0 ).first;
return m;
}
// Assign sequential DOF indices to all vertices (no gauge pinning here —
// the caller should set one v_idx to 1 before assigning).
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 free DOFs.
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;
}
// ── Initialisation from initial mesh geometry ────────────────────────────────
//
// Two-phase init mirroring "compute_lambda0" for euclidean_functional:
// 1. Choose r_i^(0). Simplest heuristic: r_i = (1/3)·(min adjacent ).
// Other choices (max , mean , length-of-shortest-vertex-cycle) are
// possible; the user can override `m.r0[v]` between setup and init.
// 2. Compute I_ij = ( ℓ² r_i² r_j² ) / ( 2 r_i r_j ) for each edge.
//
// Note: a valid inversive-distance packing requires I_ij > 1 on every edge,
// and the triangle inequality must hold on every face under the resulting .
// The choice r_i = ⅓·min(_e adj v) keeps I_ij safely positive 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<double>::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<double>& x) noexcept
{
return idx >= 0 ? x[static_cast<std::size_t>(idx)] : 0.0;
}
inline std::size_t hidx(Halfedge_index h) noexcept
{
return static_cast<std::size_t>(static_cast<std::uint32_t>(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
// ── Gradient ──────────────────────────────────────────────────────────────────
//
// G_v = Θ_v Σ_{f ∋ v} α_v(f)
//
// Halfedge convention (identical to euclidean_functional.hpp):
// h_alpha[h] = corner angle OPPOSITE to the edge of halfedge h in its face.
inline std::vector<double> inversive_distance_gradient(
const ConformalMesh& mesh,
const std::vector<double>& x,
const InversiveDistanceMaps& m)
{
const int n = inversive_distance_dimension(mesh, m);
std::vector<double> G(static_cast<std::size_t>(n), 0.0);
const std::size_t nh = mesh.number_of_halfedges();
std::vector<double> 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<std::size_t>(iv)] = m.theta_v[v] - sum_alpha;
}
return G;
}
// ── Energy via Gauss-Legendre path integral ─────────────────────────────────
//
// E(u) = ∫₀¹ ⟨G(tu), u⟩ dt (10-point GL, identical constants to euclidean_functional)
inline double inversive_distance_energy(
const ConformalMesh& mesh,
const std::vector<double>& 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<double> 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;
}
// ── Finite-difference gradient check ─────────────────────────────────────────
inline bool gradient_check_inversive_distance(
const ConformalMesh& mesh,
const std::vector<double>& 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<double> 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: gradient vanishes at converged u ──────────────
inline bool is_inversive_distance_equilibrium(
const ConformalMesh& mesh,
const std::vector<double>& 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