Files
ConformalLabpp/doc/tutorials/add-inversive-distance.md
Tarik Moussa 4f0a3035e4
Some checks failed
C++ Tests / test-fast (push) Successful in 2m50s
C++ Tests / test-fast (pull_request) Successful in 2m36s
API Docs / doc-build (pull_request) Successful in 1m10s
C++ Tests / test-cgal (push) Has been skipped
C++ Tests / test-cgal (pull_request) Failing after 10m25s
docs: full audit — fix 4 wrong port/research labels + consolidated research-track
A full audit of `doc/` plus root-level markdown files (27 files) against
the actual ground truth in the C++ code and the local Java repository at
`/Users/tarikmoussa/Desktop/conformallab/` revealed four pre-existing
mis-labels and a stale test count.  All are corrected here.

Audit findings — corrected
─────────────────────────

1. **`InversiveDistanceFunctional` mis-labelled as Java port** (4 doc sites)
   Empirical verification:
       find /Users/tarikmoussa/Desktop/conformallab -iname "*nversive*"
       (zero matches)
   The class does NOT exist in `de.varylab.discreteconformal`.  The C++
   implementation is built from Luo 2004 + Glickenstein 2011 + Bowers-
   Stephenson 2004 — new research, not a port.
   Fixed in: java-parity.md, references.md, add-inversive-distance.md.

2. **HyperIdeal Hessian mis-labelled as "Java has analytic Hessian"**
   Empirical verification: `HyperIdealFunctional.java:295-298`:
       public boolean hasHessian() { return false; }
   Java has NO Hessian at all.  Both the FD (Phase 4a) and the block-FD
   (Phase 9b) Hessians in C++ are research beyond the Java port.  The
   chain rule (b,a) → ℓ → ζ → α/β is the *mathematical formulation*
   from Springborn 2020, not something Java implements.
   Fixed in: java-parity.md.

3. **Stale test count** README:87 said "28 suites, 170 tests" — current
   actual is 35 suites, 176 CGAL + 36 non-CGAL.  Fixed.

4. **Tutorial framing** — `add-inversive-distance.md` was framed as
   "porting an InversiveDistanceFunctional.java" that does not exist.
   Rewritten as "Implementing the Inversive-Distance functional from
   Luo 2004" with prominent verification block at top.

New document: `doc/roadmap/research-track.md`
─────────────────────────────────────────────

Consolidates everything in conformallab++ that goes beyond a Java port:

* Items already on `main`: HyperIdeal FD Hessian, period matrix τ
  partial-research components, Möbius holonomy storage.
* Items on open PRs: CP-Euclidean (PR #8, port), Inversive-Distance
  (PR #8, research), block-FD Hessian (PR #9, research).
* Planned research with full citations:
  - **Phase 9b-analytic** — full analytic HyperIdeal Hessian via
    Schläfli identity (Schläfli 1858/60) and chain rule through
    ζ₁₃/ζ₁₄/ζ₁₅, citing Springborn 2020 §4, Cho-Kim 1999,
    Glickenstein 2011 §4.  Includes acceptance-criteria checklist
    (per-case derivative cross-checks, gauge null space, PSD,
    measured ≥ 3× speed-up, LaTeX correctness note).
  - **Phase 9a.2-analytic** — analytic inversive-distance Hessian
    via Glickenstein 2011 eq. (4.6).
  - **Phase 10c** — full uniformization for genus g ≥ 2 (Fuchsian
    group representation) — fully new research, no Java reference.
  - **geometry-central** GC-1/2/3 exploratory track.

* Java backlog summary: 11 worth-porting Java classes identified by
  the parallel survey (FundamentalPolygonUtility, DiscreteHarmonicForm-
  Utility, DiscreteHolomorphicFormUtility, CanonicalBasisUtility,
  HyperbolicCyclicFunctional, QuasiisothermicUtility, KoebePolyhedron, …).
  ~6 500 Java lines, ~5 months of porting work, organised by phase.

Updated documents
─────────────────

* CLAUDE.md
  - New "Port-vs-research maintenance rule" with empirical verification
    command and the four corrected mis-labels.
  - Doc map: 23 → 24 documents (research-track.md added).

* README.md
  - Test count corrected (170 → 176+36).

* doc/math/references.md
  - Luo 2004 entry corrected ("new research" instead of "not yet ported").
  - New entries for Bowers-Stephenson 2004, Glickenstein 2011,
    Bobenko-Pinkall-Springborn 2010, Schläfli 1858/60.

* doc/roadmap/phases.md
  - Phase 9 reorganised: 9a split into 9a.1 (port) / 9a.2 (research),
    9b clarified as research (Java has no Hessian), 9c expanded with
    Java line counts and effort estimates.
  - Phase 10 reorganised: 10a/10b/10c with their Java prerequisites
    explicitly listed; 10c flagged as "fully new research".
  - Phase 10b' added: parallel research track (hyperbolic functional,
    quasi-isothermic, Möbius centering).
  - Phase 10c' added: optional Java-port additions (Koebe, circle
    patterns, electrostatic sphere).

* doc/roadmap/java-parity.md
  - Inversive-distance row:  Java,  C++ (Phase 9a.2) — new research.
  - CP-Euclidean row added:  Java,  C++ (Phase 9a.1) — port.
  - HyperIdeal Hessian row:  Java, ⚠️ FD + block-FD in C++.
  - Worth-porting table replaced with the survey results (12 classes,
    Java line counts, suggested phases).
  - "HyperIdeal Hessian: FD vs analytic" section rewritten with the
    correction notice.

* doc/tutorials/add-inversive-distance.md
  - Rewritten end-to-end with prominent verification block at top.
  - Now correctly framed as "Implementing the Inversive-Distance
    functional from Luo 2004" — research, not port.
  - Includes the four required cross-validations:
    limit cases, Bowers-Stephenson round-trip, FD-vs-analytic,
    cross-validation against euclidean_functional at u=0.
  - New "How to know if it's a port or research" closing section
    with the empirical verification command.

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

301 lines
11 KiB
Markdown
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.

# Tutorial: Implementing the Inversive-Distance Functional (Phase 9a.2)
This tutorial walks through adding a **new** discrete-conformal functional
to conformallab++. The running example is the **vertex-based inversive-
distance functional** of Luo (2004), used as Phase 9a.2 of the roadmap.
> ## ⚠️ This is research, not a port
>
> An earlier draft of this document claimed this functional was a port of
> `de.varylab.discreteconformal.functional.InversiveDistanceFunctional`.
> **That Java class does not exist.** Verified empirically:
>
> ```bash
> $ find /Users/tarikmoussa/Desktop/conformallab -iname "*nversive*"
> (zero results)
> $ grep -r "InversiveDistance" /Users/tarikmoussa/Desktop/conformallab/src
> (zero matches)
> ```
>
> The closest Java cousin is `CPEuclideanFunctional.java`, which implements
> the **face-based** circle-packing variant (Phase 9a.1). The vertex-based
> inversive-distance functional (this tutorial) is built **from the
> literature**, not from a Java reference, and the correctness validation
> is cross-checked against three sources:
>
> 1. **Luo, F.** (2004). *Combinatorial Yamabe Flow on Surfaces.* Comm. Contemp. Math. 6(5), 765780.
> 2. **Bowers, P. L. & Stephenson, K.** (2004). *Uniformizing dessins and Belyĭ maps via circle packing.* Mem. AMS 170(805).
> 3. **Glickenstein, D.** (2011). *Discrete conformal variations and scalar curvature on piecewise flat manifolds.* J. Differential Geometry 87(2), 201238.
>
> The tutorial below has been re-written to match this reality.
**Prerequisite:** Read [doc/api/extending.md](../api/extending.md) first
for the general functional-porting pattern. This tutorial fills in the
mathematical and code details for one specific case.
---
## Mathematical background
Inversive-distance circle packing parametrises **each vertex** by a circle
of radius `r_i = exp(u_i)`. Two adjacent circles have an *inversive
distance* `I_ij` that is a fixed constant of the edge, derived once from
the initial geometry via the BowersStephenson identity:
```
I_ij = ( _ij² r_i² r_j² ) / ( 2 r_i r_j ) (Bowers-Stephenson 2004)
```
Geometric interpretation of `I_ij`:
| Range | Configuration |
|---|---|
| `I_ij = +1` | tangent circles (Koebe-style) |
| `I_ij ∈ (0, 1)` | overlapping with intersection angle `φ`, `I = cos φ` |
| `I_ij = 0` | orthogonal circles |
| `I_ij ∈ (1, 0)` | disjoint circles, inversive distance > 1 |
| `I_ij ≤ 1` | impossible packing |
The edge length under a state `u` is then determined by Luo's formula:
```
_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 (Luo 2004 §3)
```
The angle formula is the same numerically-stable half-tangent law of
cosines used by `euclidean_functional.hpp`; only the way `_ij` is
computed from `u` is different.
The gradient is the standard Yamabe-flow gradient:
```
∂E/∂u_v = Θ_v Σ_{T ∋ v} α_v(T) (Luo 2004 Lemma 3.1)
```
The energy is a path integral of the gradient (Luo's 1-form is closed on
the domain where every triangle is valid); we use the same 10-point
Gauss-Legendre quadrature as `euclidean_functional.hpp`.
The Hessian is finite-difference for the MVP; an analytic form
(Glickenstein 2011 eq. 4.6) is documented in the research-track roadmap.
---
## Step 1 — Create the header
```bash
cp code/include/euclidean_functional.hpp \
code/include/inversive_distance_functional.hpp
```
Modify the maps struct: replace `lambda0` (Euclidean log-length) with the
inversive-distance constant `I_e` and the initial radius `r0` (used for
the Bowers-Stephenson init).
```cpp
struct InversiveDistanceMaps {
ConformalMesh::Property_map<Vertex_index, int> v_idx; // DOF index (1 = pinned)
ConformalMesh::Property_map<Vertex_index, double> theta_v; // target cone angle
ConformalMesh::Property_map<Vertex_index, double> r0; // initial radius r_i^(0)
ConformalMesh::Property_map<Edge_index, double> I_e; // inversive distance per edge
};
```
Then implement the four entry points that any functional needs in
conformallab++:
- `setup_inversive_distance_maps(mesh)` — create maps with defaults.
- `compute_inversive_distance_init_from_mesh(mesh, m)` — choose `r_i^(0)`
from the input geometry, then compute `I_ij` via Bowers-Stephenson.
- `inversive_distance_gradient(mesh, x, m)` — Luo's Σ α`.
- `inversive_distance_energy(mesh, x, m)` — 10-point Gauss-Legendre path integral.
For the full implementation, see `code/include/inversive_distance_functional.hpp`
(part of PR #8).
---
## Step 2 — Edge-length kernel
The single new pure-math primitive is the Luo edge-length formula. Wrap
it in a small detail helper so the gradient function reads cleanly:
```cpp
namespace id_detail {
// ℓ² = exp(2u_i) + exp(2u_j) + 2 I exp(u_i + u_j)
// Returns -1 on degenerate input (no valid packing).
inline double edge_length_squared(double u_i, double u_j, double I_ij) {
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
```
This is the only place where the inversive-distance model differs from
the Euclidean one. All downstream code (angle computation, gradient
accumulation, energy integration) is structurally identical.
---
## Step 3 — Reuse `euclidean_angles()`
The half-tangent law of cosines is independent of how lengths were
obtained. Feed `log(ℓ²)` to the existing helper to compute the three
corner angles per face:
```cpp
auto fa = euclidean_angles(std::log(l12sq), std::log(l23sq), std::log(l31sq));
```
This is the **non-trivial reuse** that justifies the structural similarity
to the Euclidean functional — we get the law-of-cosines numerics for free,
and only the edge-length input changes.
---
## Step 4 — Validation tests
The acceptance criteria for this functional are stricter than for a Java
port because there is no reference implementation to compare against. We
need **three independent validations**:
### 4.1 Limit-case edge lengths
Each of Luo's special cases (`I = 1` tangent, `I = 0` orthogonal,
`I = 1` inside-tangent) gives a closed-form `` that must be reproduced
to machine precision:
```cpp
TEST(InversiveDistanceFunctional, EdgeLengthFormula_TangentialLimit) {
// r_i=1, r_j=2, I=1: ℓ² = 1 + 4 + 2·1·1·2 = 9 ⇒ = 3 = r_i + r_j
double l2 = id_detail::edge_length_squared(0.0, std::log(2.0), 1.0);
EXPECT_NEAR(std::sqrt(l2), 3.0, 1e-12);
}
```
Three such tests cover the diagnostic special cases.
### 4.2 Bowers-Stephenson round-trip
The initialisation `compute_inversive_distance_init_from_mesh` must be
self-consistent: starting from `(_3d, r_i, r_j)` and computing `I_ij`,
the round-trip back through Luo's formula must give the original ``.
```cpp
TEST(InversiveDistanceFunctional, BowersStephensonRoundTrip) {
auto mesh = make_triangle();
auto m = setup_inversive_distance_maps(mesh);
compute_inversive_distance_init_from_mesh(mesh, m);
for (auto e : mesh.edges()) {
double l_3d = /* mesh 3-D edge length */;
double ri = m.r0[mesh.source(mesh.halfedge(e))];
double rj = m.r0[mesh.target(mesh.halfedge(e))];
double l_rec = std::sqrt(ri*ri + rj*rj + 2.0 * m.I_e[e] * ri * rj);
EXPECT_NEAR(l_rec, l_3d, 1e-12);
}
}
```
### 4.3 FD-vs-analytic gradient check
Standard pattern from every functional in conformallab++ — see
`test_euclidean_functional.cpp`. Compare the analytic gradient
to a symmetric finite difference of the energy.
### 4.4 Cross-validation against `euclidean_functional.hpp`
At `u = 0`, both functionals reconstruct the input 3-D edge length
exactly (Euclidean via `compute_lambda0`, inversive distance via
Bowers-Stephenson). Therefore the actual angle sums are identical,
and the two gradients (with default `Θ_v = 2π`) must match component-wise:
```cpp
TEST(InversiveDistanceFunctional, AngleDefectAtU0_AgreesWithEuclideanAtU0) {
auto G_id = inversive_distance_gradient(mesh, /*x=0*/, m_id);
auto G_eu = euclidean_gradient (mesh, /*x=0*/, m_eu);
for (size_t i = 0; i < G_id.size(); ++i)
EXPECT_NEAR(G_id[i], G_eu[i], 1e-10);
}
```
This is the empirical statement of Glickenstein 2011 §5: different
parametrisations of the same initial discrete metric produce the same
Newton-time-zero gradient.
---
## Step 5 — Register the tests
In `code/tests/cgal/CMakeLists.txt`:
```cmake
# ── Phase 9a.2: InversiveDistance (Luo 2004 + Glickenstein 2011) ─────────
# Vertex-based inversive-distance circle-packing functional. No Java
# reference; implemented from the literature. Cross-validated against
# EuclideanCyclicFunctional at the natural initial geometry (u = 0).
test_inversive_distance_functional.cpp
```
Run:
```bash
ctest --test-dir build -R "InversiveDistance" --output-on-failure
```
---
## Step 6 — Newton solver
Once the functional passes its tests, wire a Newton wrapper into
`newton_solver.hpp`:
```cpp
inline NewtonResult newton_inversive_distance(
ConformalMesh& mesh,
std::vector<double> x0,
const InversiveDistanceMaps& m,
double tol = 1e-8,
int max_iter = 200);
```
The body is structurally identical to `newton_euclidean()` — same
SimplicialLDLT + SparseQR fallback, same termination test. Only the
inner gradient / Hessian calls differ.
---
## Checklist for a new functional
- [ ] `code/include/<name>_functional.hpp` compiles
- [ ] Limit-case edge-length tests pass at machine precision
- [ ] Round-trip identity (init ⇄ length formula) verified
- [ ] FD-vs-analytic gradient check passes on triangle, quad strip, tetra
- [ ] Cross-validation test against an existing functional at `u = 0`
- [ ] Newton wrapper added to `newton_solver.hpp`
- [ ] Registered in `code/tests/cgal/CMakeLists.txt`
- [ ] `doc/roadmap/java-parity.md` updated (port status or research note)
- [ ] `doc/math/references.md` extended with the primary paper(s)
- [ ] If this is *new research* beyond Java: add an entry in
`doc/roadmap/research-track.md` with citations and acceptance criteria
---
## How to know if it's a port or research
Run the local Java-repo check **first** before writing any tutorial doc:
```bash
find /Users/tarikmoussa/Desktop/conformallab -iname "*<feature>*"
grep -r "<ClassName>" /Users/tarikmoussa/Desktop/conformallab/src
```
If both return zero matches, the feature is **not** in Java and any C++
implementation is **new research**, not a port. The tutorial framing and
the `doc/roadmap/research-track.md` entry should reflect this from day one.