// test_cgal_phase8b_lite.cpp // // Phase 8b-Lite — Smoke tests for the four new CGAL-style entry functions // added on top of the Phase 8a MVP (`discrete_conformal_map_euclidean`). // // All entries are thin wrappers around the legacy Newton solvers; the // purpose of these tests is to verify: // • the wrapper compiles + dispatches correctly // • named parameters pass through (gradient_tolerance, max_iterations) // • the returned Result struct contains the expected DOF vector // • Newton convergence happens end-to-end via the public API #include #include #include #include #include "mesh_builder.hpp" #include "conformal_mesh.hpp" #include #include using namespace conformallab; namespace { // Mesh helper — closed regular tetrahedron, used for spherical / hyper-ideal / // circle-packing tests. inline ConformalMesh make_closed_tet() { return make_tetrahedron(); } // Open 3-face tetrahedron-minus-face, for layout testing. inline ConformalMesh make_open_3face() { ConformalMesh mesh; auto v0 = mesh.add_vertex(Point3( 1, 1, 1)); auto v1 = mesh.add_vertex(Point3( 1, -1, -1)); auto v2 = mesh.add_vertex(Point3(-1, 1, -1)); auto v3 = mesh.add_vertex(Point3(-1, -1, 1)); mesh.add_face(v0, v2, v1); mesh.add_face(v0, v1, v3); mesh.add_face(v0, v3, v2); return mesh; } } // anonymous // ════════════════════════════════════════════════════════════════════════════ // 1. Spherical entry — closed genus-0 tetrahedron, natural-theta default // ════════════════════════════════════════════════════════════════════════════ TEST(CGALPhase8bLite, Spherical_ClosedTetrahedron_NaturalThetaConverges) { auto mesh = make_closed_tet(); auto res = CGAL::discrete_conformal_map_spherical(mesh); EXPECT_TRUE(res.converged); EXPECT_LT(res.gradient_norm, 1e-8); EXPECT_EQ(res.u_per_vertex.size(), num_vertices(mesh)); // Natural-theta ⇒ u = 0 is the equilibrium ⇒ all values ≈ 0. for (double u : res.u_per_vertex) EXPECT_NEAR(u, 0.0, 1e-8); } TEST(CGALPhase8bLite, Spherical_NamedParametersTakeEffect) { auto mesh = make_closed_tet(); auto res = CGAL::discrete_conformal_map_spherical( mesh, CGAL::parameters::max_iterations(0)); EXPECT_EQ(res.iterations, 0); } // ════════════════════════════════════════════════════════════════════════════ // 2. Hyper-ideal entry — wrapper compiles + runs, returns both b_v and a_e // ════════════════════════════════════════════════════════════════════════════ TEST(CGALPhase8bLite, HyperIdeal_Tetrahedron_ReturnsBothVertexAndEdgeDOFs) { auto mesh = make_closed_tet(); auto res = CGAL::discrete_conformal_map_hyper_ideal( mesh, CGAL::parameters::max_iterations(20)); // Newton on default targets (Θ=2π, θ=π) from the "natural" b=1, a=0.5 // start may or may not converge in 20 iterations — but the wrapper must // populate the result struct in any case. EXPECT_EQ(res.b_per_vertex.size(), num_vertices(mesh)); EXPECT_EQ(res.a_per_edge.size(), num_edges (mesh)); EXPECT_GE(res.iterations, 0); EXPECT_TRUE(std::isfinite(res.gradient_norm)); } // ════════════════════════════════════════════════════════════════════════════ // 3. Circle-packing (face-based) entry — natural-phi convergence // ════════════════════════════════════════════════════════════════════════════ TEST(CGALPhase8bLite, CirclePacking_ClosedTetrahedron_NaturalPhiConverges) { auto mesh = make_closed_tet(); auto res = CGAL::discrete_circle_packing_euclidean(mesh); EXPECT_TRUE(res.converged); EXPECT_LT(res.gradient_norm, 1e-8); EXPECT_EQ(res.rho_per_face.size(), num_faces(mesh)); // Pinned face is at index 0 (first iterated face); its ρ is 0 by gauge. // After natural-phi the equilibrium is ρ_f = 0 for every face. for (double r : res.rho_per_face) EXPECT_NEAR(r, 0.0, 1e-8); } TEST(CGALPhase8bLite, CirclePacking_GradientToleranceTakesEffect) { auto mesh = make_closed_tet(); auto res_loose = CGAL::discrete_circle_packing_euclidean( mesh, CGAL::parameters::gradient_tolerance(1e-4)); EXPECT_TRUE(res_loose.converged); auto mesh2 = make_closed_tet(); auto res_strict = CGAL::discrete_circle_packing_euclidean( mesh2, CGAL::parameters::gradient_tolerance(1e-12)); EXPECT_TRUE(res_strict.converged); EXPECT_LT(res_strict.gradient_norm, 1e-10); } // ════════════════════════════════════════════════════════════════════════════ // 4. Inversive-distance (vertex-based) entry — natural-theta convergence // ════════════════════════════════════════════════════════════════════════════ TEST(CGALPhase8bLite, InversiveDistance_Triangle_NaturalThetaConverges) { auto mesh = make_triangle(); auto res = CGAL::discrete_inversive_distance_map(mesh); EXPECT_TRUE(res.converged); EXPECT_LT(res.gradient_norm, 1e-8); EXPECT_EQ(res.u_per_vertex.size(), num_vertices(mesh)); for (double u : res.u_per_vertex) EXPECT_NEAR(u, 0.0, 1e-8); } TEST(CGALPhase8bLite, InversiveDistance_QuadStrip_NamedParametersWork) { auto mesh = make_quad_strip(); // Named-parameter chaining (`a.b().c()`) is not currently supported on // the package-local tags; pass one parameter per call instead. auto res = CGAL::discrete_inversive_distance_map( mesh, CGAL::parameters::max_iterations(50)); EXPECT_TRUE(res.converged); EXPECT_LE(res.iterations, 50); } // ════════════════════════════════════════════════════════════════════════════ // 5. Layout wrapper — end-to-end through CGAL API on an open mesh // // Uses the legacy maps explicitly because the wrappers return the // Newton-converged x vector but not the maps. This exercises that the // `CGAL::euclidean_layout` shim works as expected. // ════════════════════════════════════════════════════════════════════════════ TEST(CGALPhase8bLite, Layout_EuclideanWrapper_RoundTrip) { auto mesh = make_open_3face(); // Set up the maps + run Newton via the CGAL Euclidean entry. auto res = CGAL::discrete_conformal_map_euclidean(mesh); ASSERT_TRUE(res.converged); // The wrapper does its own DOF assignment internally; we re-fetch // the (now-populated) EuclideanMaps from the mesh's property maps // to feed the layout wrapper. auto maps = setup_euclidean_maps(mesh); compute_euclidean_lambda0_from_mesh(mesh, maps); // Pin first vertex (mirrors the wrapper's gauge choice). 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 x(idx, 0.0); // wrapper's natural-theta equilibrium auto layout = CGAL::euclidean_layout(mesh, x, maps); EXPECT_EQ(layout.uv.size(), num_vertices(mesh)); // All UVs finite — basic sanity that the layout ran. for (auto& uv : layout.uv) { EXPECT_TRUE(std::isfinite(uv.x())); EXPECT_TRUE(std::isfinite(uv.y())); } }