Files
ConformalLabpp/CLAUDE.md
Tarik Moussa 26405be149
Some checks failed
C++ Tests / test-fast (push) Successful in 2m29s
C++ Tests / test-cgal (push) Failing after 1m51s
docs: add CLAUDE.md — context file for Claude Code
Covers build commands (all three modes), single-test invocation,
header-only architecture, the three geometry modes, Newton solver
sign conventions, layout BFS design, test patterns, CI structure,
and known quirks (Finder ` 2.hpp` duplicates, CGAL compile flags).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 20:47:01 +02:00

7.3 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this project is

conformallab++ is a C++ reimplementation of Stefan Sechelmann's ConformalLab Java library (TU Berlin, 2016). It solves one precise problem: given a triangulated surface, find a conformally equivalent metric satisfying prescribed curvature (angle-sum) constraints at each vertex. All code lives in code/.

The algorithmic foundation is Sechelmann's dissertation:

Variational Methods for Discrete Surface Parameterization: Applications and Implementation, TU Berlin 2016. DOI: 10.14279/depositonce-5415

Build commands

All three modes share the same source root code/:

# Mode 1 — fast tests only (no CGAL, no Boost, no display)
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 mode, requires Boost headers, no wayland/display)
# 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, requires Wayland/X11 dev headers)
cmake -S code -B build -DWITH_CGAL=ON
cmake --build build -j$(nproc)

Important: -DWITH_CGAL=ON automatically enables -DWITH_VIEWER=ON, which pulls in GLFW and requires wayland-scanner. Use -DWITH_CGAL_TESTS=ON for headless environments.

Running a single test

# Non-CGAL test by name
ctest --test-dir build -R "Clausen" --output-on-failure

# CGAL test by name (prefix is always "cgal.")
ctest --test-dir build -R "cgal.NewtonSolver" --output-on-failure

# Or run the binary directly for full GTest output
./build/conformallab_cgal_tests --gtest_filter="NewtonSolver*"
./build/conformallab_tests       --gtest_filter="Clausen*"

Architecture

Everything is header-only

All algorithms live in code/include/*.hpp. There is no compiled library. The CMake targets (conformallab_tests, conformallab_cgal_tests, conformallab_core) all compile the headers directly via their test or app .cpp files.

Central type: ConformalMesh

Defined in conformal_mesh.hpp:

using ConformalMesh = CGAL::Surface_mesh<Point3>;  // CGAL::Simple_cartesian<double>

Algorithms attach data via CGAL named property maps rather than intrusive vertex/edge types (replacing the Java CoHDS/CoVertex/CoEdge pattern). Property map naming convention:

  • "v:lambda" — per-vertex log scale factor (the conformal variable uᵢ)
  • "v:theta" — per-vertex target cone angle
  • "v:idx" — solver DOF index (-1 = pinned/boundary)
  • "e:alpha" — per-edge intersection angle (hyperbolic geometry only)

The three geometry modes

Each mode has its own Maps struct + functional + Hessian header:

Mode Headers Maps struct Newton function
Euclidean (ℝ²) euclidean_functional.hpp, euclidean_hessian.hpp EuclideanMaps newton_euclidean()
Spherical (S²) spherical_functional.hpp, spherical_hessian.hpp SphericalMaps newton_spherical()
Hyper-ideal (H²) hyper_ideal_functional.hpp, hyper_ideal_hessian.hpp HyperIdealMaps newton_hyper_ideal()

Setup follows the same pattern for all three:

EuclideanMaps maps = setup_euclidean_maps(mesh);
compute_euclidean_lambda0_from_mesh(mesh, maps);   // initialise λ° from 3-D positions
maps.v_idx[*mesh.vertices().begin()] = -1;         // pin one vertex (gauge fix)
int idx = 0;
for (auto v : mesh.vertices()) if (maps.v_idx[v] != -1) maps.v_idx[v] = idx++;

After compute_*_lambda0_from_mesh() the solver works entirely in scale-factor space; original vertex positions are no longer used.

Pipeline

load_mesh()                    → ConformalMesh
setup_*_maps()                 → *Maps  (property maps attached)
compute_*_lambda0_from_mesh()  → λ° initialised
check/enforce_gauss_bonnet()   → Σ(2πΘᵥ) = 2π·χ(M)  [mandatory for closed meshes]
newton_*()                     → NewtonResult.x  (converged scale factors)
compute_cut_graph()            → CutGraph  (2g seam edges, closed meshes only)
euclidean/spherical/hyper_ideal_layout()  → Layout2D/3D + HolonomyData
normalise_*()                  → canonical position
compute_period_matrix()        → PeriodData  τ∈ℍ  (genus 1 flat torus)
compute_fundamental_domain()   → FundamentalDomain + tiling
save_result_json/xml()         → serialised result

Newton solver (newton_solver.hpp)

Sign conventions differ between modes — the solver handles this internally:

  • Euclidean/HyperIdeal: G_v = actual target, H is PSD → SimplicialLDLT(H)
  • Spherical: G_v = target actual, H is NSD → SimplicialLDLT(H)

When SimplicialLDLT fails (rank-deficient H on closed meshes without pinned vertex), the solver automatically retries with SparseQR to find the minimum-norm step. This is the gauge-mode fallback — public API: solve_linear_system(H, rhs, &used_fallback).

Layout (layout.hpp)

BFS-trilateration using a min-heap on BFS depth (priority BFS). Root face = largest 3-D area face. Key output fields:

  • layout.uv[v.idx()] — primary UV (first/shallowest BFS visit)
  • layout.halfedge_uv[h.idx()] — UV of source(h) as seen from face(h) — seam-aware for GPU texture atlasing
  • hol.translations[i] — lattice generators ωᵢ (Euclidean/spherical)
  • hol.mobius_maps[i] — Möbius isometry Tᵢ ∈ SU(1,1) (hyperbolic)

Test design pattern

Tests use the "natural theta" trick to construct a known equilibrium at x* = 0:

// Evaluate gradient at x=0, use actual angle sums as targets → x*=0 by construction
evaluate_euclidean_gradient(mesh, x0, maps);
for (auto v : mesh.vertices())
    if (maps.v_idx[v] >= 0)
        maps.theta_v[v] = /* actual angle sum from gradient evaluation */;

Gradient checks use finite differences: perturb xᵢ ± ε, verify G(x) ≈ ∂E/∂x.

CI pipeline

Two jobs defined in .gitea/workflows/cpp-tests.yml:

  • test-fast — no flags, Eigen+GTest only, runs on all branches
  • test-cgal-DWITH_CGAL_TESTS=ON, requires Boost (installed at runtime until Docker image is rebuilt), runs on main/dev/PRs only, needs test-fast to pass first

Docker image: git.eulernest.eu/conformallab/ci-cpp:latest (Ubuntu 22.04 ARM64). Dockerfile at .gitea/docker/Dockerfile.ci-cpp.

Known issues / conventions

  • Files ending in 2.hpp (e.g. clausen 2.hpp, hyper_ideal_utility 2.hpp) are macOS Finder duplicates — ignore them, use the canonical name without 2.
  • CGAL_DISABLE_GMP and CGAL_DISABLE_MPFR are defined for all CGAL targets — CGAL runs in exact-predicates-inexact-constructions mode with Simple_cartesian<double>, which is intentional (conformal geometry does not need exact arithmetic).
  • All deps are bundled as tarballs in code/deps/tarballs/ and extracted at CMake configure time — no internet access needed at build time except for GTest (fetched via FetchContent).
  • The test-fast job intentionally includes stubs that call GTEST_SKIP() — 2 intentional skips are expected in the CGAL suite, not regressions.