// 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 #include #include #include 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 x0(static_cast(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(iv)]; } } static std::vector 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(n); std::vector xbase(sz, 0.0); for (auto v : mesh.vertices()) { int iv = maps.v_idx[v]; if (iv >= 0) xbase[static_cast(iv)] = b_base; } for (auto e : mesh.edges()) { int ie = maps.e_idx[e]; if (ie >= 0) xbase[static_cast(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(iv)]; } for (auto e : mesh.edges()) { int ie = maps.e_idx[e]; if (ie < 0) continue; maps.theta_e[e] += G[static_cast(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 x0(static_cast(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(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 1–3 ───────────────────────────────────────────────────────── auto mesh = make_spherical_tetrahedron(); auto maps = setup_spherical_maps(mesh); compute_lambda0_from_mesh(mesh, maps); int n = assign_vertex_dof_indices(mesh, maps); // ── Step 4: solve ───────────────────────────────────────────────────── std::vector x0(static_cast(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 1–3 ───────────────────────────────────────────────────────── auto mesh = make_triangle(); auto maps = setup_hyper_ideal_maps(mesh); int n = assign_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 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(i)], xbase[static_cast(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 x0(static_cast(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 x0(static_cast(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_all_dof_indices(mesh, maps); auto xbase = set_natural_hyper_ideal_targets(mesh, maps, n); std::vector 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_lambda0_from_mesh(mesh, maps); int n = assign_vertex_dof_indices(mesh, maps); std::vector x0(static_cast(n), -0.1); auto res = newton_spherical(mesh, x0, maps); EXPECT_TRUE(res.converged) << "Spherical: tetrahedron should converge"; } }