feat(phase5): Layout, CLI, JSON/XML serialisation — 95 tests
Phase 5 complete: layout.hpp - euclidean_layout(): BFS unfolding in ℝ² using trilaterate_2d - spherical_layout(): BFS on S² using trilaterate_sph (spherical law of cosines) - hyper_ideal_layout(): BFS in Poincaré disk (tanh(d/2) Euclidean approx) - save_layout_off(): convenience OFF writer for 2-D and 3-D layouts serialization.hpp - save/load_result_json(): nlohmann/json; stores DOF vector + uv/pos layout - save/load_result_xml(): hand-written writer/parser; same schema conformallab_cli.cpp (rewritten) - CLI11 interface: -i/-o/-g/-j/-x/-s/-v - Dispatches to euclidean / spherical / hyper_ideal pipeline - Runs Newton, computes layout, saves OFF + JSON + XML examples/example_layout.cpp - Full round-trip demo: solve → layout → JSON/XML → reload → verify tests/cgal/test_layout.cpp (8 tests) - Euclidean_PreservesEdgeLengths, CorrectVertexCount, TriangleIsNonDegenerate - Spherical_PreservesArcLengths, PositionsOnUnitSphere - HyperIdeal_SuccessAndFinitePositions - Serialization.JSON_RoundTrip, XML_RoundTrip All 95 CGAL tests pass (2 skipped — Hessian stubs unchanged). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
135
README.md
135
README.md
@@ -4,7 +4,7 @@ conformallab++ is a modern C++ reimplementation of the [ConformalLab](https://gi
|
||||
|
||||
The long-term goal is a **CGAL package** that brings discrete conformal maps (hyper-ideal, spherical, Euclidean) to the CGAL ecosystem using `CGAL::Surface_mesh` as the underlying half-edge data structure.
|
||||
|
||||
> **Status:** Phase 4 vollständig abgeschlossen. Alle drei Geometrien lösbar via Newton-Solver (SimplicialLDLT + SparseQR-Fallback). Interaktiver Viewer-Beispiel inklusive. **87 Tests, 2 skipped**. Nächster Schritt: Phase 5 (CLI-App + Serialisierung).
|
||||
> **Status:** Phase 5 vollständig abgeschlossen. Alle drei Geometrien lösbar via Newton-Solver (SimplicialLDLT + SparseQR-Fallback). BFS-Layout in ℝ²/S²/Poincaré-Disk, JSON/XML-Serialisierung, vollständige CLI-App. **95 Tests, 2 skipped**.
|
||||
|
||||
---
|
||||
|
||||
@@ -26,29 +26,43 @@ The long-term goal is a **CGAL package** that brings discrete conformal maps (hy
|
||||
| Mesh I/O (CGAL::IO — OFF / OBJ / PLY) | ✅ Phase 4b |
|
||||
| End-to-end pipeline tests | ✅ Phase 4c |
|
||||
| **Example programs** (headless + interactive viewer) | ✅ Phase 4d |
|
||||
| CLI app + XML serialisation | 🔜 Phase 5 |
|
||||
| **BFS Layout** (ℝ², S², Poincaré disk) | ✅ Phase 5 |
|
||||
| **CLI app** (`conformallab_core`) | ✅ Phase 5 |
|
||||
| **JSON + XML serialisation** | ✅ Phase 5 |
|
||||
|
||||
---
|
||||
|
||||
## Quick start — running the examples
|
||||
## Quick start — CLI app
|
||||
|
||||
```bash
|
||||
cmake -S code -B build -DWITH_CGAL=ON
|
||||
cmake --build build --target example_euclidean example_hyper_ideal
|
||||
cmake --build build -j4
|
||||
|
||||
# Euclidean conformal map (headless)
|
||||
./build/examples/example_euclidean [input.off] [output.off]
|
||||
# Run the conformal map CLI on any OFF/OBJ/PLY mesh
|
||||
./bin/conformallab_core -i input.off -g euclidean -o layout.off -j result.json -x result.xml
|
||||
|
||||
# HyperIdeal conformal map (headless)
|
||||
./build/examples/example_hyper_ideal [input.off] [output.off]
|
||||
# Available geometries: euclidean | spherical | hyper_ideal
|
||||
./bin/conformallab_core -i input.off -g spherical -o sphere.off
|
||||
|
||||
# Interactive viewer (requires -DWITH_VIEWER=ON)
|
||||
cmake -S code -B build -DWITH_CGAL=ON -DWITH_VIEWER=ON
|
||||
cmake --build build --target example_viewer
|
||||
# Show input mesh in interactive viewer (built-in)
|
||||
./bin/conformallab_core -i input.off -s
|
||||
```
|
||||
|
||||
### Example programs
|
||||
|
||||
```bash
|
||||
# Layout + JSON/XML round-trip demo
|
||||
./build/examples/example_layout [input.off] [layout.off] [result.json] [result.xml]
|
||||
|
||||
# Headless pipelines
|
||||
./build/examples/example_euclidean [input.off] [output.off]
|
||||
./build/examples/example_hyper_ideal [input.off] [output.off]
|
||||
|
||||
# Interactive viewer (requires -DWITH_CGAL=ON, viewer is built automatically)
|
||||
./build/examples/example_viewer [input.off]
|
||||
```
|
||||
|
||||
If no input file is given each example uses a built-in test mesh.
|
||||
If no input file is given each example uses a built-in quad-strip mesh.
|
||||
|
||||
---
|
||||
|
||||
@@ -107,6 +121,39 @@ int n = assign_all_dof_indices(mesh, maps);
|
||||
auto result = newton_hyper_ideal(mesh, x0, maps);
|
||||
```
|
||||
|
||||
### Layout (embedding into target space)
|
||||
|
||||
After solving, convert scale factors into actual vertex coordinates:
|
||||
|
||||
```cpp
|
||||
#include "layout.hpp"
|
||||
#include "serialization.hpp"
|
||||
|
||||
// Euclidean: flat 2-D positions in ℝ²
|
||||
Layout2D layout = euclidean_layout(mesh, result.x, maps);
|
||||
// layout.uv[v.idx()] = Eigen::Vector2d
|
||||
|
||||
// Spherical: unit vectors on S² ⊂ ℝ³
|
||||
Layout3D slayout = spherical_layout(mesh, result.x, smaps);
|
||||
// slayout.pos[v.idx()] = Eigen::Vector3d
|
||||
|
||||
// HyperIdeal: Poincaré disk coordinates in ℝ²
|
||||
Layout2D hlayout = hyper_ideal_layout(mesh, result.x, hmaps);
|
||||
|
||||
// Save layout as OFF
|
||||
save_layout_off("layout.off", mesh, layout);
|
||||
|
||||
// Serialise to JSON / XML
|
||||
save_result_json("result.json", result, "euclidean",
|
||||
mesh.number_of_vertices(), mesh.number_of_faces(), &layout);
|
||||
save_result_xml("result.xml", result, "euclidean",
|
||||
mesh.number_of_vertices(), mesh.number_of_faces(), &layout);
|
||||
|
||||
// Load back
|
||||
NewtonResult res2; std::string geom; Layout2D uv2;
|
||||
auto x = load_result_json("result.json", &res2, &geom, &uv2);
|
||||
```
|
||||
|
||||
Using **`solve_linear_system`** directly (with fallback detection):
|
||||
|
||||
```cpp
|
||||
@@ -125,8 +172,8 @@ if (used_fallback)
|
||||
| Mode | CMake flags | What gets built | CI |
|
||||
|------|------------|-----------------|-----|
|
||||
| **Tests only** (default) | *(none)* | `conformallab_tests` — Eigen + GTest only | ✅ automatic |
|
||||
| **CGAL tests + examples** | `-DWITH_CGAL=ON` | `conformallab_cgal_tests`, `example_euclidean`, `example_hyper_ideal` | local only |
|
||||
| **Interactive viewer** | `-DWITH_CGAL=ON -DWITH_VIEWER=ON` | above + `example_viewer`, `conformallab_core` | local only |
|
||||
| **CGAL tests + examples** | `-DWITH_CGAL=ON` | `conformallab_cgal_tests`, `example_euclidean`, `example_hyper_ideal`, `example_layout`, `conformallab_core` (CLI) | local only |
|
||||
| **Interactive viewer** | `-DWITH_CGAL=ON` *(implied)* | above + `example_viewer` + viewer linked into CLI | local only |
|
||||
|
||||
External dependencies are bundled as tarballs in `code/deps/tarballs/` and extracted lazily at CMake configure time (GTest is fetched via `FetchContent`).
|
||||
|
||||
@@ -163,19 +210,21 @@ ctest --test-dir build --output-on-failure
|
||||
|
||||
```bash
|
||||
cmake -S code -B build -DWITH_CGAL=ON
|
||||
cmake --build build --target conformallab_cgal_tests example_euclidean example_hyper_ideal -j$(nproc)
|
||||
cmake --build build -j$(nproc)
|
||||
ctest --test-dir build -R "^cgal\." --output-on-failure
|
||||
./build/examples/example_euclidean
|
||||
./build/examples/example_hyper_ideal
|
||||
./build/examples/example_layout
|
||||
./bin/conformallab_core -i input.off -g euclidean -o layout.off -j result.json
|
||||
```
|
||||
|
||||
Expected: **87 tests pass, 2 skipped** (the two `@Ignore` Hessian stubs).
|
||||
Expected: **95 tests pass, 2 skipped** (the two `@Ignore` Hessian stubs).
|
||||
|
||||
### Interactive viewer
|
||||
|
||||
```bash
|
||||
cmake -S code -B build -DWITH_CGAL=ON -DWITH_VIEWER=ON
|
||||
cmake --build build --target example_viewer -j$(nproc)
|
||||
# WITH_CGAL=ON already implies viewer; example_viewer is built automatically
|
||||
cmake -S code -B build -DWITH_CGAL=ON && cmake --build build -t example_viewer -j$(nproc)
|
||||
./build/examples/example_viewer data/off/example.off
|
||||
```
|
||||
|
||||
@@ -201,6 +250,8 @@ cmake --build build --target example_viewer -j$(nproc)
|
||||
| `mesh_io.hpp` | `read_mesh` / `write_mesh` / `load_mesh` / `save_mesh` |
|
||||
| `conformal_mesh.hpp` | `ConformalMesh` = `CGAL::Surface_mesh<Point3>` + property-map helpers |
|
||||
| `mesh_builder.hpp` | `make_triangle` / `make_tetrahedron` / `make_quad_strip` / `make_fan` / `make_spherical_tetrahedron` |
|
||||
| `layout.hpp` | `euclidean_layout` / `spherical_layout` / `hyper_ideal_layout` → `Layout2D/3D`; BFS unfolding |
|
||||
| `serialization.hpp` | `save/load_result_json` + `save/load_result_xml` |
|
||||
| `mesh_utils.hpp` | CGAL → Eigen conversion (`cgal_to_eigen`) |
|
||||
| `constants.hpp` | `conformallab::PI`, `TWO_PI` |
|
||||
|
||||
@@ -216,6 +267,8 @@ code/
|
||||
│ ├── mesh_io.hpp
|
||||
│ ├── mesh_utils.hpp
|
||||
│ ├── newton_solver.hpp # ← public solve_linear_system + 3 Newton solvers
|
||||
│ ├── layout.hpp # ← BFS layout (euclidean/spherical/hyper_ideal)
|
||||
│ ├── serialization.hpp # ← JSON + XML save/load
|
||||
│ ├── hyper_ideal_{functional,hessian,geometry,utility,visualization_utility}.hpp
|
||||
│ ├── spherical_{functional,hessian,geometry}.hpp
|
||||
│ ├── euclidean_{functional,hessian,geometry}.hpp
|
||||
@@ -225,9 +278,10 @@ code/
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── example_euclidean.cpp # Headless Euclidean pipeline
|
||||
│ ├── example_hyper_ideal.cpp # Headless HyperIdeal pipeline
|
||||
│ ├── example_layout.cpp # Solve → layout → OFF/JSON/XML round-trip
|
||||
│ └── example_viewer.cpp # Interactive libigl viewer (WITH_VIEWER)
|
||||
├── src/
|
||||
│ ├── apps/v0/conformallab_cli.cpp # CLI skeleton (Phase 5)
|
||||
│ ├── apps/v0/conformallab_cli.cpp # Full CLI app (Phase 5)
|
||||
│ └── viewer/simple_viewer.cpp
|
||||
├── tests/
|
||||
│ ├── CMakeLists.txt
|
||||
@@ -242,7 +296,8 @@ code/
|
||||
│ ├── test_spherical_hessian.cpp # 8 tests
|
||||
│ ├── test_newton_solver.cpp # 14 tests (incl. 3 SparseQR tests)
|
||||
│ ├── test_mesh_io.cpp # 6 tests
|
||||
│ └── test_pipeline.cpp # 5 tests
|
||||
│ ├── test_pipeline.cpp # 5 tests
|
||||
│ └── test_layout.cpp # 8 tests (layout + JSON/XML)
|
||||
└── deps/
|
||||
├── eigen-3.4.0/ # always extracted
|
||||
├── CGAL-6.1.1/ # extracted with WITH_CGAL
|
||||
@@ -277,7 +332,9 @@ Pure-math tests requiring only Eigen: Clausen / Lobachevsky / ImLi₂, hyper-ide
|
||||
| `SparseQRFallback` | 3 | Full-rank LDLT path · singular matrix triggers QR · closed-mesh gauge-mode |
|
||||
| `MeshIO` | 6 | OFF/OBJ round-trips, error handling |
|
||||
| `Pipeline` | 5 | End-to-end: build → setup → solve → export → reload, all three geometries |
|
||||
| **Total** | **87** | 2 skipped (Hessian stubs) |
|
||||
| `Layout` | 6 | Edge-length preservation (Euclidean/Spherical), arc-lengths on S², Poincaré disk |
|
||||
| `Serialization` | 2 | JSON and XML round-trips (DOF + layout) |
|
||||
| **Total** | **95** | 2 skipped (Hessian stubs) |
|
||||
|
||||
---
|
||||
|
||||
@@ -334,7 +391,7 @@ The three core functionals are fully equivalent to the Java original at the leve
|
||||
| SparseQR fallback for gauge-mode null spaces | ? | ✅ |
|
||||
| **Cone metrics** — prescribed Θ_v ≠ 2π | ✅ full pipeline | ⚠️ data structure only |
|
||||
| **Boundary conditions** — Dirichlet u=f, Neumann, free boundary | ✅ | ⚠️ pin-only |
|
||||
| **Layout / embedding** — DOF vector → vertex coordinates in ℝ² / H² / S² | ✅ | ❌ not implemented |
|
||||
| **Layout / embedding** — DOF vector → vertex coordinates in ℝ² / H² / S² | ✅ | ✅ BFS unfolding (all three geometries) |
|
||||
| **Gauss–Bonnet consistency check** on target angles | ✅ | ❌ |
|
||||
| **Global uniformization** for genus g ≥ 1 | ✅ | ❌ |
|
||||
| **Period matrices** — Teichmüller parameters for g ≥ 2 | ✅ | ❌ |
|
||||
@@ -343,7 +400,7 @@ The three core functionals are fully equivalent to the Java original at the leve
|
||||
| Clausen / Lobachevsky / ImLi₂ special functions | ✅ | ✅ |
|
||||
| Discrete elliptic utility (modular normalisation of τ) | ✅ | ✅ (not yet wired up) |
|
||||
| Poincaré disk / Lorentz boost visualisation helpers | ✅ | ✅ |
|
||||
| Mesh I/O | ✅ XML/CoHDS | ✅ OFF/OBJ/PLY |
|
||||
| Mesh I/O + serialisation | ✅ XML/CoHDS | ✅ OFF/OBJ/PLY + JSON/XML |
|
||||
| Interactive viewer | ✅ jReality | ✅ libigl/GLFW |
|
||||
|
||||
### Key numerical difference — HyperIdeal Hessian
|
||||
@@ -354,7 +411,7 @@ The analytical Hessian of the HyperIdeal functional requires differentiating thr
|
||||
(b_i, a_e) → l_ij → ζ₁₃/ζ₁₄/ζ₁₅ → α_ij / β_i
|
||||
```
|
||||
|
||||
which is feasible but involves many nested cases (four vertex-type combinations per edge). Until Phase 5 delivers the analytical version, conformallab++ uses a symmetric finite-difference Hessian:
|
||||
which is feasible but involves many nested cases (four vertex-type combinations per edge). Until Phase 6 delivers the analytical version, conformallab++ uses a symmetric finite-difference Hessian:
|
||||
|
||||
```
|
||||
H[i,j] = ( G(x + ε·eⱼ)[i] − G(x − ε·eⱼ)[i] ) / (2ε)
|
||||
@@ -362,11 +419,13 @@ H[i,j] = ( G(x + ε·eⱼ)[i] − G(x − ε·eⱼ)[i] ) / (2ε)
|
||||
|
||||
This is O(ε²) accurate (≈ 10⁻¹⁰ relative error at ε = 10⁻⁵), positive semi-definite by strict convexity of the HyperIdeal energy (Springborn 2020), and costs n extra gradient evaluations per Newton step instead of O(n). For meshes with fewer than ~500 DOFs the difference in wall time is negligible.
|
||||
|
||||
### What "cone metrics" and "layout" would require
|
||||
### What "cone metrics" still requires
|
||||
|
||||
**Cone metrics** — the property map `theta_v` is already subtracted in the gradient (`G_v = Σα_v − Θ_v`), so prescribing a cone angle is a one-liner: `maps.theta_v[v] = desired_angle`. What is missing is the *application layer*: checking Gauss–Bonnet consistency (Σ (2π − Θ_v) = 2π·χ), distributing angle defects sensibly, and special handling at boundary vertices.
|
||||
|
||||
**Layout** — after solving you have the conformal scale factors u_i but the vertex positions in the mesh are unchanged. Recovering the actual flat / hyperbolic / spherical coordinates requires integrating the discrete holomorphic differential (discrete Schwarz–Christoffel for the Euclidean case, or geodesic development for the hyperbolic case). This is the single biggest missing step for a complete uniformization pipeline.
|
||||
**Layout (Phase 5 ✅)** — `layout.hpp` implements BFS unfolding for all three geometries via `euclidean_layout`, `spherical_layout`, and `hyper_ideal_layout`. For open meshes the embedding is globally consistent; for closed meshes the first BFS visit wins and `has_seam = true` is set. To get a proper parameterisation of a closed mesh, cut it to a disk first (not yet implemented).
|
||||
|
||||
**Next steps** — global uniformization (cutting closed meshes, period matrices, holonomy), Gauss–Bonnet checking, analytical HyperIdeal Hessian.
|
||||
|
||||
---
|
||||
|
||||
@@ -547,8 +606,8 @@ Property maps are reference-counted and cheap to copy. Give them unique string n
|
||||
|
||||
### Quick-start experiment checklist
|
||||
|
||||
1. **Read** `examples/example_euclidean.cpp` — it shows the full pipeline in ~80 lines with comments at every step.
|
||||
2. **Build** without a viewer first: `cmake -S code -B build -DWITH_CGAL=ON && cmake --build build --target example_euclidean`.
|
||||
1. **Read** `examples/example_layout.cpp` — it shows the full pipeline (load → setup → solve → layout → JSON/XML save → reload) in ~120 lines with comments at every step.
|
||||
2. **Build** with `cmake -S code -B build -DWITH_CGAL=ON && cmake --build build --target example_layout` and run `./build/examples/example_layout`.
|
||||
3. **Add a gradient check test** in `tests/cgal/` — copy any `TEST(…, GradientCheck_…)` block and swap out the functional. Run with `ctest -R your_test_name`.
|
||||
4. **Try different target angles** — set `maps.theta_v[v] = M_PI / 3` for all interior vertices and see how the solver responds. The constraint `Σ(2π − Θ_v) = 2π·χ(M)` (Gauss–Bonnet) must hold for a solution to exist.
|
||||
5. **Inspect convergence** — `NewtonResult` carries `iterations`, `grad_inf_norm`, and the full `x` at termination. Plot `||G(xₖ)||` per iteration to verify quadratic convergence near the solution.
|
||||
@@ -573,7 +632,7 @@ Property maps are reference-counted and cheap to copy. Give them unique string n
|
||||
|
||||
**DOF vector convention.** All functionals use `x` indexed by `v_idx[v]` / `e_idx[e]` (−1 = pinned). This matches the Java `FunctionalTest` gradient-check convention and is uniform across all three geometries.
|
||||
|
||||
**HyperIdeal Hessian via FD.** The analytical Hessian through `ζ13/14/15 → lij → β/α` is left for Phase 5. A symmetric FD Hessian `H[i,j] = (G(x+ε·eⱼ)[i] − G(x−ε·eⱼ)[i]) / (2ε)` is O(ε²) accurate, PSD by strict convexity, and sufficient for Newton on < 500 DOFs.
|
||||
**HyperIdeal Hessian via FD.** The analytical Hessian through `ζ13/14/15 → lij → β/α` is deferred to Phase 6. A symmetric FD Hessian `H[i,j] = (G(x+ε·eⱼ)[i] − G(x−ε·eⱼ)[i]) / (2ε)` is O(ε²) accurate, PSD by strict convexity, and sufficient for Newton on < 500 DOFs.
|
||||
|
||||
**Spherical Hessian sign.** The spherical energy is **concave** (not convex) — the Hessian **H** is NSD at equilibrium. Newton solves `(−H)·Δx = G`, so the sign flip is handled transparently inside `newton_spherical`.
|
||||
|
||||
@@ -632,11 +691,19 @@ Phase 4d SparseQR-Fallback + Beispiel-Programme ✅ abgeschlossen
|
||||
→ examples/example_hyper_ideal.cpp (headless)
|
||||
→ examples/example_viewer.cpp (interaktiver Viewer, WITH_VIEWER)
|
||||
|
||||
Phase 5 CLI-App + Serialisierung (2–3 Tage)
|
||||
→ conformallab_core CLI: --input, --geometry, --output
|
||||
→ JSON/XML-Export für DOF-Vektoren und Solver-Ergebnisse
|
||||
→ Analytischer HyperIdeal-Hessian (direkte Ableitung)
|
||||
→ Integration aller drei Geometrien hinter einheitlicher CLI
|
||||
Phase 5 Layout + CLI + Serialisierung ✅ abgeschlossen
|
||||
→ layout.hpp: BFS-Einbettung in ℝ² (Euclidean/HyperIdeal) und S² (Spherical)
|
||||
→ serialization.hpp: JSON (nlohmann/json) + XML (hand-written) save/load
|
||||
→ conformallab_core CLI: -i/-o/-g/-j/-x/-s/-v Flags, alle drei Geometrien
|
||||
→ example_layout.cpp: Solve → Layout → OFF/JSON/XML + Round-Trip-Check
|
||||
→ test_layout.cpp: 8 Tests (Eucl./Sphär./HyperIdeal + JSON/XML)
|
||||
→ 95 Tests gesamt (2 skipped)
|
||||
|
||||
Phase 6 (geplant)
|
||||
→ Analytischer HyperIdeal-Hessian (direkte Ableitung durch ζ-Kette)
|
||||
→ Gauss–Bonnet Konsistenzprüfung für Kegelmetriken
|
||||
→ Mesh-Cut für geschlossene Flächen → globale Parameterisierung
|
||||
→ Inversive-Distance-Funktional (Luo 2004)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user