Standardize the low-level free-function API on <verb>_<geom>_<rest>, matching the already-consistent setup_<geom>_maps. Old names kept as [[deprecated]] inline aliases for one release; all internal call sites migrated. Renames: assign_vertex_dof_indices -> assign_spherical_vertex_dof_indices assign_all_spherical_dof_indices -> assign_spherical_all_dof_indices assign_all_dof_indices -> assign_hyper_ideal_all_dof_indices compute_lambda0_from_mesh -> compute_spherical_lambda0_from_mesh gradient_check -> gradient_check_hyper_ideal A4/A5 (public CGAL API) intentionally deferred pending the license/ provenance decision (see CGAL submission audit G0/G1). Verified: 277/277 CGAL tests pass, no deprecation warnings. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
148 lines
6.3 KiB
C++
148 lines
6.3 KiB
C++
// 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_hyper_ideal_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;
|
||
}
|