Files
ConformalLabpp/code/examples/example_hyper_ideal.cpp
Tarik Moussa 3f124eb071 feat(phase4): HyperIdeal Newton solver, SparseQR fallback, examples, docs
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>
2026-05-13 00:11:25 +02:00

148 lines
6.2 KiB
C++
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.

// example_hyper_ideal.cpp
//
// conformallab++ — Hyper-ideal discrete conformal map (headless example)
//
// Demonstrates the full library pipeline for the HYPER-IDEAL discrete conformal
// functional (Springborn 2020). The hyper-ideal functional operates in
// hyperbolic geometry: vertices have "horoball radii" (DOF b_i) and edges have
// "intersection lengths" (DOF a_e). The energy is strictly convex, so Newton
// converges globally from any valid starting point.
//
// Pipeline:
// 1. Load (or synthesise) a triangle mesh
// 2. Set up HyperIdeal maps + assign all vertex and edge DOFs
// 3. Choose equilibrium base point (b=1.0, a=0.5) and set natural targets
// 4. Perturb and solve with Newton
// 5. Print DOF values at equilibrium
// 6. Save result mesh
//
// Build (requires -DWITH_CGAL=ON):
// cmake -S code -B build -DWITH_CGAL=ON
// cmake --build build --target example_hyper_ideal
// ./build/examples/example_hyper_ideal [input.off] [output.off]
#include "conformal_mesh.hpp"
#include "mesh_builder.hpp"
#include "mesh_io.hpp"
#include "hyper_ideal_functional.hpp"
#include "newton_solver.hpp"
#include <iostream>
#include <string>
#include <vector>
#include <cmath>
using namespace conformallab;
int main(int argc, char* argv[])
{
// ── Step 1: obtain mesh ───────────────────────────────────────────────
ConformalMesh mesh;
std::string input_path = (argc > 1) ? argv[1] : "";
std::string output_path = (argc > 2) ? argv[2] : "/tmp/conformallab_hyper_ideal_out.off";
if (input_path.empty()) {
std::cout << "[example_hyper_ideal] No input file — using make_triangle().\n";
mesh = make_triangle();
} else {
std::cout << "[example_hyper_ideal] Loading mesh from: " << input_path << "\n";
try { mesh = load_mesh(input_path); }
catch (const std::exception& e) {
std::cerr << "Error loading mesh: " << e.what() << "\n";
return 1;
}
}
std::cout << "[example_hyper_ideal] Mesh: "
<< mesh.number_of_vertices() << " vertices, "
<< mesh.number_of_faces() << " faces.\n";
// ── Step 2: set up functional maps ────────────────────────────────────
auto maps = setup_hyper_ideal_maps(mesh);
int n = assign_all_dof_indices(mesh, maps);
std::cout << "[example_hyper_ideal] DOFs: " << n
<< " (" << mesh.number_of_vertices() << " vertex + "
<< mesh.number_of_edges() << " edge).\n";
// ── Step 3: choose equilibrium base point and set natural targets ─────
//
// x = 0 is degenerate for the HyperIdeal functional (log-space).
// We pick a valid base point (b_i = b_base, a_e = a_base), evaluate
// the gradient there, and absorb it into the target angles so that
// G(xbase) = 0. This makes xbase the equilibrium x*.
//
// In a real application you would set theta_v / theta_e to the desired
// hyperbolic angle targets (e.g. from a reference mesh).
const double b_base = 1.0; // horoball radii at equilibrium
const double a_base = 0.5; // edge-length DOFs at equilibrium
const auto sz = static_cast<std::size_t>(n);
std::vector<double> xbase(sz, 0.0);
for (auto v : mesh.vertices()) {
int iv = maps.v_idx[v];
if (iv >= 0) xbase[static_cast<std::size_t>(iv)] = b_base;
}
for (auto e : mesh.edges()) {
int ie = maps.e_idx[e];
if (ie >= 0) xbase[static_cast<std::size_t>(ie)] = a_base;
}
// G = Σβ theta_target; absorb G(xbase) into targets so G(xbase) = 0
auto G0 = evaluate_hyper_ideal(mesh, xbase, maps, false).gradient;
for (auto v : mesh.vertices()) {
int iv = maps.v_idx[v];
if (iv >= 0) maps.theta_v[v] += G0[static_cast<std::size_t>(iv)];
}
for (auto e : mesh.edges()) {
int ie = maps.e_idx[e];
if (ie >= 0) maps.theta_e[e] += G0[static_cast<std::size_t>(ie)];
}
// ── Step 4: perturb and solve ─────────────────────────────────────────
const double perturb = 0.25;
std::vector<double> x0 = xbase;
for (auto& v : x0) v += perturb;
double g_start = 0.0;
for (double v : G0) g_start = std::max(g_start, std::abs(v));
std::cout << "[example_hyper_ideal] Starting Newton from perturbation +" << perturb
<< " (G at xbase = " << g_start << ").\n";
auto result = newton_hyper_ideal(mesh, x0, maps, /*tol=*/1e-9, /*max_iter=*/200);
// ── Step 5: report ────────────────────────────────────────────────────
if (result.converged) {
std::cout << "[example_hyper_ideal] Converged in " << result.iterations
<< " iterations. ||G||_inf = " << result.grad_inf_norm << "\n";
} else {
std::cout << "[example_hyper_ideal] Did NOT converge after " << result.iterations
<< " iterations. ||G||_inf = " << result.grad_inf_norm << "\n";
}
std::cout << "[example_hyper_ideal] DOF values at equilibrium:\n";
for (auto v : mesh.vertices()) {
int iv = maps.v_idx[v];
if (iv < 0) continue;
std::cout << " v" << v
<< " b = " << result.x[static_cast<std::size_t>(iv)]
<< " (expected " << b_base << ")\n";
}
for (auto e : mesh.edges()) {
int ie = maps.e_idx[e];
if (ie < 0) continue;
std::cout << " e" << e
<< " a = " << result.x[static_cast<std::size_t>(ie)]
<< " (expected " << a_base << ")\n";
}
// ── Step 6: write output mesh ─────────────────────────────────────────
try {
save_mesh(output_path, mesh);
std::cout << "[example_hyper_ideal] Mesh saved to: " << output_path << "\n";
} catch (const std::exception& e) {
std::cerr << "Warning: could not write output: " << e.what() << "\n";
}
return result.converged ? 0 : 1;
}