Phase 4 complete — 87 CGAL tests pass, 2 skipped. Newton solver (phase4a): - hyper_ideal_hessian.hpp: symmetric FD Hessian (O(ε²), PSD by convexity) - newton_hyper_ideal(): Newton + backtracking for the HyperIdeal functional - detail::solve_with_fallback(): optional bool* fallback_used parameter - solve_linear_system(): public API exposing LDLT→SparseQR fallback SparseQR fallback tests (SparseQRFallback.*): - FullRankSystem_CorrectSolution: LDLT path, fallback_used=false - SingularMatrix_FallbackActivated: zero-pivot → QR activated, fallback_used=true - Euclidean_ClosedMeshNoPinConverges: gauge-mode null space handled via QR HyperIdeal Newton tests (NewtonSolver.HyperIdeal_*): - ConvergesTriangleAllVariable, ResultFieldsConsistent, ConvergesTetrahedron, SparseQRFallbackNoCrash - Natural-target base point (b=1.0, a=0.5) — x=0 is degenerate in log-space Pipeline tests (test_pipeline.cpp): - End-to-end: all three geometries, mesh I/O round-trip, solve+export Example programs (code/examples/): - example_euclidean.cpp: headless Euclidean pipeline - example_hyper_ideal.cpp: headless HyperIdeal pipeline - example_viewer.cpp: interactive libigl viewer with jet colour map README: - Mathematical scope table: C++ vs Java original (18 rows) - "For mathematicians" section: mental model, step-by-step new-functional guide, half-edge traversal snippets, recommended reading Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
conformallab++
conformallab++ is a modern C++ reimplementation of the ConformalLab software by Stefan Sechelmann for experiments in discrete conformal geometry and related mesh transformations.
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).
Features
| Area | Status |
|---|---|
| Clausen / Lobachevsky / ImLi₂ functions | ✅ Phase 1 |
| Hyper-ideal geometry (ζ, lᵢⱼ, αᵢⱼ, σᵢ, σᵢⱼ) | ✅ Phase 2 |
CGAL Surface_mesh infrastructure + mesh builders |
✅ Phase 3a |
| Hyper-ideal functional (energy + gradient) | ✅ Phase 3b |
| Spherical functional (energy + gradient + gauge-fix) | ✅ Phase 3c/3e |
| Euclidean functional (energy + gradient) | ✅ Phase 3d |
| Euclidean Hessian (cotangent-Laplace, Pinkall–Polthier) | ✅ Phase 3f |
| Spherical Hessian (∂α/∂u from law of cosines) | ✅ Phase 3f |
| Hyper-ideal Hessian (numerical FD, symmetrised) | ✅ Phase 4a |
| Newton solver — all three geometries | ✅ Phase 4a |
| SparseQR fallback for rank-deficient H (gauge modes) | ✅ Phase 4a |
| 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 |
Quick start — running the examples
cmake -S code -B build -DWITH_CGAL=ON
cmake --build build --target example_euclidean example_hyper_ideal
# Euclidean conformal map (headless)
./build/examples/example_euclidean [input.off] [output.off]
# HyperIdeal conformal map (headless)
./build/examples/example_hyper_ideal [input.off] [output.off]
# Interactive viewer (requires -DWITH_VIEWER=ON)
cmake -S code -B build -DWITH_CGAL=ON -DWITH_VIEWER=ON
cmake --build build --target example_viewer
./build/examples/example_viewer [input.off]
If no input file is given each example uses a built-in test mesh.
Library usage — minimal Euclidean pipeline
#include "conformal_mesh.hpp"
#include "mesh_builder.hpp"
#include "mesh_io.hpp"
#include "euclidean_functional.hpp"
#include "newton_solver.hpp"
using namespace conformallab;
int main() {
// 1. Load mesh
ConformalMesh mesh = load_mesh("input.off");
// 2. Set up functional maps
auto maps = setup_euclidean_maps(mesh);
compute_euclidean_lambda0_from_mesh(mesh, maps);
// 3. Assign DOFs — pin first vertex (gauge fix)
auto vit = mesh.vertices().begin();
maps.v_idx[*vit++] = -1; // pinned: u[v0] = 0
int idx = 0;
for (; vit != mesh.vertices().end(); ++vit)
maps.v_idx[*vit] = idx++;
const int n = idx;
// 4. Set target angles (natural equilibrium: x* = 0)
std::vector<double> x0(n, 0.0);
auto G0 = euclidean_gradient(mesh, x0, maps);
for (auto v : mesh.vertices()) {
int iv = maps.v_idx[v];
if (iv >= 0) maps.theta_v[v] -= G0[iv];
}
// 5. Solve
auto result = newton_euclidean(mesh, std::vector<double>(n, -0.1), maps);
// 6. Save result
if (result.converged)
save_mesh("output.off", mesh);
return result.converged ? 0 : 1;
}
For HyperIdeal geometry:
auto maps = setup_hyper_ideal_maps(mesh);
int n = assign_all_dof_indices(mesh, maps);
// … set theta_v / theta_e targets …
auto result = newton_hyper_ideal(mesh, x0, maps);
Using solve_linear_system directly (with fallback detection):
#include "newton_solver.hpp"
bool used_fallback = false;
auto dx = conformallab::solve_linear_system(H, rhs, &used_fallback);
if (used_fallback)
std::cout << "SparseQR was used (H is rank-deficient)\n";
Build modes
| 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 |
External dependencies are bundled as tarballs in code/deps/tarballs/ and extracted lazily at CMake configure time (GTest is fetched via FetchContent).
Boost is required only with -DWITH_CGAL=ON (header-only use by CGAL 6.x).
Prerequisites
| Tool | Minimum |
|---|---|
| C++ compiler (GCC or Clang) | C++17 |
| CMake | 3.20 |
| Boost headers | 1.70 (only with -DWITH_CGAL=ON) |
Getting started
git clone https://codeberg.org/TMoussa/ConformalLabpp
cd ConformalLabpp
Tests only (CI default — no system deps)
cmake -S code -B build
cmake --build build --target conformallab_tests -j$(nproc)
ctest --test-dir build --output-on-failure
CGAL tests + headless examples (needs system Boost)
cmake -S code -B build -DWITH_CGAL=ON
cmake --build build --target conformallab_cgal_tests example_euclidean example_hyper_ideal -j$(nproc)
ctest --test-dir build -R "^cgal\." --output-on-failure
./build/examples/example_euclidean
./build/examples/example_hyper_ideal
Expected: 87 tests pass, 2 skipped (the two @Ignore Hessian stubs).
Interactive viewer
cmake -S code -B build -DWITH_CGAL=ON -DWITH_VIEWER=ON
cmake --build build --target example_viewer -j$(nproc)
./build/examples/example_viewer data/off/example.off
Public headers (code/include/)
| Header | Description |
|---|---|
clausen.hpp |
Clausen Cl₂, Lobachevsky Л, ImLi₂ |
hyper_ideal_geometry.hpp |
ζ functions, lᵢⱼ, αᵢⱼ, σᵢ, σᵢⱼ |
hyper_ideal_utility.hpp |
Tetrahedron volume (Meyerhoff / Kolpakov–Mednykh) |
hyper_ideal_functional.hpp |
HyperIdeal energy + gradient on ConformalMesh |
hyper_ideal_hessian.hpp |
HyperIdeal Hessian (numerical FD, symmetrised) |
hyper_ideal_visualization_utility.hpp |
Poincaré disk projection, circumcircle helpers |
spherical_geometry.hpp |
Spherical arc length, half-angle formula |
spherical_functional.hpp |
Spherical energy + gradient + gauge-fix |
spherical_hessian.hpp |
Spherical Hessian (∂α/∂u, law of cosines) |
euclidean_geometry.hpp |
Euclidean corner-angle (t-value / atan2) |
euclidean_functional.hpp |
Euclidean energy + gradient |
euclidean_hessian.hpp |
Cotangent-Laplace Hessian (Pinkall–Polthier) |
newton_solver.hpp |
newton_euclidean / newton_spherical / newton_hyper_ideal + public solve_linear_system |
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 |
mesh_utils.hpp |
CGAL → Eigen conversion (cgal_to_eigen) |
constants.hpp |
conformallab::PI, TWO_PI |
Project structure
code/
├── include/ # All public headers (header-only library)
│ ├── conformal_mesh.hpp
│ ├── mesh_builder.hpp
│ ├── mesh_io.hpp
│ ├── mesh_utils.hpp
│ ├── newton_solver.hpp # ← public solve_linear_system + 3 Newton solvers
│ ├── hyper_ideal_{functional,hessian,geometry,utility,visualization_utility}.hpp
│ ├── spherical_{functional,hessian,geometry}.hpp
│ ├── euclidean_{functional,hessian,geometry}.hpp
│ ├── clausen.hpp
│ └── constants.hpp
├── examples/ # Standalone example programs
│ ├── CMakeLists.txt
│ ├── example_euclidean.cpp # Headless Euclidean pipeline
│ ├── example_hyper_ideal.cpp # Headless HyperIdeal pipeline
│ └── example_viewer.cpp # Interactive libigl viewer (WITH_VIEWER)
├── src/
│ ├── apps/v0/conformallab_cli.cpp # CLI skeleton (Phase 5)
│ └── viewer/simple_viewer.cpp
├── tests/
│ ├── CMakeLists.txt
│ ├── *.cpp # conformallab_tests (no CGAL)
│ └── cgal/
│ ├── CMakeLists.txt
│ ├── test_conformal_mesh.cpp # 14 tests
│ ├── test_hyper_ideal_functional.cpp # 7 tests (1 skipped)
│ ├── test_spherical_functional.cpp # 11 tests (1 skipped)
│ ├── test_euclidean_functional.cpp # 11 tests
│ ├── test_euclidean_hessian.cpp # 8 tests
│ ├── 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
└── deps/
├── eigen-3.4.0/ # always extracted
├── CGAL-6.1.1/ # extracted with WITH_CGAL
├── libigl-2.6.0/ # extracted with WITH_VIEWER
├── glfw-3.4/ # extracted with WITH_VIEWER
├── libigl-glad/
└── single_includes/ # CLI11, json.hpp
Test suites
conformallab_tests (CI — always built)
Pure-math tests requiring only Eigen: Clausen / Lobachevsky / ImLi₂, hyper-ideal geometry, tetrahedron volumes.
conformallab_cgal_tests (local — -DWITH_CGAL=ON)
| Suite | Tests | What it checks |
|---|---|---|
ConformalMeshTopology |
4 | Euler characteristic, vertex/edge/face counts |
ConformalMeshTraversal |
4 | Halfedge iteration, valence, opposite |
ConformalMeshProperties |
5 | Property maps (λ, θ, idx, α, geometry type) |
ConformalMeshValidity |
1 | CGAL validity for all factory meshes |
HyperIdealFunctional |
7 | FD gradient checks + Hessian symmetry |
SphericalFunctional |
11 | Angle formula + gradient + gauge-fix |
EuclideanFunctional |
11 | Angle formula + gradient |
EuclideanHessian |
8 | Cotangent-Laplace structure, FD agreement, PSD, null space |
SphericalHessian |
8 | Derivative correctness, NSD at equilibrium |
NewtonSolver |
11 | Convergence (Euclidean ×3, Spherical ×4, HyperIdeal ×4) |
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) |
Newton solver & SparseQR fallback
newton_solver.hpp exposes three solvers with a unified interface:
NewtonResult newton_euclidean (mesh, x0, maps [, tol, max_iter])
NewtonResult newton_spherical (mesh, x0, maps [, tol, max_iter])
NewtonResult newton_hyper_ideal(mesh, x0, maps [, tol, max_iter, hess_eps])
Each iteration:
- Evaluate gradient G
- Evaluate Hessian H (analytical for Euclidean / Spherical; numerical FD for HyperIdeal)
- Solve H·Δx = −G — try
Eigen::SimplicialLDLT, fall back toEigen::SparseQRon failure - Backtracking line search (up to 20 halvings)
Gradient sign conventions:
| Geometry | G | H sign |
|---|---|---|
| Euclidean | Θ_v − Σα_v | PSD → LDLT on H |
| Spherical | Θ_v − Σα_v | NSD → LDLT on −H |
| HyperIdeal | Σβ_v − Θ_v | PSD → LDLT on H |
SparseQR fallback (solve_linear_system):
When SimplicialLDLT fails (singular/rank-deficient H, e.g. gauge modes on closed meshes without a pinned vertex), SparseQR finds the minimum-norm Newton step orthogonal to the null space. Because the gradient always lies in the row space of H, the solver converges correctly without requiring the caller to pin a vertex.
The fallback is a public API:
bool used_fallback = false;
auto dx = conformallab::solve_linear_system(H, rhs, &used_fallback);
Mathematical scope — C++ vs. Java original
The three core functionals are fully equivalent to the Java original at the level of energy, gradient, and Hessian formulas. The table below shows where parity holds, where there is a numerical difference, and what is not yet ported.
| Mathematical layer | Java ConformalLab | conformallab++ |
|---|---|---|
| Euclidean functional — energy, gradient | ✅ | ✅ |
| Spherical functional — energy, gradient, gauge-fix | ✅ | ✅ |
| HyperIdeal functional — energy, gradient | ✅ | ✅ |
| Inversive-distance functional (Luo 2004, Bowers–Stephenson) | ✅ | ❌ not ported |
| Euclidean Hessian — cotangent-Laplace (Pinkall–Polthier 1993) | ✅ analytical | ✅ analytical |
| Spherical Hessian — ∂α/∂u from law of cosines | ✅ analytical | ✅ analytical |
| HyperIdeal Hessian — through ζ → lᵢⱼ → β/α chain | ✅ analytical | ⚠️ symmetric FD |
| Newton solver | ✅ | ✅ |
| 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 |
| Gauss–Bonnet consistency check on target angles | ✅ | ❌ |
| Global uniformization for genus g ≥ 1 | ✅ | ❌ |
| Period matrices — Teichmüller parameters for g ≥ 2 | ✅ | ❌ |
| Holonomy / monodromy | ✅ | ❌ |
| HyperIdeal generator — constructing geometrically valid meshes | ✅ | ❌ only test meshes |
| 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 |
| Interactive viewer | ✅ jReality | ✅ libigl/GLFW |
Key numerical difference — HyperIdeal Hessian
The analytical Hessian of the HyperIdeal functional requires differentiating through the chain
(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:
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
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.
For mathematicians — extending the library
This section explains how to add new functionals, test conjectures numerically, and hook into the existing solver infrastructure, with no assumed prior knowledge of the codebase.
Mental model
The library is built around one central idea: a discrete conformal functional E(x) whose critical points are the conformally equivalent metrics. Everything else is infrastructure for evaluating E, its gradient G = ∂E/∂x, and its Hessian H = ∂²E/∂x².
ConformalMesh — half-edge mesh (CGAL::Surface_mesh)
+ property maps — per-vertex / per-edge data (λ, θ, α, DOF index, …)
Maps struct — collects all property maps for one functional
theta_v[v] — target angle at vertex v (your input)
v_idx[v] — DOF index, or −1 if pinned
e_idx[e] — DOF index for edge DOFs (HyperIdeal only)
x ∈ ℝⁿ — the DOF vector the solver optimises
evaluate_*(mesh, x, maps) → { energy, gradient, … }
newton_*(mesh, x0, maps) → { x*, iterations, converged, … }
The mesh geometry (vertex positions) is only used to initialise the log edge-lengths λ°. From then on the solver works entirely in the x-space.
Adding a new functional — step-by-step
Copy euclidean_functional.hpp as a template (it is the simplest of the three). You need to provide:
1. A Maps struct that holds the property maps your functional needs:
// my_functional.hpp
#pragma once
#include "conformal_mesh.hpp"
namespace conformallab {
struct MyMaps {
// property maps attached to the mesh
ConformalMesh::Property_map<Vertex_index, double> lambda; // log edge-lengths
ConformalMesh::Property_map<Vertex_index, double> theta_v; // target angles
ConformalMesh::Property_map<Vertex_index, int> v_idx; // DOF indices
// any extra parameters your functional needs
double my_parameter = 1.0;
};
inline MyMaps setup_my_maps(ConformalMesh& mesh) { … }
2. An energy + gradient function:
struct MyResult {
double energy;
std::vector<double> gradient;
};
inline MyResult evaluate_my_functional(
ConformalMesh& mesh,
const std::vector<double>& x,
const MyMaps& m,
bool compute_energy = true)
{
MyResult res;
res.gradient.assign(x.size(), 0.0);
for (auto f : mesh.faces()) {
// iterate halfedges around face
// compute your per-face contribution to E and G
// accumulate: res.gradient[m.v_idx[v]] += …
}
// subtract target-angle term
for (auto v : mesh.vertices()) {
int iv = m.v_idx[v];
if (iv < 0) continue;
res.gradient[iv] -= m.theta_v[v]; // G_v = actual - target
}
return res;
}
3. A gradient check — before trusting your formula, verify it numerically. There is a ready-made helper in hyper_ideal_functional.hpp you can call directly, or write your own:
// Finite-difference gradient check for any functional
bool my_gradient_check(ConformalMesh& mesh,
const std::vector<double>& x,
const MyMaps& m,
double eps = 1e-6, double tol = 1e-5)
{
auto r0 = evaluate_my_functional(mesh, x, m, false);
const int n = static_cast<int>(x.size());
for (int i = 0; i < n; ++i) {
auto xp = x; xp[i] += eps;
auto xm = x; xm[i] -= eps;
double fd = (evaluate_my_functional(mesh, xp, m).energy
- evaluate_my_functional(mesh, xm, m).energy) / (2*eps);
if (std::abs(fd - r0.gradient[i]) > tol * (1 + std::abs(fd)))
return false;
}
return true;
}
Add a TEST(MyFunctional, GradientCheck_Triangle) in tests/cgal/ and it will be picked up automatically by CTest.
4. Hook into the Newton solver. Once your gradient and Hessian are correct, plug in solve_linear_system or write a thin wrapper in the style of newton_euclidean:
// Use a numerical Hessian first (safe starting point)
#include "newton_solver.hpp"
#include <Eigen/Sparse>
// Build H by FD of your gradient, then:
bool ok = false;
auto dx = detail::solve_with_fallback(H, -G, ok);
Or supply an analytical Hessian as a sparse matrix and pass it directly.
Where the key mathematical objects live
| Object | File | What to look for |
|---|---|---|
| Corner angle formula (Euclidean) | euclidean_geometry.hpp |
euclidean_corner_angle() — inputs are log half-edge lengths |
| Spherical angle formula | spherical_geometry.hpp |
spherical_corner_angle() — uses spherical law of cosines |
| HyperIdeal angle (ζ₁₃/ζ₁₄/ζ₁₅) | hyper_ideal_geometry.hpp |
zeta13/14/15(), alpha_ij() — the four vertex-type cases |
| Per-face energy term | *_functional.hpp |
the inner loop over mesh.faces() |
| Gradient accumulation | *_functional.hpp |
grad[v_idx[v]] += … after the face loop |
| Cotangent-Laplace structure | euclidean_hessian.hpp |
euclidean_hessian() — shows the sparse-triplet pattern |
| Spherical Hessian derivation | spherical_hessian.hpp |
comments give the ∂α/∂u formula step by step |
| Special functions | clausen.hpp |
Cl2(), lobachevsky(), imLi2() — all take a double angle |
How to navigate the half-edge mesh
for (auto f : mesh.faces()) {
// The three halfedges of face f:
auto h0 = mesh.halfedge(f);
auto h1 = mesh.next(h0);
auto h2 = mesh.next(h1);
// Vertices opposite to each halfedge (the vertex NOT on h):
Vertex_index v0 = mesh.target(h2); // opposite to edge h0-h1
Vertex_index v1 = mesh.target(h0); // opposite to edge h1-h2
Vertex_index v2 = mesh.target(h1); // opposite to edge h0-h2 (= h2 target)
// Access DOF index (−1 = pinned):
int i0 = maps.v_idx[v0];
// The opposite halfedge (for the adjacent face, if not on boundary):
auto h_opp = mesh.opposite(h0);
bool is_boundary = mesh.is_border(h_opp);
}
Attaching new data to a mesh
// Add a per-vertex curvature field (survives mesh copy):
auto [curv, created] = mesh.add_property_map<Vertex_index, double>("v:my_curv", 0.0);
// Write and read:
curv[v] = 1.234;
double k = curv[v];
// Pass it through your Maps struct so functions can access it.
Property maps are reference-counted and cheap to copy. Give them unique string names to avoid collision.
Quick-start experiment checklist
- Read
examples/example_euclidean.cpp— it shows the full pipeline in ~80 lines with comments at every step. - Build without a viewer first:
cmake -S code -B build -DWITH_CGAL=ON && cmake --build build --target example_euclidean. - Add a gradient check test in
tests/cgal/— copy anyTEST(…, GradientCheck_…)block and swap out the functional. Run withctest -R your_test_name. - Try different target angles — set
maps.theta_v[v] = M_PI / 3for all interior vertices and see how the solver responds. The constraintΣ(2π − Θ_v) = 2π·χ(M)(Gauss–Bonnet) must hold for a solution to exist. - Inspect convergence —
NewtonResultcarriesiterations,grad_inf_norm, and the fullxat termination. Plot||G(xₖ)||per iteration to verify quadratic convergence near the solution.
Recommended reading
| Paper | Relevance to this codebase |
|---|---|
| Springborn, Schröder, Pinkall — Conformal Equivalence of Triangle Meshes (2008) | Euclidean & spherical functionals; the Schläfli formula at the core of spherical_functional.hpp |
| Springborn — Ideal Hyperbolic Polyhedra and Discrete Uniformization (2020) | HyperIdeal functional; the ζ₁₃/ζ₁₄/ζ₁₅ functions in hyper_ideal_geometry.hpp |
| Pinkall, Polthier — Computing Discrete Minimal Surfaces (1993) | Cotangent-Laplace Hessian in euclidean_hessian.hpp |
| Luo — Combinatorial Yamabe Flow on Surfaces (2004) | Inversive-distance functional (not yet ported — good first contribution) |
| Bobenko, Springborn — Variational Principles for Circle Patterns (2004) | Background for the angle-sum variational framework used throughout |
Key design decisions
CGAL as CoHDS replacement. CGAL::Surface_mesh<Point3> replaces the Java CoHDS half-edge data structure. Vertex/edge/face/halfedge descriptors are typed integers — no raw handles, no RTTI.
Property maps. mesh.add_property_map<Vertex_index, double>("v:lambda", 0.0) replaces the Java adapter/decorator pattern. Multiple maps attach to one mesh without subclassing.
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.
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.
Natural theta trick. Tests set theta_v = Σα_v(x=x_base) to make x_base the known equilibrium, avoiding any need to manufacture reference solutions. For HyperIdeal x_base = (b=1.0, a=0.5) is used (x=0 is degenerate in log-space).
CI
Tests run automatically on push to main, dev, and claude/** branches via a self-hosted Gitea Actions runner (eulernest, ARM64). The CI image contains cmake, g++, git, and Node.js. Only conformallab_tests runs in CI (no Boost/CGAL dependency there).
# Rebuild and push the CI image when the Dockerfile changes
docker buildx build \
--platform linux/arm64 \
-f .gitea/docker/Dockerfile.ci-cpp \
-t git.eulernest.eu/conformallab/ci-cpp:latest \
--push \
.gitea/docker/
Roadmap
Phase 1 Clausen / Lobachevsky / ImLi₂ ✅ abgeschlossen
Phase 2 Hyper-ideal Geometrie (ζ, lᵢⱼ, αᵢⱼ, σᵢ) ✅ abgeschlossen
Phase 3a CGAL Surface_mesh Infrastruktur ✅ abgeschlossen
Phase 3b HyperIdealFunctional ✅ abgeschlossen
Phase 3c SphericalFunctional ✅ abgeschlossen
Phase 3d EuclideanCyclicFunctional ✅ abgeschlossen
Phase 3e Gauge-Fix für SphericalFunctional ✅ abgeschlossen
Phase 3f Analytische Hessians (Eucl. + Sphär.) ✅ abgeschlossen
Phase 3g PI-Konstante konsolidieren ✅ abgeschlossen
Phase 4a Newton-Solver (alle drei Geometrien) ✅ abgeschlossen
→ newton_euclidean / newton_spherical / newton_hyper_ideal
→ detail::solve_with_fallback → public solve_linear_system
→ Backtracking-Line-Search
→ hyper_ideal_hessian.hpp (numerischer FD-Hessian)
Phase 4b CGAL::IO Mesh-Import/Export ✅ abgeschlossen
→ mesh_io.hpp: read/write/load/save
→ Format-Erkennung aus Dateiendung (OFF, OBJ, PLY)
Phase 4c End-to-End-Pipeline Tests ✅ abgeschlossen
→ test_pipeline.cpp: 5 Tests (alle 3 Geometrien, I/O, full loop)
Phase 4d SparseQR-Fallback + Beispiel-Programme ✅ abgeschlossen
→ solve_linear_system als öffentliche API mit fallback_used-Flag
→ 3 dedizierte SparseQR-Tests (full-rank, singular, closed mesh)
→ examples/example_euclidean.cpp (headless)
→ 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
License
conformallab++ is released under the MIT License (see LICENSE).