Files
ConformalLabpp/doc/tutorials/add-inversive-distance.md
Tarik Moussa 48e6d2131e docs: split Phase 9a into 9a.1 (CPEuclidean) + 9a.2 (InversiveDistance)
Audit during Phase 9a preparation revealed:

* The Java repo `varylab/conformallab` does NOT contain
  `InversiveDistanceFunctional.java`.  The original roadmap mention
  was based on a misreading.
* The closest existing Java class is `CPEuclideanFunctional.java`
  (260 lines, with FunctionalTest), implementing the Bobenko-Pinkall-
  Springborn 2010 face-based circle-packing functional.

These two functionals are mathematically distinct (face-dual vs vertex-
based) but related: BPS-CP generalises inversive-distance via the
intersection angle parameter θ_e (I_ij = cos θ_e for orthogonal
limit, I_ij = 1 for tangential).

The roadmap is now split into:
- 9a.1  CPEuclideanFunctional  (Java port + test, BPS 2010 reference)
- 9a.2  InversiveDistanceFunctional  (from-literature, Luo 2004
        + Glickenstein 2011)

Both belong in Phase 9a; cross-validation between them in the
tangential limit (θ=0 ⇔ I=1) becomes a Phase 9a acceptance test.

The tutorial `doc/tutorials/add-inversive-distance.md` is corrected:
it no longer claims `InversiveDistanceFunctional.java` exists upstream,
and cites Luo 2004 + Glickenstein 2011 + Bowers-Stephenson 2004 instead.
Updated edge-length formula from incorrect hyperbolic cosh form to
the correct Luo §3 Euclidean form:
  ℓ_ij² = exp(2u_i) + exp(2u_j) + 2 I_ij exp(u_i + u_j)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 23:18:57 +02:00

7.8 KiB
Raw Blame History

Tutorial: Porting the Inversive-Distance Functional (Phase 9a.2)

This is a complete, step-by-step example of how to add a new functional to conformallab++.

Note: This tutorial implements the vertex-based inversive-distance circle packing of Luo (2004) and Glickenstein (2011). Despite an earlier draft of this document, the Java reference repository varylab/conformallab does not contain an InversiveDistanceFunctional.java. This is a from-the-literature implementation, validated by cross-checking against the face-based circle-packing functional CPEuclideanFunctional.java (Bobenko-Pinkall-Springborn 2010, see Phase 9a.1) in the tangential limit I_ij = 1θ_e = 0.

Prerequisite: Read doc/api/extending.md first for the general pattern. This tutorial fills in every detail for one specific case.


Mathematical background

Inversive distance circle packing parametrises each vertex by a circle of radius r_i = exp(u_i). For each edge, the inversive distance I_ij between the two adjacent circles is a fixed constant of the edge, computed once from the initial geometry:

I_ij = (_ij²  r_i²  r_j²) / (2 r_i r_j)                       (Bowers-Stephenson)
Range of I_ij Geometric meaning
I_ij = 1 Tangential circles (Koebe-style)
I_ij = cos φ ∈ (0,1) Overlapping circles with intersection angle φ
I_ij = 0 Orthogonal circles
I_ij ∈ (1, 0) Disjoint circles (inversive-distance > 1)

The edge length is then determined by the radii via:

_ij(u)² = exp(2 u_i) + exp(2 u_j) + 2 I_ij exp(u_i + u_j)        (Luo 2004 §3)

The angle formula is the same law of cosines (numerically stable half-tangent form) as the Euclidean functional — only the edge-length mapping changes. The gradient

∂E/∂u_i = Θ_i  Σ_{T ∋ i} α_i(T)

is the standard Yamabe-flow gradient (Luo 2004 Lemma 3.1).

Primary references:

  • Luo, F. (2004). Combinatorial Yamabe Flow on Surfaces. Communications in Contemporary Mathematics, 6(5), 765780.
  • Bowers, P. L. & Stephenson, K. (2004). Uniformizing dessins and Belyĭ maps via circle packing. Memoirs of the AMS 170(805).
  • Glickenstein, D. (2011). Discrete conformal variations and scalar curvature on piecewise flat manifolds. J. Differential Geometry 87(2), 201238 (analytic Hessian, eq. 4.6).

Java source: none — this functional is implemented from the above papers. See doc/roadmap/phases.md §9a for the historical clarification.


Step 1 — Create the header

cp code/include/euclidean_functional.hpp \
   code/include/inversive_distance_functional.hpp

Edit the new file:

#pragma once
// inversive_distance_functional.hpp
//
// Phase 9a — Inversive-distance discrete conformal functional (Luo 2004).
// Ported from de.varylab.discreteconformal.functional.InversiveDistanceFunctional.
//
// Usage: identical to euclidean_functional.hpp — replace lambda0 with
// inversive_distance0 (the initial inversive distances per edge).

#include "conformal_mesh.hpp"
#include <Eigen/Dense>
#include <cmath>

namespace conformallab {

struct InversiveDistanceMaps {
    CGAL::Surface_mesh<CGAL::Simple_cartesian<double>::Point_3>
        ::Property_map<Edge_index, double>   inv_dist0; ///< I_{ij} per edge
    CGAL::Surface_mesh<CGAL::Simple_cartesian<double>::Point_3>
        ::Property_map<Vertex_index, double> theta_v;   ///< target corner angles Θ_v
    CGAL::Surface_mesh<CGAL::Simple_cartesian<double>::Point_3>
        ::Property_map<Vertex_index, int>    v_idx;     ///< DOF index (-1 = pinned)
};

// ... (follow euclidean_functional.hpp exactly, replacing lambda0 with inv_dist0
//      and the angle formula with the Luo (2004) version)

Step 2 — Implement the energy, gradient, and angle formula

The Euclidean angle formula uses the law of cosines on edge lengths derived from log-lengths. For inversive distance, the edge lengths are derived from inversive distances I_{ij} and the u-vector:

// Euclidean (reference):
//   lambda_ij = lambda0_ij + u_i + u_j
//   l_ij      = exp(lambda_ij / 2)

// Inversive distance (Luo 2004):
//   cosh(l̃_ij) = I_ij * cosh((u_i + u_j) / 2)   [simplified form]
//   l̃_ij       = acosh(I_ij * cosh((u_i + u_j) / 2))

Then the corner angle at vertex k opposite edge {i,j} in triangle {i,j,k} is computed by the standard law of cosines from l̃_{ij}, l̃_{jk}, l̃_{ki}.

The gradient is the same as Euclidean:

G_v = Σ_{faces containing v} alpha_v(face, u)    Theta_v

Step 3 — Write the gradient-check test

Create code/tests/cgal/test_inversive_distance.cpp:

#include "inversive_distance_functional.hpp"
#include "mesh_factory.hpp"
#include <gtest/gtest.h>

// Finite-difference gradient check: copy the pattern from
// test_euclidean_functional.cpp :: EuclideanFunctional.GradientCheck_Triangle
TEST(InversiveDistance, GradientCheck_Triangle)
{
    // Build a single equilateral triangle (open mesh, no boundary issues).
    ConformalMesh mesh = MeshFactory::make_open_mesh(MeshFactory::Kind::triangle);
    InversiveDistanceMaps maps = setup_inversive_distance_maps(mesh);
    compute_inversive_distance0_from_mesh(mesh, maps);  // I_ij from 3D edge lengths

    const int n = /* count free DOFs */;
    std::vector<double> x0(n, 0.0);
    constexpr double eps = 1e-5;

    auto G  = inversive_distance_gradient(mesh, x0, maps);

    for (int i = 0; i < n; ++i) {
        auto xp = x0; xp[i] += eps;
        auto xm = x0; xm[i] -= eps;
        double Ep = inversive_distance_energy(mesh, xp, maps);
        double Em = inversive_distance_energy(mesh, xm, maps);
        double fd = (Ep - Em) / (2.0 * eps);
        EXPECT_NEAR(G[i], fd, 1e-6) << "gradient mismatch at DOF " << i;
    }
}

Run with:

./build/conformallab_cgal_tests --gtest_filter="InversiveDistance.*"

Step 4 — Register in CMakeLists

Add to code/tests/cgal/CMakeLists.txt inside the add_executable block:

    # ── Phase 9a: Inversive-distance functional ────────────────────────────────
    test_inversive_distance.cpp

Step 5 — Add a Newton wrapper

In code/include/newton_solver.hpp add:

/// Solve the inversive-distance conformal problem.
/// \see newton_euclidean() — identical structure.
inline NewtonResult newton_inversive_distance(
    ConformalMesh&                  mesh,
    std::vector<double>             x0,
    const InversiveDistanceMaps&    m,
    double                          tol      = 1e-8,
    int                             max_iter = 200)
{
    // Copy newton_euclidean() exactly, replacing euclidean_* with
    // inversive_distance_*.  The Hessian is PSD (Luo 2004, Thm. 1),
    // so SimplicialLDLT with SparseQR fallback applies directly.
}

Step 6 — Verify against Java output

The Java library outputs text results for a triangulated torus. To compare:

  1. Run Java ConformalLab on the same OFF mesh with Inversive-Distance mode.
  2. Record the final gradient norm and angle sums.
  3. Run the C++ equivalent and check:
EXPECT_LT(res.grad_inf_norm, 1e-8);
EXPECT_LT(res.iterations, 50);  // inversive-distance converges faster than hyper-ideal

Java reference: InversiveDistanceFunctionalTest.java in de.varylab.discreteconformal.test


Checklist

  • code/include/inversive_distance_functional.hpp compiles
  • GradientCheck_Triangle passes
  • GradientCheck_OpenMesh passes (copy from Euclidean tests)
  • Newton converges on torus_4x4.off
  • Result matches Java output (gradient norm < 1e-8 on same mesh)
  • Registered in CMakeLists.txt
  • doc/roadmap/java-parity.md updated: Phase 9a →