README.md: reduced from 703 to ~75 lines — what/why, status, quick start, minimal usage example, navigation table to doc/ files. doc/architecture/overall_pipeline.md: trimmed — roadmap, extension points, declarative pipeline YAML, and references sections removed (each now has its own dedicated file). Replaced with a link table. New files: doc/getting-started.md — build modes, single-test invocation, CLI doc/api/pipeline.md — full pipeline API with code for all 3 geometries doc/api/extending.md — new functionals, geometry modes, Java porting guide doc/api/contracts.md — processing unit preconditions/provides table doc/api/cgal-package.md — Phase 8 CGAL package design + YAML pipeline (TODO) doc/math/geometry-modes.md — Euclidean/Spherical/HyperIdeal comparison doc/math/references.md — all papers by module doc/roadmap/phases.md — Phases 1–10 with porting/research boundary doc/roadmap/java-parity.md — Java vs C++ feature parity table doc/contributing.md — language policy, test standards, release flow Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
5.9 KiB
5.9 KiB
Pipeline API Reference
The full pipeline from mesh loading to layout and serialisation. See doc/architecture/overall_pipeline.md for the Mermaid diagram and stage descriptions.
Minimal Euclidean pipeline
#include "conformal_mesh.hpp"
#include "mesh_io.hpp"
#include "euclidean_functional.hpp"
#include "gauss_bonnet.hpp"
#include "newton_solver.hpp"
#include "layout.hpp"
using namespace conformallab;
ConformalMesh mesh = load_mesh("input.off");
// 1. Set up property maps and initialise log-edge-lengths from 3-D positions
EuclideanMaps maps = setup_euclidean_maps(mesh);
compute_euclidean_lambda0_from_mesh(mesh, maps);
// 2. Assign DOF indices — pin first vertex (gauge fix for open meshes)
auto vit = mesh.vertices().begin();
maps.v_idx[*vit++] = -1; // pinned: u_v = 0
int idx = 0;
for (; vit != mesh.vertices().end(); ++vit)
maps.v_idx[*vit] = idx++;
// 3. Set target angles — "natural equilibrium" makes x* = 0
std::vector<double> x0(idx, 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]];
// 4. Check Gauss–Bonnet (mandatory before solving)
check_gauss_bonnet(mesh, maps); // throws if Σ(2π−Θᵥ) ≠ 2π·χ(M)
// 5. Solve
NewtonResult res = newton_euclidean(mesh, x0, maps);
// res.converged, res.x, res.iterations, res.grad_inf_norm
// 6. Layout
Layout2D layout = euclidean_layout(mesh, res.x, maps);
// layout.uv[v.idx()], layout.halfedge_uv[h.idx()]
Layout with holonomy (closed surfaces)
#include "cut_graph.hpp"
#include "period_matrix.hpp"
#include "fundamental_domain.hpp"
// Tree-cotree cut graph — 2g seam edges
CutGraph cg = compute_cut_graph(mesh);
// cg.cut_edge_flags[e.idx()] — true if seam edge
// cg.genus — topological genus g
// Layout with holonomy tracking
HolonomyData hol;
Layout2D layout = euclidean_layout(mesh, res.x, maps, &cg, &hol, /*normalise=*/true);
// hol.translations[i] — lattice generator ωᵢ ∈ ℂ (Euclidean)
// Period matrix (genus 1 flat torus)
PeriodData pd = compute_period_matrix(hol);
// pd.tau — complex period ratio τ = ω₂/ω₁ ∈ ℍ
// pd.omega — {ω₁, ω₂} as complex<double>
// pd.in_fundamental_domain — true if SL(2,ℤ)-reduced to |τ|≥1, |Re(τ)|<½
// Fundamental domain parallelogram
FundamentalDomain fd = compute_fundamental_domain(hol);
// fd.vertices[0..3] — CCW corners: {0, ω₁, ω₁+ω₂, ω₂}
// fd.generators[0..1] — ω₁, ω₂
// Tiling copies for universal cover visualisation
auto tiles = tiling_neighbourhood(layout, hol, /*m_max=*/2, /*n_max=*/2);
// returns (2·m_max+1)·(2·n_max+1) translated layout copies
HyperIdeal pipeline
#include "hyper_ideal_functional.hpp"
#include "newton_solver.hpp"
#include "layout.hpp"
#include "cut_graph.hpp"
HyperIdealMaps maps = setup_hyper_ideal_maps(mesh);
compute_hyper_ideal_lambda0_from_mesh(mesh, maps);
// HyperIdeal has both vertex and edge DOFs — assign all automatically
int n_dofs = assign_all_dof_indices(mesh, maps);
// No vertex needs to be pinned — strictly convex functional
std::vector<double> x0(n_dofs, 0.0);
NewtonResult res = newton_hyper_ideal(mesh, x0, maps);
// Layout with Möbius holonomy
CutGraph cg = compute_cut_graph(mesh);
HolonomyData hol;
Layout2D layout = hyper_ideal_layout(mesh, res.x, maps, &cg, &hol);
// hol.mobius_maps[i] — Möbius isometry Tᵢ ∈ SU(1,1) per cut edge
Spherical pipeline
#include "spherical_functional.hpp"
#include "newton_solver.hpp"
#include "layout.hpp"
SphericalMaps maps = setup_spherical_maps(mesh);
compute_spherical_lambda0_from_mesh(mesh, maps);
// Pin one vertex (gauge fix) + assign DOFs
auto vit = mesh.vertices().begin();
maps.v_idx[*vit++] = -1;
int idx = 0;
for (; vit != mesh.vertices().end(); ++vit)
maps.v_idx[*vit] = idx++;
std::vector<double> x0(idx, 0.0);
NewtonResult res = newton_spherical(mesh, x0, maps);
// 3-D layout on the unit sphere
Layout3D slayout = spherical_layout(mesh, res.x, maps);
// slayout.xyz[v.idx()] — 3-D position on S²
SparseQR fallback — direct use
#include "newton_solver.hpp"
bool used_fallback = false;
Eigen::VectorXd dx = conformallab::solve_linear_system(H, rhs, &used_fallback);
if (used_fallback)
std::cerr << "Warning: H is rank-deficient, used SparseQR\n";
The fallback is automatically invoked by all three newton_* functions when
SimplicialLDLT fails. It finds the minimum-norm Newton step orthogonal to the null space.
Serialisation
#include "serialization.hpp"
#include "mesh_io.hpp"
// Save layout as OFF mesh file
save_layout_off("layout.off", mesh, layout);
// Full result: DOF vector + metadata + layout UVs
save_result_json("result.json", res, "euclidean", mesh, &layout);
save_result_xml ("result.xml", res, "euclidean", mesh, &layout);
// Round-trip load
NewtonResult res2;
std::string geom;
Layout2D uv2;
load_result_json("result.json", &res2, &geom, &uv2);
Attaching custom property maps
// Add a custom per-vertex map
auto [curv, created] = mesh.add_property_map<Vertex_index, double>("v:my_curv", 0.0);
curv[v] = 3.14;
// Retrieve an existing map
auto [curv2, found] = mesh.property_map<Vertex_index, double>("v:my_curv");
Halfedge traversal conventions
for (auto f : mesh.faces()) {
auto h0 = mesh.halfedge(f); // canonical halfedge of face f
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 halfedge h0 (edge v1–v2)
// h_alpha[h0] = α₃, h_alpha[h1] = α₁, h_alpha[h2] = α₂
Edge_index e0 = mesh.edge(h0);
bool is_seam = cg.cut_edge_flags[e0.idx()];
bool is_boundary = mesh.is_border(mesh.opposite(h0));
}