All checks were successful
C++ Tests / test-fast (pull_request) Successful in 1m57s
API Docs / doc-build (pull_request) Successful in 59s
Markdown link check / check (pull_request) Successful in 51s
C++ Tests / test-cgal (pull_request) Has been skipped
C++ Tests / quality-gates (pull_request) Successful in 2m19s
Add bit-for-bit (1e-12) golden-value oracle tests pinning the C++ pure-math and functional cores against the compiled upstream Java library (openjdk 17): - HyperIdealGoldenJava: Clausen/Л/ImLi2, ζ13/14/15/ζ, both tetrahedron-volume formulas (real de.varylab…Clausen / HyperIdealUtility). - EuclideanGoldenJava / SphericalGoldenJava: angle formulas + β relations + Л energy terms, plus FULL-MESH oracles driving the real EuclideanCyclicFunctional / SphericalFunctional on a shared tetrahedron — per-vertex gradient (Θ−Σα) and ΔE = E(x)−E(0) (C++ Gauss-Legendre path integral vs Java closed form). - SphericalGoldenJava.FullMeshEdgeDofGradient: edge-DOF gradient (vertex + edge components, α_opp⁺+α_opp⁻−θ_e) vs raw conformalEnergyAndGradient — locks Finding 3 at the solution level (audit items 4 & 5). - PeriodMatrix.NormalizeModulus_GoldenJava: τ-reduction fold convention vs the real DiscreteEllipticUtility.normalizeModulus (audit items 7 & 8). Subtlety documented: the spherical oracles call Java's raw conformalEnergyAndGradient, not evaluate() (which pre-runs a Brent gauge maximization that C++ factors into the Newton solver's spherical_gauge_shift). Also: - P1-2 (layout.hpp): Euclidean holonomy now uses a per-cut-edge rigid-motion fit g(z)=a·z+b, exposing residual_rotation = |arg(a)| as a diagnostic; non- regressive (flat case a=1 reduces to the old midpoint formula). - P1-3 (period_matrix.hpp): is_in_fundamental_domain fixed to the correct half-open SL(2,ℤ) domain (−½ ≤ Re < ½). Updated the now-exposed ComputePeriodMatrix_ReducedTau_InFD to assert the normalizeModulus domain (closed +½ edge) instead. Test counts (single source of truth = doc/api/tests.md): 272/272 pass, 0 skipped (26 non-CGAL + 246 CGAL). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
444 lines
34 KiB
Markdown
444 lines
34 KiB
Markdown
# 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 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:
|
||
|
||
```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.
|
||
|
||
### Compile-time options (v0.10.0 matrix)
|
||
|
||
The CGAL test build defaults to **PCH + Unity Build ON** (CGAL test wall-time 78 s → 55 s, CPU time 676 s → 167 s on Apple M1). Opt-in/opt-out flags — full measurements + macOS-vs-Linux notes in `doc/architecture/compile-time.md`:
|
||
|
||
| Flag | Default | Effect |
|
||
|---|---|---|
|
||
| `-DBUILD_TESTING=OFF` | ON | Skip the entire test subtree (headers-only consumers). |
|
||
| `-DCONFORMALLAB_USE_PCH=OFF` | ON | Disable precompiled headers for `conformallab_cgal_tests`. |
|
||
| `-DCMAKE_UNITY_BUILD=OFF` | ON | Disable Unity (jumbo) build. |
|
||
| `-DCONFORMALLAB_DEV_BUILD=ON` | OFF | Dev iteration: PCH on, Unity forced off (cheaper incremental rebuilds). |
|
||
| `-DCONFORMALLAB_FAST_TEST_BUILD=ON` | OFF | `-O0 -g` for tests (faster compile, slower run). |
|
||
| `-DCONFORMALLAB_USE_CCACHE=ON` | OFF | Route compiles through ccache. |
|
||
| `-DCONFORMALLAB_HEADERS_CHECK=ON` | OFF | Standalone header self-containment check target. |
|
||
|
||
### 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 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 | S² | 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](https://github.com/varylab/conformallab) and use it as the reference implementation.
|
||
|
||
**† High-precision requirement (Phase 9c / 10):** The Java uniformization classes (`FundamentalPolygon`, `CanonicalFormUtility`) use `RnBig`/`PnBig`/`P2Big` with `MathContext(50)` — 50 significant decimal digits. Reason: products of hyperbolic isometry generators grow exponentially, so `double` fails when verifying the group relation ∏gᵢ = Id. When porting, replicate this **locally** with `boost::multiprecision::cpp_dec_float_50` (or MPFR `mpreal`) — only inside the uniformization module, NOT globally and NOT in the Eigen solver. The core flattening (Newton/energy) stays `double` (see `conformal_mesh.hpp:45`).
|
||
|
||
## 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 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<Vertex_index, double>("v:my_data", 0.0);
|
||
my_map[v] = 3.14;
|
||
```
|
||
|
||
## CI pipeline
|
||
|
||
Three jobs in `.gitea/workflows/cpp-tests.yml`:
|
||
|
||
| Job | CMake flags | Deps | Triggers on | Status |
|
||
|---|---|---|---|---|
|
||
| `test-fast` | *(none)* | Eigen + GTest only | all branches | **active** |
|
||
| `test-cgal` | `-DWITH_CGAL_TESTS=ON` | + Boost | pull requests only | **DISABLED 2026-05-26** (`if: false`) |
|
||
| `quality-gates` | *(none)* | + codespell, shellcheck | all branches (`needs: test-fast`) | **active** |
|
||
|
||
Runner: `eulernest` — self-hosted Raspberry Pi, ARM64, Ubuntu 22.04. Docker image: `git.eulernest.eu/conformallab/ci-cpp:latest`. `test-cgal` and `quality-gates` both need `test-fast` to pass first (`needs: test-fast`).
|
||
|
||
`quality-gates` runs four required structural gates: `license-headers.sh`, `cgal-conventions.py`, `codespell.sh`, `shellcheck.sh --strict`. Seven more gates (clang-format, cmake-format, cppcheck, sanitizers, clang-tidy, multi-compiler, reproducible-build) are local-only — see `scripts/quality/README.md`.
|
||
|
||
**`test-cgal` is gated off** (`if: false`, see the DISABLED-2026-05-26 comment block in the workflow): the `-j1` CGAL build is too memory-heavy for the 1.6 GB runner and OOMs intermittently without exposing real regressions. Consequence: the two structural sub-gates that lived inside it — `scripts/check-test-counts.sh` and `scripts/try_it.sh` — are currently **un-gated**; run them locally before tagging a release. Re-enable by restoring `if: github.event_name == 'pull_request'`.
|
||
|
||
Two other workflows are also restricted to `workflow_dispatch:` only (auto-trigger disabled 2026-05-26 while the codeberg pages-branch push is being stabilised):
|
||
- `.gitea/workflows/doxygen-pages.yml` — publishes Doxygen HTML + reviewer hub to the codeberg `pages` branch.
|
||
- `.gitea/workflows/perf-compile-time.yml` — Linux ARM64 compile-time benchmark.
|
||
|
||
Expected results: `test-fast` + `quality-gates` green on every push, 0 skipped, 0 failed. The canonical counts live in `doc/api/tests.md` — do not hardcode them anywhere else (see [`doc/release-policy.md`](doc/release-policy.md)).
|
||
|
||
## Release state
|
||
|
||
Current release: **v0.10.0** (tag on `main`, released 2026-05-26 — the "reviewer-ready" release).
|
||
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). v0.10.0 added: 100 % Doxygen public-API coverage (396/396 symbols, 0 warnings), a 14-gate structural quality suite (4 required in CI), the reviewer materials package (`doc/reviewer/`), `output_uv_map` for 4 of 5 DCE entries, six new roadmap phases + three RESEARCH phases, and a six-mode compile-time workflow matrix. Numbers (single source of truth = `doc/api/tests.md`): **272/272 tests pass, 0 skipped** (26 non-CGAL + 246 CGAL). 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, `CHANGELOG.md` for full release notes, and `doc/roadmap/phases.md` for the phase plan.
|
||
|
||
## Phase 8 CGAL-package decisions (frozen 2026-05-19)
|
||
|
||
Locked architecture choices — full design in [`doc/api/cgal-package.md`](doc/api/cgal-package.md), locked-vs-flexible split in `doc/architecture/locked-vs-flexible.md`:
|
||
**MIT license** (no LGPL switch) · **Named Parameters** (`CGAL::parameters::...`) · default kernel **`Simple_cartesian<double>`** · **dual-layer wrapper** (`code/include/*.hpp` = implementation, `include/CGAL/*.h` = thin wrapper, no duplication) · target design is generic **`FaceGraph + HalfedgeGraph`** but ships Surface_mesh-only · upstream CGAL submission is pre-submission-ready, not bound (12+ month horizon). Phase 8 extensions (8a.2 generic FaceGraph, 8c manuals, 8d CGAL-format tests, 8e YAML pipeline) are deferred to on-demand.
|
||
|
||
## Port-vs-research maintenance rule
|
||
|
||
Before claiming something "ports X from Java", **verify empirically** — if zero matches, it is **new research** (→ `doc/roadmap/research-track.md` with citations, **not** `doc/roadmap/java-parity.md`):
|
||
|
||
```bash
|
||
find /Users/tarikmoussa/Desktop/conformallab -iname "*X*"
|
||
grep -r "ClassName" /Users/tarikmoussa/Desktop/conformallab/src
|
||
```
|
||
|
||
The 2026-05-21 audit corrected four mis-labels (now fixed): `InversiveDistanceFunctional`, both HyperIdeal Hessians (FD + analytic), and the inversive-distance tutorial were all wrongly tagged "Java port" — they are research (no Java parent; Java declares `hasHessian()==false`). Details in `research-track.md`.
|
||
|
||
### Java↔C++ math-correctness audit (2026-05-29)
|
||
|
||
Full line-by-line audit of all math-critical headers against `de.varylab.discreteconformal.*` lives in **`doc/reviewer/java-port-audit.md`** (read it before re-investigating any of these). All 11 findings are resolved or noted; the four that needed code changes are now ✅ FIXED and **all CGAL tests pass** (246 after the Euclidean holonomy/τ end-to-end tests, the spherical edge-DOF closed-form oracle, and the Java golden-value oracle suites — incl. three *full-mesh* oracles driving the real `EuclideanCyclicFunctional`/`SphericalFunctional` on a shared tetrahedron, one of them an edge-DOF gradient oracle — landed on top; see `doc/api/tests.md`):
|
||
|
||
- **Finding 3** (`spherical_functional.hpp`) — spherical edge-DOF now uses Java's *replacement* parameterization (`Λ = λ_e` when the edge is a DOF, via helper `spher_eff_lambda`); edge gradient is `α_opp⁺ + α_opp⁻ − θ_e` (dropped the extra `−(S_f⁺+S_f⁻)/2` term). Vertex-only path is bit-for-bit unchanged.
|
||
- **Finding 4** (`spherical_hessian.hpp`) — added an always-compiled `throw std::logic_error` edge-DOF guard (mirrors Finding 2 for Euclidean).
|
||
- **Finding 6** (`period_matrix.hpp`) — `compute_period_matrix` now calls the faithful `normalizeModulus` (matches Java oracle: `0 ≤ Re ≤ ½`, `Im ≥ 0`, `|τ| ≥ 1`); `reduce_to_fundamental_domain` retained for the canonical SL(2,ℤ) domain.
|
||
- **Finding 9** (`inversive_distance_functional.hpp`) — degenerate-face gradient now uses limiting angles instead of skipping (mirrors Finding 1); the genuinely-non-real `l*sq <= 0` skip is kept.
|
||
|
||
**Java golden-value oracles (2026-05-29, P0):** five suites now pin the C++ pure-math core bit-for-bit (1e-12) against the compiled Java library (openjdk 17, real `Clausen.Л` / `HyperIdealUtility` / `DiscreteEllipticUtility.normalizeModulus`): `HyperIdealGoldenJava` (Clausen/Л/ImLi₂, ζ₁₃/₁₄/₁₅/ζ, both tetrahedron-volume formulas), `EuclideanGoldenJava` (angle formula + 2·Л energy), `SphericalGoldenJava` (law-of-cosines angles + β relations + Л energy), and `PeriodMatrix.NormalizeModulus_GoldenJava` (audit missing-test item 7, ✅ done). Oracle harnesses live in `/tmp/oracle/*.java` (recipe in the audit doc).
|
||
|
||
**Full-mesh oracles (2026-05-29):** `EuclideanGoldenJava.FullMeshGradientAndEnergy_Tetrahedron` and `SphericalGoldenJava.FullMeshGradientAndEnergy_Tetrahedron` drive the *real* `EuclideanCyclicFunctional` / `SphericalFunctional` on a shared tetrahedron (`/tmp/oracle/{tet.obj,EucMeshOracle.java,SphereMeshOracle.java}`) and pin both the per-vertex gradient (`Θ−Σα`) and `ΔE = E(x)−E(0)` to 1e-12 — the energy cross-checks C++'s Gauss-Legendre *path integral* against Java's *closed-form* functional (audit missing-test item 5, ✅ done). **Finding surfaced:** the spherical oracle must call Java's raw `conformalEnergyAndGradient`, NOT `evaluate()` — the latter pre-runs a 1-D Brent maximization over the global-scale gauge (`maximizeInNegativeDirection`), which C++ deliberately factors into the Newton solver's `spherical_gauge_shift` instead (both correct; different factoring).
|
||
|
||
**Open follow-ups (tests only, not bugs):** (a) the spherical edge-DOF *gradient* is now Java-oracle'd at the solution level (`SphericalGoldenJava.FullMeshEdgeDofGradient_Tetrahedron` — vertex + edge components vs raw `conformalEnergyAndGradient`, locking Finding 3); the only remaining edge-DOF gap is *Newton-to-convergence* with edge DOFs, blocked by the Finding-4 spherical-Hessian guard (needs an FD-Hessian or guard relaxation first) — a solver feature, not a correctness gap; (b) inversive-distance degenerate-face robustness (item 10) has no Java oracle because the functional is research with no Java parent — only a NaN-free limiting-angle regression test applies. Build/test reminder: the CGAL build dir used for this audit is **`build-cgal`** at the *repo root* (not under `code/`), target `conformallab_cgal_tests`, filter `ctest -R '^cgal\.'`.
|
||
|
||
## Documentation map
|
||
|
||
38 documents across 7 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` |
|
||
| Compile-time measurements + six-mode workflow matrix (macOS-vs-Linux honesty notes) | `doc/architecture/compile-time.md` |
|
||
| Required vs optional dependencies + standalone-verification recipe | `doc/architecture/dependencies.md` |
|
||
| Which 12 architecture decisions are locked vs still flexible | `doc/architecture/locked-vs-flexible.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` |
|
||
|
||
### Reviewer materials (v0.10.0)
|
||
|
||
| Question | Document |
|
||
|---|---|
|
||
| One-page reviewer briefing | `doc/reviewer/briefing.md` |
|
||
| Seven scoped reviewer questions (Q1–Q7) | `doc/reviewer/questions.md` |
|
||
| Internal meeting agenda | `doc/reviewer/agenda.md` |
|
||
| Reviewer landing index | `doc/reviewer/README.md` |
|
||
| Hand-curated reviewer landing page (HTML, source-of-truth for codeberg pages) | `doc/reviewer/hub.html` |
|
||
|
||
The published hub lives at https://tmoussa.codeberg.page/ConformalLabpp/ (Doxygen index at `/doxygen.html`). See the "Codeberg pages" quirk below for how it is republished.
|
||
|
||
### 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).
|
||
|
||
## Agentic workflow patterns
|
||
|
||
Recommended loops when working in this repo. Prefer the cheapest gate that catches the class of error you just touched.
|
||
|
||
- **Add/port an algorithm**: new `.hpp` in `code/include/` → test in `code/tests/cgal/` (must include a gradient-check, see Test design patterns) → register in `code/tests/cgal/CMakeLists.txt` → build `conformallab_cgal_tests` → `ctest -R "^cgal\."`. Before claiming "ports X", run the empirical port-vs-research check above.
|
||
- **Inner dev loop** (fast iteration): `-DCONFORMALLAB_DEV_BUILD=ON` (PCH on, Unity off) for cheap incremental rebuilds; run a single suite via `--gtest_filter`. Switch back to the default (Unity on) for a final full build.
|
||
- **Before any commit**: run the four required gates locally — they mirror CI exactly and are seconds-cheap: `bash scripts/quality/license-headers.sh`, `python3 scripts/quality/cgal-conventions.py`, `bash scripts/quality/codespell.sh`, `bash scripts/quality/shellcheck.sh --strict`.
|
||
- **Before tagging a release**: also run the two now-un-gated structural gates (test-cgal is disabled in CI): `BUILD_DIR=build bash scripts/check-test-counts.sh` and `bash scripts/try_it.sh`. Update `CHANGELOG.md`, `CITATION.cff`, and the `doc/api/tests.md` counts (single source of truth).
|
||
- **Touching public-API headers**: rebuild Doxygen (`cmake --build build --target doc`) and re-check coverage (`bash scripts/doxygen-coverage.sh --threshold 100`); regenerate `doc/api/headers.md` via `python3 scripts/gen-headers-md.py` (or `bash scripts/regen-docs.sh`).
|
||
- **Landing to `main`** (origin is protected): branch → push to `origin` → open PR via `gh`/Gitea API → merge via API → also push `codeberg/main` directly → keep both remotes in sync.
|
||
- **Republishing the reviewer hub**: see the Codeberg `pages` quirk below — manual force-push of an orphan branch; verify the live URL with a cache-bust query.
|
||
- **Delegation**: this repo's heavy builds are slow on the ARM64 runner — when a task is genuinely parallelisable and independent, consider a background agent; otherwise handle inline. Always verify an agent's actual diff, not just its summary.
|
||
- **settings.json**: `.claude/settings.json` (committed) pre-allows the safe read/build/test/quality commands so they don't prompt, and denies destructive git on `main`. Extend the allowlist as new safe commands recur rather than re-approving each time.
|
||
|
||
### Token hygiene — session-cut (Tier 1, highest impact)
|
||
|
||
Every turn re-sends the whole conversation, so context accumulation is the largest avoidable cost.
|
||
|
||
- **One session per task.** After a task is done, start a fresh session (`/clear`) instead of pivoting to an unrelated task in the same thread — the old task's context is dead weight in every later turn.
|
||
- **Compact proactively at clean breakpoints.** Run `/compact <what matters>` when a task finishes and before the next starts, rather than waiting for auto-compaction at the limit (which you don't control).
|
||
- **Delegate read-heavy sweeps to a subagent.** An `Explore`/`Plan` subagent reads large amounts in *its* context and returns a short summary — the bulk never enters the main context. Use it for "where is X used / what depends on Y"; for a *known* path, `Read` directly (a subagent starts cold and only pays off on a large search space).
|
||
|
||
### Token hygiene — command discipline (Tier 2)
|
||
|
||
- **Scope + filter together.** Path-scope searches (`grep -rn "newton_" code/include/`, not repo-wide). Filter test output (`ctest -R "cgal.NewtonSolver" --output-on-failure`, or `--gtest_filter` for one suite) instead of dumping all 272 results.
|
||
- **Never read raw logs into context.** Redirect to a file, then `grep`/`tail` it. With `run_in_background`, read the output file selectively rather than pulling it whole.
|
||
- **Don't re-read a file you just edited.** `Edit` errors on stale state — the harness tracks it; a `Read`-back after a successful edit is wasted context.
|
||
- **Reference by line number** (`newton_solver.hpp:147`) instead of re-pasting code blocks.
|
||
|
||
Cache discipline (Tier 3) is a user-facing guide — see [`.claude/token-hygiene.md`](.claude/token-hygiene.md). **Remind the user of the relevant Tier-3 rule when you observe the matching anti-pattern** (mid-session CLAUDE.md edits, chained sub-5-minute waits, long multi-topic sessions).
|
||
|
||
## 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, SSH). Push to both after every significant change.
|
||
- **Codeberg `pages` branch is an orphan publish target** that serves the reviewer hub + Doxygen HTML at https://tmoussa.codeberg.page/ConformalLabpp/. Its source-of-truth is `doc/reviewer/hub.html` (installed as `index.html`) + a local Doxygen build (`cmake --build build --target doc`, demoted to `/doxygen.html`). Because the auto-publish workflow (`doxygen-pages.yml`) is on `workflow_dispatch:` only, the branch is **not** refreshed on normal pushes and has been accidentally lost during force-push/merge cleanups. To republish manually: build Doxygen, copy `doc/doxygen/html/.` into a scratch dir, `mv index.html doxygen.html`, copy `hub.html` → `index.html`, then `git init -b pages && git commit && git push -f codeberg pages:pages`. Codeberg pages caches for ~10 min (`Cache-Control: max-age=600`) — verify with a cache-busting `?cb=$(date +%s)` query. Protect the branch in codeberg settings to prevent deletion (leave force-push allowed so CI/manual republish still works).
|
||
- **Both `main` branches are independent for the pages cycle**: `origin/main` is protected (PR-only); `codeberg/main` can be pushed directly.
|