Files
ConformalLabpp/CLAUDE.md
Tarik Moussa 279f964b96
Some checks failed
C++ Tests / test-fast (push) Successful in 2m35s
C++ Tests / test-cgal (push) Failing after 2m5s
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 <noreply@anthropic.com>
2026-05-17 20:58:52 +02:00

251 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](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:
> 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 17 (done):** Direct port of the Java library algorithms to C++
- **Phase 89 (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:
```bash
# 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
```bash
# 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 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:
```cpp
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/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 — 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 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) | GaussBonnet: Σ(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
```cpp
// 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
```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 v1v2)
// 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<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 | `main`, `dev`, PRs 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**, **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.