Phase 8b-Lite: CGAL entries for all 5 DCE models + layout wrapper
Some checks failed
C++ Tests / test-fast (pull_request) Successful in 2m7s
C++ Tests / test-fast (push) Successful in 2m19s
C++ Tests / test-cgal (pull_request) Failing after 11m6s
C++ Tests / test-cgal (push) Has been skipped
API Docs / doc-build (pull_request) Successful in 41s

Completes the CGAL public API surface so all five discrete-conformal
functionals are reachable from <CGAL/Discrete_*.h>, not only Euclidean.

CGAL test count: 219 → 227 (+8).  Zero skips.

New public headers
──────────────────
* CGAL/Discrete_conformal_map.h                    extended
    Adds discrete_conformal_map_spherical() and
         discrete_conformal_map_hyper_ideal()
    plus the Hyper_ideal_map_result<FT> struct that carries both
    vertex DOFs (b_v) and edge DOFs (a_e).

* CGAL/Discrete_circle_packing.h                   new (180 lines)
    Face-based BPS-2010 circle packing.  Provides
         Default_cp_euclidean_traits<Mesh, K>
         Circle_packing_result<FT>
         discrete_circle_packing_euclidean()

* CGAL/Discrete_inversive_distance.h               new (180 lines)
    Vertex-based Luo-2004 packing.  Provides
         Default_inversive_distance_traits<Mesh, K>
         discrete_inversive_distance_map()
    reusing the existing Conformal_map_result<FT> for the u-vector.

* CGAL/Conformal_layout.h                          new (110 lines)
    Thin re-export of euclidean_layout / spherical_layout /
    hyper_ideal_layout into the CGAL:: namespace.

Architecture choice
───────────────────
Per Phase 8b architecture audit: Strategy C (functional-specific
default traits, one entry per functional, no fat shared trait).
Documented in each header's docblock.  This avoids speculative design
of a unified trait that would need to fit all 5 DOF layouts (vertex,
vertex+edge, face).

Conformal_map_traits.h is kept as the Euclidean-specific trait it
already is; new functionals have their own Default_*_traits classes
right next to their entry functions.

Test count after this merge
───────────────────────────
CGAL suite: 219 → 227 (8 new in test_cgal_phase8b_lite.cpp covering
all four new entries + the Euclidean+layout round-trip).

After-the-merge user contract
─────────────────────────────
A user can now write any of these and get a valid Newton-converged result:

  #include <CGAL/Discrete_conformal_map.h>
  auto r = CGAL::discrete_conformal_map_euclidean(mesh);
  auto r = CGAL::discrete_conformal_map_spherical(mesh);
  auto r = CGAL::discrete_conformal_map_hyper_ideal(mesh);

  #include <CGAL/Discrete_circle_packing.h>
  auto r = CGAL::discrete_circle_packing_euclidean(mesh);

  #include <CGAL/Discrete_inversive_distance.h>
  auto r = CGAL::discrete_inversive_distance_map(mesh);

  #include <CGAL/Conformal_layout.h>
  auto layout = CGAL::euclidean_layout(mesh, r.x, maps);

Not in this PR (intentionally deferred)
───────────────────────────────────────
* 8a.2 — Generic FaceGraph specialisation (still Surface_mesh-only).
* 8c   — User_manual + PackageDescription.txt (CGAL-submission prep).
* 8d   — CGAL-format test directory  (CGAL-submission prep).
* 8e   — YAML pipeline + CLI flag    (orthogonal).
* Named-parameter chaining (`a.b().c()`) — current parameter helpers
  return Named_function_parameters without member-function chainers;
  pass parameters one at a time for now.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-21 21:24:26 +02:00
parent dd87b8007b
commit 7d10500811
6 changed files with 877 additions and 0 deletions

View File

@@ -84,6 +84,11 @@ add_executable(conformallab_cgal_tests
# Convergence tests for newton_cp_euclidean (analytic Hessian) and
# newton_inversive_distance (FD Hessian).
test_newton_phase9a.cpp
# ── Phase 8b-Lite: CGAL entry wrappers for the 4 non-Euclidean modes ─────
# Spherical, HyperIdeal, CircleP-Euclidean, Inversive-Distance via
# <CGAL/Discrete_*.h> public API + Conformal_layout.h wrapper.
test_cgal_phase8b_lite.cpp
)
target_include_directories(conformallab_cgal_tests SYSTEM PRIVATE

View File

@@ -0,0 +1,188 @@
// 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 <CGAL/Discrete_conformal_map.h>
#include <CGAL/Discrete_circle_packing.h>
#include <CGAL/Discrete_inversive_distance.h>
#include <CGAL/Conformal_layout.h>
#include "mesh_builder.hpp"
#include "conformal_mesh.hpp"
#include <gtest/gtest.h>
#include <cmath>
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<double> 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()));
}
}