conformallab++ 0.7.0
Discrete conformal maps on triangle meshes — C++17 reimplementation of ConformalLab (TU Berlin)
Loading...
Searching...
No Matches
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 four distinct phase blocks (updated 2026-05-22):

  • Phase 1–7 (done, v0.7.0): Direct port of the Java library algorithms to C++.
  • Phase 8a MVP + 8b-Lite (done, v0.9.0): CGAL public-API surface for all five DCE models via <CGAL/Discrete_*.h>. Phase 8a.2 (generic FaceGraph), 8c (manuals), 8d (CGAL-test-format), 8e (YAML pipeline) deferred on-demand.
  • Phase 9a + 9b (done, v0.9.0): Two new functionals (CP-Euclidean port, Inversive-Distance research), two new Newton solvers, block-FD HyperIdeal Hessian.
  • Phase 9b-analytic + 9c (planned): Full analytic HyperIdeal Hessian via Schläfli identity (research, see doc/roadmap/research-track.md); 4g-polygon fundamental domain for genus g > 1 (mixed port + research).
  • Phase 10+ (research): 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 five DCE models

Each model has its own Maps struct that bundles all property maps, plus a functional, optional Hessian, Newton solver, and (since v0.9.0) a CGAL public-API entry function:

Model Space DOFs Maps struct Key headers Newton function CGAL entry
Euclidean ℝ² vertex EuclideanMaps euclidean_functional.hpp, euclidean_hessian.hpp newton_euclidean() discrete_conformal_map_euclidean()
Spherical vertex SphericalMaps spherical_functional.hpp, spherical_hessian.hpp newton_spherical() discrete_conformal_map_spherical()
Hyper-ideal H² (Poincaré disk) vertex + edge HyperIdealMaps hyper_ideal_functional.hpp, hyper_ideal_hessian.hpp (block-FD, Phase 9b) newton_hyper_ideal() discrete_conformal_map_hyper_ideal()
CP-Euclidean (BPS 2010) face-based circle packing face CPEuclideanMaps cp_euclidean_functional.hpp newton_cp_euclidean() discrete_circle_packing_euclidean()
Inversive-Distance (Luo 2004) vertex-based circle packing vertex InversiveDistanceMaps inversive_distance_functional.hpp newton_inversive_distance() discrete_inversive_distance_map()

DOF-assignment patterns:

  • Vertex-only models (Euclidean, Spherical, Inversive-Distance): pin one vertex manually (maps.v_idx[first_vertex] = -1) then assign sequential indices. The CGAL public entries do this automatically with the "natural-theta" trick (so calling them with no arguments returns x = 0 as the equilibrium).
  • HyperIdeal: assign_all_dof_indices(mesh, maps) assigns vertex + edge DOFs automatically.
  • CP-Euclidean: face-based — assign_cp_euclidean_face_dof_indices(mesh, maps, pinned_face) pins one face and indexes the rest.

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)

Gradient sign convention differs across the five models:

  • Euclidean / Spherical / Inversive-Distance: G_v = Θ_v − actual_angle_sum (target minus actual).
  • HyperIdeal: G_v = actual_angle_sum − Θ_v (actual minus target).
  • CP-Euclidean: G_f = φ_f − Σ_{h:face(h)=f} (p(θ*,Δρ) + θ*) (face-based; see cp_euclidean_functional.hpp header for the full formula).

Hessian sign and solver per model:

  • Euclidean: H is PSD (cotangent Laplacian) → SimplicialLDLT(H).
  • Spherical: H is NSD (concave energy) → SimplicialLDLT(−H) (sign flip inside newton_spherical).
  • HyperIdeal: H is PSD (strictly convex) → SimplicialLDLT(H). Phase 9b uses a block-FD Hessian (per-face 6×6 local block, ~96× speed-up vs full FD on V=200). Full analytic Hessian via the chain (bᵢ, aₑ) → lᵢⱼ → ζ₁₃/ζ₁₄/ζ₁₅ → αᵢⱼ/βᵢ is planned research — see doc/roadmap/research-track.md Phase 9b-analytic.
  • CP-Euclidean: analytic 2×2-per-edge h_jk = sin θ / (cosh Δρ − cos θ) (BPS 2010), strictly convex → SimplicialLDLT(H).
  • Inversive-Distance: FD Hessian (inline in newton_inversive_distance). Analytic via Glickenstein 2011 eq. (4.6) is planned research (Phase 9a.2-analytic).

When SimplicialLDLT fails (rank-deficient H — gauge mode on a closed mesh without pinned vertex/face), 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).

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 of source(h) as seen from face(h); at seam halfedges the two opposite halfedges carry different UV values, enabling proper GPU texture atlasing without vertex duplication
  • hol.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: full test suite passing, 0 skipped, 0 failed. The canonical counts live in doc/api/tests.md — do not hardcode them anywhere else (see doc/release-policy.md).

Release state

Current release: v0.9.0 (tag on main, released 2026-05-22). Phases 1–9a complete, Phase 8b-Lite CGAL API surface complete (all 5 DCE models reachable via <CGAL/Discrete_*.h>), Phase 9b block-FD HyperIdeal Hessian shipped (~96× speed-up). Next planned milestones: Phase 9c (4g-polygon, genus g > 1) and Phase 9b-analytic (Schläfli identity). See doc/release-policy.md for the version-tag policy and doc/roadmap/phases.md for the phase plan.

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 wrappercode/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 to doc/contributing.md
  • scripts/try_it.sh — one-script quickstart: build → 209 tests → example run
  • CMake install target: cmake --install build --prefix /usr/local → headers land in include/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
Per-suite breakdown and counts (single source of truth) 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
Versioning rules + release process + single-source-of-truth list doc/release-policy.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

  • No GTEST_SKIP stubs remain (since v0.9.0): the three stale HDS-port stub files were removed because the CGAL test suite covers the same functionality with real tests. The pure-math conformallab_tests target now only contains active tests.
  • 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.
  • main branch is protected on origin (Gitea). Push to dev, then merge via pull request. Codeberg main can 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.