Files
ConformalLabpp/code/tests/cgal/test_pipeline.cpp
Tarik Moussa 65fc8ac816
All checks were successful
C++ Tests / test-fast (pull_request) Successful in 2m20s
C++ Tests / quality-gates (pull_request) Has been skipped
C++ Tests / test-cgal (pull_request) Has been skipped
refactor(api): consistent naming for spherical + hyper-ideal helpers (A1–A3)
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>
2026-05-31 10:37:06 +02:00

296 lines
15 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.

// Copyright (c) 2024-2026 Tarik Moussa.
// SPDX-License-Identifier: MIT
// test_pipeline.cpp
//
// Phase 4c — End-to-end pipeline tests and library-user examples.
//
// These tests exercise the full conformallab++ pipeline as a user would:
//
// 1. Build (or load) a mesh
// 2. Set up maps and assign DOFs
// 3. Configure target angles / targets
// 4. Solve with Newton
// 5. Inspect / export the result
//
// Each test mirrors a realistic usage scenario documented in the README.
//
// Tests:
// 1. Pipeline_Euclidean_TriangleToEquilibrium
// Read mesh → setup Euclidean maps → solve → verify convergence
// 2. Pipeline_Spherical_TetrahedronToEquilibrium
// Setup spherical tetrahedron → solve → verify angles sum to 4π
// 3. Pipeline_HyperIdeal_TriangleRoundTrip
// Build triangle → setup HyperIdeal → solve → verify G ≈ 0
// 4. Pipeline_MeshIO_SolveAndExport
// Build mesh → solve → write OFF → reload → verify vertex count intact
// 5. Pipeline_AllThreeGeometries_SameTopology
// Same quad-strip mesh solved under all three geometries: all converge
#include "conformal_mesh.hpp"
#include "mesh_builder.hpp"
#include "mesh_io.hpp"
#include "euclidean_functional.hpp"
#include "spherical_functional.hpp"
#include "hyper_ideal_functional.hpp"
#include "newton_solver.hpp"
#include <gtest/gtest.h>
#include <cmath>
#include <vector>
#include <filesystem>
using namespace conformallab;
// ────────────────────────────────────────────────────────────────────────────
// Shared helpers
// ────────────────────────────────────────────────────────────────────────────
static void pin_first_vertex_euclidean(ConformalMesh& mesh, EuclideanMaps& maps, int& n)
{
auto vit = mesh.vertices().begin();
Vertex_index v0 = *vit++;
maps.v_idx[v0] = -1;
int idx = 0;
for (; vit != mesh.vertices().end(); ++vit)
maps.v_idx[*vit] = idx++;
n = idx;
}
static void set_natural_euclidean_theta(ConformalMesh& mesh, EuclideanMaps& maps, int n)
{
std::vector<double> x0(static_cast<std::size_t>(n), 0.0);
auto G = euclidean_gradient(mesh, x0, maps);
for (auto v : mesh.vertices()) {
int iv = maps.v_idx[v];
if (iv < 0) continue;
maps.theta_v[v] -= G[static_cast<std::size_t>(iv)];
}
}
static std::vector<double> set_natural_hyper_ideal_targets(
ConformalMesh& mesh, HyperIdealMaps& maps, int n,
double b_base = 1.0, double a_base = 0.5)
{
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;
}
auto G = evaluate_hyper_ideal(mesh, xbase, maps, false).gradient;
for (auto v : mesh.vertices()) {
int iv = maps.v_idx[v];
if (iv < 0) continue;
maps.theta_v[v] += G[static_cast<std::size_t>(iv)];
}
for (auto e : mesh.edges()) {
int ie = maps.e_idx[e];
if (ie < 0) continue;
maps.theta_e[e] += G[static_cast<std::size_t>(ie)];
}
return xbase;
}
// ════════════════════════════════════════════════════════════════════════════
// Test 1 — Euclidean full pipeline: triangle → equilibrium
//
// Simulates a user doing:
// auto mesh = make_triangle();
// auto maps = setup_euclidean_maps(mesh);
// compute_euclidean_lambda0_from_mesh(mesh, maps);
// // … set target angles and DOF indices …
// auto result = newton_euclidean(mesh, x0, maps);
// assert(result.converged);
// ════════════════════════════════════════════════════════════════════════════
TEST(Pipeline, Euclidean_TriangleToEquilibrium)
{
// ── Step 1: build mesh ────────────────────────────────────────────────
auto mesh = make_triangle();
// ── Step 2: set up maps ───────────────────────────────────────────────
auto maps = setup_euclidean_maps(mesh);
compute_euclidean_lambda0_from_mesh(mesh, maps);
// ── Step 3: assign DOFs (pin v0) ──────────────────────────────────────
int n = 0;
pin_first_vertex_euclidean(mesh, maps, n);
ASSERT_EQ(n, 2);
// ── Step 4: choose natural target angles → x* = 0 ────────────────────
set_natural_euclidean_theta(mesh, maps, n);
// ── Step 5: solve ─────────────────────────────────────────────────────
std::vector<double> x0(static_cast<std::size_t>(n), -0.1);
auto result = newton_euclidean(mesh, x0, maps);
// ── Step 6: verify ────────────────────────────────────────────────────
EXPECT_TRUE(result.converged)
<< "Euclidean pipeline: triangle should converge; "
"grad_inf_norm = " << result.grad_inf_norm;
EXPECT_LT(result.grad_inf_norm, 1e-8);
EXPECT_EQ(static_cast<int>(result.x.size()), n);
}
// ════════════════════════════════════════════════════════════════════════════
// Test 2 — Spherical full pipeline: tetrahedron → equilibrium
//
// The spherical tetrahedron equilibrium x* = 0 is built into the maps.
// After solving, the total angle defect Σ(Θ_v Σα_v) should be ≈ 0.
// ════════════════════════════════════════════════════════════════════════════
TEST(Pipeline, Spherical_TetrahedronToEquilibrium)
{
// ── Steps 13 ─────────────────────────────────────────────────────────
auto mesh = make_spherical_tetrahedron();
auto maps = setup_spherical_maps(mesh);
compute_spherical_lambda0_from_mesh(mesh, maps);
int n = assign_spherical_vertex_dof_indices(mesh, maps);
// ── Step 4: solve ─────────────────────────────────────────────────────
std::vector<double> x0(static_cast<std::size_t>(n), -0.2);
auto result = newton_spherical(mesh, x0, maps);
// ── Step 5: verify convergence ────────────────────────────────────────
EXPECT_TRUE(result.converged)
<< "Spherical pipeline: tetrahedron should converge; "
"grad_inf_norm = " << result.grad_inf_norm;
EXPECT_LT(result.grad_inf_norm, 1e-8);
// ── Step 6: verify geometric invariant — total angle defect ≈ 0 ──────
auto G_final = spherical_gradient(mesh, result.x, maps);
double total_defect = 0.0;
for (double gv : G_final) total_defect += gv;
EXPECT_NEAR(total_defect, 0.0, 1e-7)
<< "Spherical: total angle defect should vanish at equilibrium";
}
// ════════════════════════════════════════════════════════════════════════════
// Test 3 — HyperIdeal full pipeline: triangle → equilibrium
// ════════════════════════════════════════════════════════════════════════════
TEST(Pipeline, HyperIdeal_TriangleRoundTrip)
{
// ── Steps 13 ─────────────────────────────────────────────────────────
auto mesh = make_triangle();
auto maps = setup_hyper_ideal_maps(mesh);
int n = assign_hyper_ideal_all_dof_indices(mesh, maps);
// ── Step 4: natural targets (equilibrium at b=1.0, a=0.5) ────────────
auto xbase = set_natural_hyper_ideal_targets(mesh, maps, n);
// Verify: gradient at xbase must be ≈ 0 before solving
auto G_at_base = evaluate_hyper_ideal(mesh, xbase, maps, false).gradient;
double max_g = 0.0;
for (double v : G_at_base) max_g = std::max(max_g, std::abs(v));
ASSERT_LT(max_g, 1e-10) << "Natural target setup: gradient at base should be ~0";
// ── Step 5: perturb and solve ─────────────────────────────────────────
std::vector<double> x0 = xbase;
for (auto& v : x0) v += 0.3;
auto result = newton_hyper_ideal(mesh, x0, maps);
// ── Step 6: verify ────────────────────────────────────────────────────
EXPECT_TRUE(result.converged)
<< "HyperIdeal pipeline: triangle should converge; "
"grad_inf_norm = " << result.grad_inf_norm;
EXPECT_LT(result.grad_inf_norm, 1e-8);
// Solution should be close to xbase (same equilibrium)
for (int i = 0; i < n; ++i) {
EXPECT_NEAR(result.x[static_cast<std::size_t>(i)],
xbase[static_cast<std::size_t>(i)], 1e-6)
<< "DOF " << i << " should recover the equilibrium value";
}
}
// ════════════════════════════════════════════════════════════════════════════
// Test 4 — Mesh I/O in the pipeline: solve → write → reload → check
//
// Demonstrates: compute a conformal factor on a mesh, write it to OFF, reload
// and check that the mesh topology is preserved.
// ════════════════════════════════════════════════════════════════════════════
TEST(Pipeline, MeshIO_SolveAndExport)
{
// ── Build and solve ───────────────────────────────────────────────────
auto mesh = make_quad_strip();
auto maps = setup_euclidean_maps(mesh);
compute_euclidean_lambda0_from_mesh(mesh, maps);
int n = 0;
pin_first_vertex_euclidean(mesh, maps, n);
set_natural_euclidean_theta(mesh, maps, n);
std::vector<double> x0(static_cast<std::size_t>(n), -0.1);
auto result = newton_euclidean(mesh, x0, maps);
ASSERT_TRUE(result.converged) << "Solver must converge before export test";
// ── Write mesh ────────────────────────────────────────────────────────
const std::string tmp_path = "/tmp/conformallab_pipeline_test.off";
ASSERT_NO_THROW(save_mesh(tmp_path, mesh));
ASSERT_TRUE(std::filesystem::exists(tmp_path));
// ── Reload and verify topology ────────────────────────────────────────
ConformalMesh mesh2;
ASSERT_NO_THROW(mesh2 = load_mesh(tmp_path));
EXPECT_EQ(mesh2.number_of_vertices(), mesh.number_of_vertices())
<< "Vertex count must survive OFF round-trip";
EXPECT_EQ(mesh2.number_of_faces(), mesh.number_of_faces())
<< "Face count must survive OFF round-trip";
std::filesystem::remove(tmp_path);
}
// ════════════════════════════════════════════════════════════════════════════
// Test 5 — All three geometries, same quad-strip topology
//
// Validates that the solver infrastructure works uniformly: the same mesh
// topology is solvable under Euclidean, Spherical, and HyperIdeal geometries.
// ════════════════════════════════════════════════════════════════════════════
TEST(Pipeline, AllThreeGeometries_QuadStrip)
{
// ── Euclidean ──────────────────────────────────────────────────────────
{
auto mesh = make_quad_strip();
auto maps = setup_euclidean_maps(mesh);
compute_euclidean_lambda0_from_mesh(mesh, maps);
int n = 0;
pin_first_vertex_euclidean(mesh, maps, n);
set_natural_euclidean_theta(mesh, maps, n);
std::vector<double> x0(static_cast<std::size_t>(n), -0.1);
auto res = newton_euclidean(mesh, x0, maps);
EXPECT_TRUE(res.converged) << "Euclidean: quad strip should converge";
}
// ── HyperIdeal ────────────────────────────────────────────────────────
{
auto mesh = make_quad_strip();
auto maps = setup_hyper_ideal_maps(mesh);
int n = assign_hyper_ideal_all_dof_indices(mesh, maps);
auto xbase = set_natural_hyper_ideal_targets(mesh, maps, n);
std::vector<double> x0 = xbase;
for (auto& v : x0) v += 0.1;
auto res = newton_hyper_ideal(mesh, x0, maps);
EXPECT_TRUE(res.converged) << "HyperIdeal: quad strip should converge";
}
// ── Spherical (tetrahedron: smallest closed mesh with all vertices free) ─
{
auto mesh = make_spherical_tetrahedron();
auto maps = setup_spherical_maps(mesh);
compute_spherical_lambda0_from_mesh(mesh, maps);
int n = assign_spherical_vertex_dof_indices(mesh, maps);
std::vector<double> x0(static_cast<std::size_t>(n), -0.1);
auto res = newton_spherical(mesh, x0, maps);
EXPECT_TRUE(res.converged) << "Spherical: tetrahedron should converge";
}
}