From 279f964b96c72f944c1266192236aea6d17a6ed9 Mon Sep 17 00:00:00 2001 From: Tarik Moussa Date: Sun, 17 May 2026 20:58:52 +0200 Subject: [PATCH] docs(claude): rewrite CLAUDE.md with full project context Incorporates README, architecture doc, and Java ConformalLab source structure. Adds: - Long-term CGAL package goal made explicit - Language policy: all code/comments/docs in English - Java-to-C++ porting table (what is done, what is Phase 9, what is Phase 10) - Java class names as reference anchors for future porting work - "Natural theta" and gradient-check test patterns - Halfedge traversal conventions - CI job table with expected pass/skip counts - Both remotes (origin + Codeberg) sync requirement - Known quirks (Finder duplicates, protected main, Boost header-only) Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 258 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 182 insertions(+), 76 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4e57a12..a9e27d2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,143 +2,249 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## What this project is +## Project purpose and long-term goal -conformallab++ is a C++ reimplementation of Stefan Sechelmann's [ConformalLab](https://github.com/varylab/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/`. +conformallab++ is a C++17 reimplementation of [ConformalLab](https://github.com/varylab/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: -The algorithmic foundation is Sechelmann's dissertation: -> *Variational Methods for Discrete Surface Parameterization: Applications and Implementation*, TU Berlin 2016. DOI: 10.14279/depositonce-5415 +> Stefan Sechelmann — *Variational Methods for Discrete Surface Parameterization: Applications and Implementation*, TU Berlin 2016. +> DOI: [10.14279/depositonce-5415](https://depositonce.tu-berlin.de/items/8e2988b2-d991-45b5-aad5-9fb7988f3b2f) · 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 three modes share the same source root `code/`: +All source lives under `code/`. Three build modes: ```bash -# Mode 1 — fast tests only (no CGAL, no Boost, no display) +# 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 mode, requires Boost headers, no wayland/display) -# macOS: brew install boost Linux: apt install libboost-dev +# 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, requires Wayland/X11 dev headers) +# 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) ``` -**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. +`-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 ```bash -# 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 +# 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 + +```bash +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 CMake targets (`conformallab_tests`, `conformallab_cgal_tests`, `conformallab_core`) all compile the headers directly via their test or app `.cpp` files. +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` -Defined in `conformal_mesh.hpp`: +`conformal_mesh.hpp` defines the core type: ```cpp using ConformalMesh = CGAL::Surface_mesh; // CGAL::Simple_cartesian ``` -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) + +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` (floating-point, no exact arithmetic) because conformal geometry does not require exact predicates. ### The three geometry modes -Each mode has its own Maps struct + functional + Hessian header: +Each mode has its own Maps struct that bundles all property maps, plus functional, Hessian, and Newton function: -| 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()` | +| 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()` | -Setup follows the same pattern for all three: -```cpp -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. +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. -### Pipeline +### The full 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 +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`) -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)` +The gradient sign convention differs between modes: +- **Euclidean/HyperIdeal:** `G_v = actual_angle_sum − Θ_v`, H is PSD → `SimplicialLDLT(H)` +- **Spherical:** `G_v = Θ_v − actual_angle_sum`, 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)`. +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)`. -### Layout (`layout.hpp`) +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. -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) +### Layout and holonomy (`layout.hpp`) -### Test design pattern +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](https://github.com/varylab/conformallab) and use it as the reference implementation. + +## Test design patterns + +### "Natural theta" — constructing a known equilibrium at x* = 0 -Tests use the **"natural theta"** trick to construct a known equilibrium at `x* = 0`: ```cpp -// Evaluate gradient at x=0, use actual angle sums as targets → x*=0 by construction -evaluate_euclidean_gradient(mesh, x0, maps); +// Evaluate gradient at x=0; set target angles = actual angle sums → x*=0 by definition +std::vector 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] = /* actual angle sum from gradient evaluation */; + 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 + +```cpp +// 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 + +```cpp +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 + +```cpp +auto [my_map, created] = mesh.add_property_map("v:my_data", 0.0); +my_map[v] = 3.14; ``` -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 +Two jobs in `.gitea/workflows/cpp-tests.yml`: -Docker image: `git.eulernest.eu/conformallab/ci-cpp:latest` (Ubuntu 22.04 ARM64). Dockerfile at `.gitea/docker/Dockerfile.ci-cpp`. +| Job | CMake flags | Deps | Triggers on | +|---|---|---|---| +| `test-fast` | *(none)* | Eigen + GTest only | all branches | +| `test-cgal` | `-DWITH_CGAL_TESTS=ON` | + Boost | `main`, `dev`, PRs only | -## Known issues / conventions +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`). -- 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`, 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. +Expected results: **36 non-CGAL tests pass**, **158 CGAL tests pass, 2 skipped** (intentional `GTEST_SKIP` stubs for genus-2 homology and analytic HyperIdeal Hessian — these are Java features deferred to Phase 9). + +## Known quirks + +- **Finder duplicate files**: `include/` contains files like `clausen 2.hpp`, `hyper_ideal_utility 2.hpp` — macOS Finder duplicates. Always use the canonical name without ` 2`. These should be deleted. +- **`test-fast` also runs stubs**: `conformallab_tests` (non-CGAL) contains `GTEST_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. +- **`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.