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>
20 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project purpose and long-term goal
conformallab++ is a C++17 reimplementation of ConformalLab — Stefan Sechelmann's Java research library for discrete conformal geometry (TU Berlin, ~850 commits, v1.0.0 2018). The algorithmic foundation is his dissertation:
Stefan Sechelmann — Variational Methods for Discrete Surface Parameterization: Applications and Implementation, TU Berlin 2016. DOI: 10.14279/depositonce-5415 · CC BY-SA 4.0
The long-term goal is a CGAL package — a submission to the CGAL library that brings discrete conformal maps (hyper-ideal, spherical, Euclidean) to the CGAL ecosystem using CGAL::Surface_mesh as the underlying halfedge data structure, with a traits-class design compatible with arbitrary CGAL-conforming mesh types.
The project has three distinct phases:
- Phase 1–7 (done): Direct port of the Java library algorithms to C++
- Phase 8–9 (planned): CGAL package infrastructure + remaining Java features not yet ported (inversive-distance functional, analytic HyperIdeal Hessian, genus-g > 1 fundamental domain)
- Phase 10+ (research): New mathematics beyond the Java original — holomorphic differentials, Siegel period matrix Ω ∈ H_g, full uniformization for genus g ≥ 2
Language
All code, comments, documentation, commit messages, and test descriptions must be in English. The project is intended for international collaboration and CGAL submission. Existing German-language comments in older files should be replaced with English when editing those files.
Build commands
All source lives under code/. Three build modes:
# Mode 1 — fast tests, no CGAL, no Boost, no display (CI default)
cmake -S code -B build
cmake --build build --target conformallab_tests -j$(nproc)
ctest --test-dir build --output-on-failure
# Mode 2 — CGAL tests, headless (CI full, requires Boost headers only)
# macOS: brew install boost Linux: apt install libboost-dev
cmake -S code -B build -DWITH_CGAL_TESTS=ON
cmake --build build --target conformallab_cgal_tests -j$(nproc)
ctest --test-dir build -R "^cgal\." --output-on-failure
# Mode 3 — full local build: CLI app + viewer + examples (requires Wayland/X11)
cmake -S code -B build -DWITH_CGAL=ON
cmake --build build -j$(nproc)
-DWITH_CGAL=ON automatically enables -DWITH_VIEWER=ON, which pulls in GLFW and requires wayland-scanner. Never use this in headless CI.
Running a single test
# By GTest suite/test name
./build/conformallab_cgal_tests --gtest_filter="NewtonSolver*"
./build/conformallab_tests --gtest_filter="Clausen*"
# By CTest regex (prefix "cgal." for all CGAL tests)
ctest --test-dir build -R "cgal.NewtonSolver" --output-on-failure
Rebuilding the CI Docker image
docker buildx build \
--platform linux/arm64 \
-f .gitea/docker/Dockerfile.ci-cpp \
-t git.eulernest.eu/conformallab/ci-cpp:latest \
--push \
.gitea/docker/
Architecture
Everything is header-only
All algorithms live in code/include/*.hpp. There is no compiled library. The three CMake targets (conformallab_tests, conformallab_cgal_tests, conformallab_core) compile headers directly from their .cpp entry points. To add a new algorithm: create a .hpp in code/include/, add a test in code/tests/cgal/, and register the test file in code/tests/cgal/CMakeLists.txt.
Central type: ConformalMesh
conformal_mesh.hpp defines the core type:
using ConformalMesh = CGAL::Surface_mesh<Point3>; // CGAL::Simple_cartesian<double>
This replaces the Java CoHDS (half-edge data structure) and its intrusive CoVertex/CoEdge/CoFace types. Data is attached via named CGAL property maps instead of intrusive fields:
| Property map name | Type | Meaning |
|---|---|---|
"v:lambda" |
double per vertex |
log scale factor (conformal variable uᵢ) |
"v:theta" |
double per vertex |
target cone angle Θᵥ |
"v:idx" |
int per vertex |
solver DOF index; -1 = pinned/boundary |
"e:alpha" |
double per edge |
intersection angle αᵢⱼ (hyperbolic only) |
"f:type" |
int per face |
geometry type (0=Euclidean, 1=Hyperbolic, 2=Spherical) |
CGAL_DISABLE_GMP and CGAL_DISABLE_MPFR are defined for all CGAL targets — the library deliberately uses Simple_cartesian<double> (floating-point, no exact arithmetic) because conformal geometry does not require exact predicates.
The three geometry modes
Each mode has its own Maps struct that bundles all property maps, plus functional, Hessian, and Newton function:
| Mode | Space | Maps struct | Key headers | Newton function |
|---|---|---|---|---|
| Euclidean | ℝ² | EuclideanMaps |
euclidean_functional.hpp, euclidean_hessian.hpp |
newton_euclidean() |
| Spherical | S² | SphericalMaps |
spherical_functional.hpp, spherical_hessian.hpp |
newton_spherical() |
| Hyper-ideal | H² (Poincaré disk) | HyperIdealMaps |
hyper_ideal_functional.hpp, hyper_ideal_hessian.hpp |
newton_hyper_ideal() |
HyperIdeal also has edge DOFs (e_idx[e]); Euclidean and Spherical are vertex-DOF only. For HyperIdeal: assign_all_dof_indices(mesh, maps) assigns all vertex and edge DOFs automatically. For Euclidean/Spherical: pin one vertex manually (maps.v_idx[first_vertex] = -1) then assign sequential indices.
The full pipeline
load_mesh() → ConformalMesh (OFF/OBJ/PLY)
setup_*_maps(mesh) → *Maps (property maps created, all zero)
compute_*_lambda0_from_mesh(mesh, m) → λ° initialised from 3-D edge lengths
DOF assignment → v_idx[v] set; -1 = pinned
check_gauss_bonnet(mesh, maps) → throws if Σ(2π−Θᵥ) ≠ 2π·χ(M)
enforce_gauss_bonnet(mesh, maps) → redistributes angle defect uniformly
newton_*(mesh, x0, maps) → NewtonResult{x*, iterations, converged}
compute_cut_graph(mesh) → CutGraph (2g seam edges, tree-cotree)
*_layout(mesh, x*, maps, &cg, &hol) → Layout2D/3D + HolonomyData
normalise_*(layout) → canonical position (PCA / Möbius / Rodrigues)
compute_period_matrix(hol) → PeriodData{τ∈ℍ} (genus 1 flat torus)
compute_fundamental_domain(hol) → FundamentalDomain{vertices, generators}
tiling_neighbourhood(layout, hol) → vector of translated layout copies
save_result_json/xml() → serialised result
After compute_*_lambda0_from_mesh() the original vertex positions are no longer used — all subsequent computation is in log-length/scale-factor space.
Newton solver (newton_solver.hpp)
The gradient sign convention differs between modes:
- Euclidean/Spherical:
G_v = Θ_v − actual_angle_sum(target minus actual) - HyperIdeal:
G_v = actual_angle_sum − Θ_v(actual minus target)
Hessian sign and solver:
- Euclidean: H is PSD (cotangent Laplacian) →
SimplicialLDLT(H) - Spherical: H is NSD (concave energy) →
SimplicialLDLT(−H)(sign flip insidenewton_spherical) - HyperIdeal: H is PSD (strictly convex) →
SimplicialLDLT(H)
When SimplicialLDLT fails (rank-deficient H — gauge mode on closed mesh without pinned vertex), the solver automatically retries with Eigen::SparseQR to find the minimum-norm step orthogonal to the null space. Public API: solve_linear_system(H, rhs, &used_fallback).
The HyperIdeal Hessian is currently a symmetric finite-difference approximation (O(ε²), costs n extra gradient evaluations per Newton step). The analytic Hessian via the chain (bᵢ, aₑ) → lᵢⱼ → ζ₁₃/ζ₁₄/ζ₁₅ → αᵢⱼ/βᵢ is deferred to Phase 9b.
Layout and holonomy (layout.hpp)
BFS-trilateration with a priority min-heap on BFS depth (depth = max(depth[src], depth[tgt]) + 1). Root face = largest 3-D area face. This minimises trilateration error accumulation compared to simple BFS.
Key output fields:
layout.uv[v.idx()]— primary UV (first/shallowest BFS visit per vertex)layout.halfedge_uv[h.idx()]— UV ofsource(h)as seen fromface(h); at seam halfedges the two opposite halfedges carry different UV values, enabling proper GPU texture atlasing without vertex duplicationhol.translations[i]— lattice generator ωᵢ ∈ ℂ (Euclidean/spherical)hol.mobius_maps[i]— Möbius isometry Tᵢ ∈ SU(1,1) (hyperbolic, Poincaré disk)
MobiusMap is defined in layout.hpp: T(z) = (az+b)/(cz+d). Key methods: from_three() (fit to 3 point correspondences via 3×3 complex linear system), compose(), inverse(), apply(Vector2d).
Key mathematical reference for each header
| Header | Java original | Key reference |
|---|---|---|
hyper_ideal_geometry.hpp |
HyperIdealGeometry.java |
Springborn (2020) — ζ₁₃/ζ₁₄/ζ₁₅ functions |
euclidean_hessian.hpp |
EuclideanHessian.java |
Pinkall & Polthier (1993) — cotangent Laplacian |
spherical_hessian.hpp |
SphericalHessian.java |
∂α/∂u from spherical law of cosines |
cut_graph.hpp |
CuttingUtility.java |
Erickson & Whittlesey (SODA 2005) — tree-cotree |
period_matrix.hpp |
PeriodMatrixUtility.java |
Sechelmann (2016) §4 — SL(2,ℤ) reduction |
gauss_bonnet.hpp |
(distributed across Java) | Gauss–Bonnet: Σ(2π−Θᵥ) = 2π·χ(M) |
Java features not yet ported (Phase 9)
The Java library under de.varylab.discreteconformal contains these items not yet in C++:
| Java class | Planned C++ header | Phase |
|---|---|---|
InversiveDistanceFunctional |
inversive_distance_functional.hpp |
9a |
| Analytic HyperIdeal Hessian | hyper_ideal_hessian.hpp (replace FD) |
9b |
4g-polygon boundary walk in FundamentalDomainUtility |
fundamental_domain.hpp (extend) |
9c |
DiscreteHarmonicFormUtility |
Phase 10a prerequisite | 10 |
DiscreteHolomorphicFormUtility |
Phase 10a | 10 |
HomologyUtility, CanonicalBasisUtility |
Phase 10 | 10 |
When porting a Java class, locate the original in de.varylab.discreteconformal.* at github.com/varylab/conformallab and use it as the reference implementation.
Test design patterns
"Natural theta" — constructing a known equilibrium at x* = 0
// Evaluate gradient at x=0; set target angles = actual angle sums → x*=0 by definition
std::vector<double> x0(n_dofs, 0.0);
auto G0 = euclidean_gradient(mesh, x0, maps);
for (auto v : mesh.vertices())
if (maps.v_idx[v] >= 0)
maps.theta_v[v] -= G0[maps.v_idx[v]]; // shift so G(x=0) = 0
This is used in virtually every Newton convergence test — it avoids hardcoding specific angle values.
Gradient check pattern
// Copy from any test_*_functional.cpp — GradientCheck_* test suite
double eps = 1e-5;
for (int i = 0; i < n; ++i) {
xp[i] += eps; auto Gp = euclidean_gradient(mesh, xp, maps);
xm[i] -= eps; auto Gm = euclidean_gradient(mesh, xm, maps);
double fd = (energy(xp) - energy(xm)) / (2*eps);
EXPECT_NEAR(G[i], fd, 1e-7);
xp[i] = xm[i] = x0[i];
}
All new functionals must have a gradient-check test before being considered complete.
Halfedge traversal
for (auto f : mesh.faces()) {
auto h0 = mesh.halfedge(f); // canonical halfedge of face
auto h1 = mesh.next(h0);
auto h2 = mesh.next(h1);
Vertex_index v1 = mesh.source(h0); // = mesh.target(h2)
Vertex_index v2 = mesh.source(h1);
Vertex_index v3 = mesh.source(h2);
// Angle at v3 is opposite to h0 (edge v1–v2)
// h_alpha[h0] = α₃, h_alpha[h1] = α₁, h_alpha[h2] = α₂
bool is_boundary = mesh.is_border(mesh.opposite(h0));
}
Attaching custom data to the mesh
auto [my_map, created] = mesh.add_property_map<Vertex_index, double>("v:my_data", 0.0);
my_map[v] = 3.14;
CI pipeline
Two jobs in .gitea/workflows/cpp-tests.yml:
| Job | CMake flags | Deps | Triggers on |
|---|---|---|---|
test-fast |
(none) | Eigen + GTest only | all branches |
test-cgal |
-DWITH_CGAL_TESTS=ON |
+ Boost | pull requests only |
Runner: eulernest — self-hosted Raspberry Pi, ARM64, Ubuntu 22.04. Docker image: git.eulernest.eu/conformallab/ci-cpp:latest. test-cgal needs test-fast to pass first (needs: test-fast).
Expected results: 36 non-CGAL tests pass, 176 CGAL tests pass, 0 skipped.
Release state
Current release: v0.7.0 (tag on origin/dev, PR to main open).
Phase 7 is complete. Phase 7.5 (Doxygen) and Phase 8 (CGAL package) are next.
Phase 8 strategic decisions (2026-05-19)
The CGAL-package architecture was frozen on 2026-05-19. Full design:
doc/api/cgal-package.md. Key decisions:
| Decision | Choice |
|---|---|
| Submission to upstream CGAL | Pre-submission-ready, not bound. 12+ months horizon. |
| License | MIT preserved (no LGPL switch). |
| Mesh-type flexibility | Generic FaceGraph + HalfedgeGraph in target design; MVP starts Surface_mesh-only. |
| Parameter style | Named Parameters (CGAL::parameters::...). |
| Default kernel | Simple_cartesian<double> (status quo). |
| Backward compatibility | Dual-layer wrapper — code/include/*.hpp stays as implementation, include/CGAL/*.h is thin wrapper. No algorithm duplication. |
| Implementation strategy | Hybrid MVP — minimum Phase 8 (traits + one wrapper) first, then Phase 9 in full, then Phase 8 extensions only on concrete demand. |
| Phase-8 MVP acceptance test | Phase 9a (Inversive-Distance) as the first new client of the new traits API. |
Implementation sequence (committed)
1. Phase 7.5 Doxygen + cleanup done ✅
2. Phase 8 MVP — traits + one euclidean wrapper 3–5 days
3. Phase 9a — Inversive-Distance against new traits 3–5 days
4. Phase 9b — analytic HyperIdeal Hessian 1 week
5. Phase 9c — 4g-polygon for genus g > 1 1 week
→ port really complete, v0.9.0 release
Phase 8 extensions (8a.2 generic FaceGraph, 8c full Doxygen manuals, 8d CGAL-format tests, 8e YAML pipeline) are deferred to on-demand status — no speculative architecture for an uncertain submission.
Root-level files added at v0.7.0:
CITATION.cff— machine-readable citation (Sechelmann 2016, Springborn 2020, Bobenko–Springborn 2004)CONTRIBUTING.md— short root-level pointer todoc/contributing.mdscripts/try_it.sh— one-script quickstart: build → 209 tests → example run- CMake install target:
cmake --install build --prefix /usr/local→ headers land ininclude/conformallab/
Port-vs-research maintenance rule (2026-05-21 audit)
Before claiming something "ports X from Java", verify empirically:
find /Users/tarikmoussa/Desktop/conformallab -iname "*X*"
grep -r "ClassName" /Users/tarikmoussa/Desktop/conformallab/src
If zero matches, the work is new research — add it to
doc/roadmap/research-track.md with primary literature citations,
not to doc/roadmap/java-parity.md.
The 2026-05-21 audit found four pre-existing mis-labels:
| Item | Wrong claim | Reality |
|---|---|---|
InversiveDistanceFunctional |
"Java port (Luo 2004)" | No such Java class exists |
| HyperIdeal Hessian (FD) | "Phase 4a" | Research — Java has hasHessian()==false |
| HyperIdeal Hessian (analytic) | "Phase 9b port" | Research — derivation via Schläfli 1858 |
| Tutorial framing | "ports InversiveDistanceFunctional.java" |
Implementation from Luo 2004 + Glickenstein 2011 |
All four are corrected as of this commit. Future contributors must follow the empirical verification rule above before any new claim.
Documentation map
24 documents across 6 categories. Read the relevant one before reasoning from scratch — do not hallucinate content that is already written down.
Mathematics & theory
| Question | Document |
|---|---|
| What problem does this library solve mathematically? | doc/math/discrete-conformal-theory.md |
| How do the three geometry modes differ (Euclidean/Spherical/HyperIdeal)? | doc/math/geometry-modes.md |
| What analytic invariants can be used to validate correctness? | doc/math/validation.md |
| What are the exact ctest commands with expected terminal output? | doc/math/validation-protocol.md |
| What is the O() complexity and how does it scale with mesh size? | doc/math/complexity.md |
| Which papers are referenced by which header? | doc/math/references.md |
| How does conformallab++ compare to libigl, CGAL, geometry-central, pmp-library? | doc/math/software-landscape.md |
| What is unique about conformallab++ (novelty, target audience)? | doc/math/novelty-statement.md |
Architecture & design
| Question | Document |
|---|---|
| Full pipeline diagram and data-flow overview | doc/architecture/overall_pipeline.md |
| Directory tree, build targets, file organisation | doc/architecture/project-structure.md |
| Key architectural decisions and their rationale | doc/architecture/design-decisions.md |
| Detailed comparison with geometry-central (CMU): overlap, adoption, scientific value | doc/architecture/geometry-central-comparison.md |
| Phase 9a validation report (CP-Euclidean port + Luo-inversive-distance literature check) | doc/architecture/phase-9a-validation.md |
API & extension
| Question | Document |
|---|---|
| All 24 public headers with descriptions | doc/api/headers.md |
| Full pipeline API for all three geometries | doc/api/pipeline.md |
| What does each processing unit require/provide (contracts)? | doc/api/contracts.md |
| How to add a new functional / geometry mode / port from Java | doc/api/extending.md |
| All 35 test suites, 176+36 tests, individual counts | doc/api/tests.md |
| Phase 8 CGAL package design + Declarative YAML pipeline spec | doc/api/cgal-package.md |
Concepts & specs
| Question | Document |
|---|---|
| Declarative YAML pipeline: token vocabulary, 5 examples, validation algorithm | doc/concepts/declarative-pipeline.md |
Roadmap & porting
| Question | Document |
|---|---|
| Phases 1–10 with status and sub-tasks | doc/roadmap/phases.md |
| Which Java classes are ported, which are planned, which are skipped? | doc/roadmap/java-parity.md |
| New research items (beyond Java) — citations, acceptance criteria | doc/roadmap/research-track.md |
Tutorials & onboarding
| Question | Document |
|---|---|
| Build modes, single-test invocation, CLI, Docker image rebuild | doc/getting-started.md |
| Step-by-step: port the Inversive Distance functional (Phase 9a template) | doc/tutorials/add-inversive-distance.md |
| Language policy, test standards, release flow | doc/contributing.md |
geometry-central context
geometry-central (Keenan Crane, CMU) implements the same discrete conformal
equivalence problem (Gillespie, Springborn & Crane, SIGGRAPH 2021) but uses
Ptolemaic flips on intrinsic triangulations instead of Newton on the original mesh.
It has no period matrix, holonomy, or spherical geometry mode.
The shared mathematical core (Springborn 2020) means cross-validation is meaningful.
Full analysis: doc/architecture/geometry-central-comparison.md.
Optional adoption roadmap (GC-1/2/3): doc/roadmap/phases.md (Optional section).
Known quirks
test-fastalso runs stubs:conformallab_tests(non-CGAL) containsGTEST_SKIP-based stubs for functionals that need CGAL. This is intentional — those tests document what was in the Java port scope but requires the CGAL mesh type.- Boost is header-only: CGAL 6.x uses only Boost headers (
Boost.Config,Boost.Graph). No compiled Boost libraries are needed.find_package(Boost REQUIRED)only locates the include path. mainbranch is protected onorigin(Gitea). Push todev, then merge via pull request. Codebergmaincan be pushed to directly.- Both remotes must stay in sync:
origin=git.eulernest.eu(CI runs here),codeberg=codeberg.org/TMoussa/ConformalLabpp(public mirror). Push to both after every significant change.