// test_cgal_traits_mvp.cpp // // Phase 8 MVP — first tests for the new CGAL-style public API. // // Validates: // 1. Default_conformal_map_traits compiles and // provides all advertised types and property-map accessors. // 2. discrete_conformal_map_euclidean() runs end-to-end on a small mesh. // 3. Named-parameter overrides (gradient_tolerance, max_iterations) // change the Newton behaviour as expected. // 4. The result agrees with the legacy newton_euclidean() at the // same DOF assignment — proving the wrapper is non-destructive. // // These tests are the Phase 8 MVP acceptance probe. Phase 9a // (Inversive-Distance) will become the next, deeper validation by // implementing a new functional against this same trait API. #include #include #include "mesh_builder.hpp" // make_triangle, make_quad_strip, make_tetrahedron #include "euclidean_functional.hpp" #include "newton_solver.hpp" #include #include using namespace conformallab; // ════════════════════════════════════════════════════════════════════════════ // 1. Traits class: compile-time type sanity // ════════════════════════════════════════════════════════════════════════════ TEST(CGALConformalTraits, DefaultTraitsTypes) { using K = CGAL::Simple_cartesian; using Mesh = CGAL::Surface_mesh; using Tr = CGAL::Default_conformal_map_traits; // FT comes from the kernel. static_assert(std::is_same_v); // Descriptors come from boost::graph_traits, not from Surface_mesh directly. static_assert(std::is_same_v); static_assert(std::is_same_v< typename Tr::Vertex_descriptor, typename boost::graph_traits::vertex_descriptor>); // Property-map types should match Surface_mesh::Property_map for the // appropriate key. static_assert(std::is_same_v< typename Tr::Theta_pmap, typename Mesh::template Property_map>); static_assert(std::is_same_v< typename Tr::Vertex_index_pmap, typename Mesh::template Property_map>); static_assert(std::is_same_v< typename Tr::Lambda0_pmap, typename Mesh::template Property_map>); // Default kernel: Simple_cartesian. using TrDefault = CGAL::Default_conformal_map_traits; static_assert(std::is_same_v>); } // ════════════════════════════════════════════════════════════════════════════ // 2. Traits property-map accessors are non-destructive // // Setting up via the trait helpers and via setup_euclidean_maps() must yield // the same property map (Surface_mesh deduplicates by name). // ════════════════════════════════════════════════════════════════════════════ TEST(CGALConformalTraits, AccessorsReuseExistingMaps) { using K = CGAL::Simple_cartesian; using Mesh = CGAL::Surface_mesh; using Tr = CGAL::Default_conformal_map_traits; auto mesh = make_triangle(); auto maps = setup_euclidean_maps(mesh); auto theta_via_traits = Tr::theta_map(mesh); auto idx_via_traits = Tr::vertex_index_map(mesh); auto lambda0_via_traits = Tr::lambda0_map(mesh); // Surface_mesh property maps with the same key type are equality-comparable // by name lookup — accessing through the traits class must return the // same map that setup_euclidean_maps() created. EXPECT_EQ(theta_via_traits, maps.theta_v); EXPECT_EQ(idx_via_traits, maps.v_idx); EXPECT_EQ(lambda0_via_traits, maps.lambda0); } // ════════════════════════════════════════════════════════════════════════════ // 3. End-to-end: discrete_conformal_map_euclidean() on a small open mesh // ════════════════════════════════════════════════════════════════════════════ TEST(CGALDiscreteConformalMap, SingleTriangleConverges) { auto mesh = make_triangle(); auto result = CGAL::discrete_conformal_map_euclidean(mesh); EXPECT_TRUE(result.converged) << "Newton did not converge on a single triangle"; EXPECT_LT(result.gradient_norm, 1e-8); EXPECT_GE(result.iterations, 0); EXPECT_EQ(result.u_per_vertex.size(), num_vertices(mesh)); // With the default flat-disc target curvature and the first vertex pinned, // the natural-theta equilibrium is at u = 0 — Newton should accept x0=0. for (double u : result.u_per_vertex) EXPECT_NEAR(u, 0.0, 1e-8); } TEST(CGALDiscreteConformalMap, QuadStripConverges) { auto mesh = make_quad_strip(); auto result = CGAL::discrete_conformal_map_euclidean(mesh); EXPECT_TRUE(result.converged); EXPECT_LT(result.gradient_norm, 1e-8); EXPECT_EQ(result.u_per_vertex.size(), num_vertices(mesh)); } // ════════════════════════════════════════════════════════════════════════════ // 4. Named-parameter overrides take effect // ════════════════════════════════════════════════════════════════════════════ TEST(CGALDiscreteConformalMap, MaxIterationsTakesEffect) { auto mesh = make_triangle(); // max_iterations(0) forces Newton to give up immediately. auto result = CGAL::discrete_conformal_map_euclidean( mesh, CGAL::parameters::max_iterations(0)); EXPECT_EQ(result.iterations, 0); // Trivial natural-theta case: gradient is already zero at x=0, // so even 0 iterations may report "converged" depending on the // initial gradient check. The point is just that the parameter // was *read* — verified by EXPECT_EQ on iterations above. } TEST(CGALDiscreteConformalMap, GradientToleranceTakesEffect) { auto mesh = make_quad_strip(); // Loose tolerance — must still converge, but possibly in fewer steps. auto result_loose = CGAL::discrete_conformal_map_euclidean( mesh, CGAL::parameters::gradient_tolerance(1e-4)); EXPECT_TRUE(result_loose.converged); // Strict tolerance — also must converge, gradient norm must be tighter. auto result_strict = CGAL::discrete_conformal_map_euclidean( mesh, CGAL::parameters::gradient_tolerance(1e-12)); EXPECT_TRUE(result_strict.converged); EXPECT_LT(result_strict.gradient_norm, 1e-10); } // ════════════════════════════════════════════════════════════════════════════ // 5. Wrapper agrees with the legacy newton_euclidean() at the same setup // // This is the cross-API consistency check: same mesh, same default settings // (first vertex pinned, x0=0) — the u-vector returned by the wrapper must // match what newton_euclidean produces directly. // ════════════════════════════════════════════════════════════════════════════ TEST(CGALDiscreteConformalMap, WrapperMatchesLegacyAPI) { auto mesh = make_quad_strip(); // ── New API: applies natural-theta automatically ─────────────────────── auto result_new = CGAL::discrete_conformal_map_euclidean(mesh); // ── Legacy API on a fresh mesh — must replicate the *same* preparation // that the wrapper performs internally (pin first vertex, assign // DOFs, apply natural-theta). Otherwise the comparison is unfair // (Newton would diverge without natural-theta on these meshes). ──── auto mesh_legacy = make_quad_strip(); auto maps = setup_euclidean_maps(mesh_legacy); compute_euclidean_lambda0_from_mesh(mesh_legacy, maps); // Pin first vertex (gauge), assign sequential DOFs to the rest. auto vit = mesh_legacy.vertices().begin(); maps.v_idx[*vit++] = -1; int idx = 0; for (; vit != mesh_legacy.vertices().end(); ++vit) maps.v_idx[*vit] = idx++; // Natural-theta: shift Θ so that x = 0 is the natural equilibrium. std::vector x0(idx, 0.0); auto G0 = euclidean_gradient(mesh_legacy, x0, maps); for (auto v : mesh_legacy.vertices()) { int j = maps.v_idx[v]; if (j >= 0) maps.theta_v[v] -= G0[static_cast(j)]; } auto nr = newton_euclidean(mesh_legacy, x0, maps, 1e-10, 200); // ── Compare ──────────────────────────────────────────────────────────── EXPECT_EQ(result_new.converged, nr.converged); EXPECT_NEAR(result_new.gradient_norm, nr.grad_inf_norm, 1e-12); // Pinned vertex u is 0 in both; for the rest the values agree. for (auto v : mesh_legacy.vertices()) { int j = maps.v_idx[v]; double u_legacy = (j >= 0) ? nr.x[static_cast(j)] : 0.0; EXPECT_NEAR(result_new.u_per_vertex[v.idx()], u_legacy, 1e-10) << "Wrapper diverges from legacy for vertex " << v.idx(); } }