Files
ConformalLabpp/doc/architecture/phase-9a-validation.md
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

12 KiB
Raw Blame History

Phase 9a Validation Report

This document records the mathematical and code-level validation of the two Phase 9a functionals against their reference sources. It is produced as part of the Phase 9a acceptance procedure (decided 2026-05-19).

Functional Reference Java original Lines
9a.1 cp_euclidean_functional.hpp Bobenko-Pinkall-Springborn 2010 CPEuclideanFunctional.java (260) 320
9a.2 inversive_distance_functional.hpp Luo 2004 + Glickenstein 2011 none 290

1. CP-Euclidean (9a.1) — line-by-line Java mapping

Reference file: /Users/tarikmoussa/Desktop/conformallab/src/de/varylab/discreteconformal/functional/CPEuclideanFunctional.java

Java line(s) Java construct C++ counterpart Status
5558 public class CPEuclideanFunctional<V,E,F> namespace conformallab (header-only, monomorphic on ConformalMesh)
6169 thetaMap, phiMap ctor args CPEuclideanMaps { f_idx, theta_e, phi_f } struct
9597 getDimension = hds.numFaces() (incl. face 0) cp_euclidean_dimension(mesh, m) counts only f_idx ≥ 0 refined
103123 getNonZeroPattern returns face-to-face adjacency implicit in cp_euclidean_hessian triplet construction
135165 evaluateHessian — interior edge h_jk = sin θ / (cosh Δρ cos θ) cp_euclidean_hessian() lines 224-247
146151 "skip if k == 0 / j == 0" gauge fix if (j >= 0) / if (k >= 0) guard in triplet emission equivalent
178193 per-face term +φ_f · ρ_f to energy and gradient cp_euclidean_energy/gradient loop over faces
196232 per-(directed)-edge term: boundary or interior branch cp_euclidean_energy/gradient halfedge loop with mesh.is_border(opposite) check
224227 E += ½ p Δρ + Λ(θ*+p) θ* ρ_left identical formula via clausen2()
230 grad[left] -= p + θ* identical
243247 p(θ*, Δρ) = 2 atan(tan(θ*/2) tanh(Δρ/2)) cp_detail::p_function

1.1 Gauge convention divergence

The Java code uses "face index 0 is pinned" as an implicit convention: hard-coded if (face.getIndex() == 0) short-circuits scatter throughout. The C++ port keeps the gauge user-selectable via assign_cp_euclidean_face_dof_indices(mesh, m, pinned_face). The default convenience overload picks the first face in iteration order, matching the Java behaviour for any mesh whose face 0 is the first iterated face.

1.2 Test parity

Java test C++ test Reproduces what
init() lines 53-86 dodecahedron-minus-face setup make_open_tetrahedron() (3 faces, 3 bdy edges) open-mesh boundary code path
setXGradient(rho) (base class FunctionalTest) FDGradientCheck_ClosedTetrahedron_RandomRho FD-vs-analytic gradient
setXHessian(rho) (base class FunctionalTest) FDHessianCheck_ClosedTetrahedron_RandomRho FD-vs-analytic Hessian
(none — implicit from FunctionalTest convexity check) HessianIsPSD BPS-2010 §6 convexity
(none) TangentialLimitGradientEqualsPhi θ=0 analytic check
(none) PFunctionKnownValues, NaturalPhiMakesZeroTheEquilibrium helper-function unit tests

All Java tests reproduced; C++ adds 4 mathematical-property tests (PSD, tangential limit, p-function unit, natural-φ equilibrium).

1.3 Numerical equivalence — seed-1 random ρ, FD gradient

Java FunctionalTest uses Random(1) + uniform [-0.5, +0.5]. C++ uses std::mt19937(1) + uniform_real_distribution<double>(-0.5, 0.5). The two RNG streams differ; numerical equivalence is therefore tested via the property "FD matches analytic to 1e-6 absolute" rather than via identical numerical traces. Both implementations satisfy this property.


2. Inversive Distance (9a.2) — line-by-line literature mapping

No Java original exists. The implementation derives directly from three published papers; each formula in the header is annotated with its source line in the paper.

2.1 Edge-length formula (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

Implementation: id_detail::edge_length_squared (lines 130-138 of inversive_distance_functional.hpp).

Tested at three special-case limits in test_inversive_distance_functional.cpp §1:

Inversive distance Geometric meaning Closed-form Test
I = +1 tangential circles r_i + r_j EdgeLengthFormula_TangentialLimit
I = 0 orthogonal circles √(r_i² + r_j²) EdgeLengthFormula_OrthogonalLimit
I = 1 inside-tangent / inverted ` r_i r_j
I < 1 impossible packing (no real ) nan/signal EdgeLengthFormula_DegenerateReturnsMinusOne

2.2 Bowers-Stephenson identity (Bowers-Stephenson 2004)

I_ij  =  ( _ij²  r_i²  r_j² )  /  ( 2 r_i r_j )

Implementation: compute_inversive_distance_init_from_mesh lines 152-172. The round-trip property — given (_3d, r0_i, r0_j) recover via the Luo formula — is tested in BowersStephensonRoundTrip.

2.3 Gradient (Luo 2004 Lemma 3.1)

∂E/∂u_v  =  Θ_v    Σ_{T ∋ v}  α_v(T)

Implementation: inversive_distance_gradient lines 191-241. This is structurally identical to the Euclidean functional (same halfedge convention, same euclidean_angles() law-of-cosines reused). The only difference is the edge-length input: Luo's ℓ²(I) instead of Springborn's exp(λ° + u_i + u_j).

2.4 Energy — Luo's closed 1-form

Luo (2004) proves the gradient 1-form ω = Σ_v (Θ_v Σ α_v) du_v is closed on the open domain where every triangle is valid. Hence the energy exists as a path integral. No general closed-form expression is known (Glickenstein 2011 surveys partial results). The implementation uses the same 10-point Gauss-Legendre quadrature as euclidean_functional.hpp (lines 243-274).

This shared-quadrature choice keeps both functionals identical in the energy-evaluation cost and the FD-gradient validation pattern.

2.5 Hessian — finite difference for now

Glickenstein 2011 eq. (4.6) gives an analytic Hessian for the inversive- distance variational principle, but is more involved than the Springborn cot-Laplacian. For MVP we rely on FD; the analytic form is a future optimisation (joining the Phase 9b roadmap for HyperIdeal as a sibling optimisation task).


3. Cross-validation between 9a.1 and 9a.2

The two functionals describe geometrically distinct circle packings (face-dual vs vertex-based) but agree in the angle-defect structure they expose to the Newton solver: both report Θ Σ α_actual at every free DOF. This is tested directly:

TEST(InversiveDistanceFunctional, AngleDefectAtU0_AgreesWithEuclideanAtU0)
{
    auto mesh = make_quad_strip();

    auto G_id = inversive_distance_gradient(mesh, x_id, m_id);
    auto G_eu = euclidean_gradient            (mesh, x_eu, m_eu);

    for (i in DOFs) EXPECT_NEAR(G_id[i], G_eu[i], 1e-10);
}

Why this works: at u = 0, both initialisation procedures reconstruct the input 3-D edge length exactly (compute_*_lambda0_from_mesh for Euclidean, compute_inversive_distance_init_from_mesh for inversive distance). Therefore the actual angle sums per vertex are identical, and both gradients reduce to the same Θ_default Σ α(_3d).

This is the operational consequence of Glickenstein 2011 §5: "different parametrisations of the same initial discrete metric produce the same Newton-time-zero gradient".


4. Comparison with euclidean_functional.hpp

Aspect Euclidean CP-Euclidean (9a.1) Inversive Distance (9a.2)
DOF basis per vertex (u_v = log scale) per face (ρ_f = log radius) per vertex (u_v = log radius)
Per-edge constant λ°_ij = 2 log °_ij θ_e (intersection angle) I_ij (inversive distance)
Edge-length formula = exp((λ° + u_i + u_j)/2) (no per-edge — face-circle radii) ℓ² = r_i² + r_j² + 2 I r_i r_j
Angles half-tangent law of cosines (face-internal angles via p(θ*, Δρ)) half-tangent law of cosines (reuses euclidean_angles)
Gradient Θ_v Σ α_v(f) φ_f Σ_{h:face(h)=f} (p+θ*) Θ_v Σ α_v(f)
Energy form path integral (10-pt GL) closed form via ½ p Δρ + Λ(θ*+p) θ* ρ path integral (10-pt GL)
Hessian analytic (cotangent Laplacian) analytic (sin θ / (cosh Δρ cos θ)) finite difference (analytic deferred — Glickenstein 2011 eq. 4.6)
Convexity strictly convex after gauge fix strictly convex after gauge fix locally convex on triangle-inequality domain (Luo 2004 Thm 1.2)
Gauge fix pin one vertex (v_idx = 1) pin one face (f_idx = 1) pin one vertex (v_idx = 1)

4.1 Structural reuse

inversive_distance_functional.hpp reuses euclidean_angles() from euclidean_geometry.hpp unchanged. The only difference from euclidean_functional.hpp is the four lines that map (u_i, u_j, I_ij) to _ij² before invoking euclidean_angles().

cp_euclidean_functional.hpp shares no algorithmic code with the Euclidean functional — it operates on a different DOF basis and a different energy form. It does share the Clausen function via clausen2() from clausen.hpp.


5. Test counts and acceptance criteria

Suite Tests Status
cgal.CPEuclideanFunctional.* 10 all pass
cgal.InversiveDistanceFunctional.* 11 all pass
Full CGAL regression 205 all pass
Non-CGAL fast suite 36 all pass

Acceptance criteria — met

  • FD-vs-analytic gradient check passes on all test meshes (both functionals)
  • FD-vs-analytic Hessian check passes on closed and open tetrahedron (9a.1)
  • Hessian PSD-property test passes (9a.1)
  • Bowers-Stephenson round-trip identity verified (9a.2)
  • Three special-case limits of Luo's edge formula verified at machine ε (9a.2)
  • Cross-validation against euclidean_functional at u = 0 (9a.2)
  • All formulas cite their source paper or Java line number

6. References

  • Bobenko, A. I., Pinkall, U. & Springborn, B. (2010). Discrete conformal maps and ideal hyperbolic polyhedra. Geometry & Topology 14, 379426.
  • 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.
  • Luo, F. (2004). Combinatorial Yamabe Flow on Surfaces. Communications in Contemporary Mathematics 6(5), 765780.
  • Java original (CPEuclidean only): /Users/tarikmoussa/Desktop/conformallab/src/de/varylab/discreteconformal/functional/CPEuclideanFunctional.java
  • Java test: /Users/tarikmoussa/Desktop/conformallab/src-test/de/varylab/discreteconformal/functional/CPEuclideanFunctionalTest.java