From 957f50667981d54bfe09fcf696d8d1ede3c5a258 Mon Sep 17 00:00:00 2001 From: Tarik Moussa Date: Sun, 31 May 2026 18:54:50 +0200 Subject: [PATCH] feat(cgal): propagate Newton status + conditioning diagnostics into CGAL results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to S2: surface the new NewtonResult diagnostics through the public CGAL result types so callers of the high-level API see them too. - Add `CGAL::Newton_status` (alias of conformallab::NewtonStatus) and three fields — `status`, `sparse_qr_fallback_used`, `min_ldlt_pivot` — to Conformal_map_result, Hyper_ideal_map_result and Circle_packing_result. (sparse_qr_fallback_used already existed on Conformal_map_result but was never populated; it is now wired through.) - Map them from NewtonResult at all five entry points (euclidean / spherical / hyper_ideal / inversive_distance / cp_euclidean). - Test: SingleTriangleConverges now asserts status == Converged and the diagnostics propagate. Additive only — existing `converged`/`iterations`/`gradient_norm` semantics unchanged. 301/301 CGAL tests pass. Co-Authored-By: Claude Opus 4.8 --- code/include/CGAL/Discrete_circle_packing.h | 14 +++++++++ code/include/CGAL/Discrete_conformal_map.h | 30 +++++++++++++++++++ .../CGAL/Discrete_inversive_distance.h | 3 ++ code/tests/cgal/test_cgal_traits_mvp.cpp | 5 ++++ 4 files changed, 52 insertions(+) diff --git a/code/include/CGAL/Discrete_circle_packing.h b/code/include/CGAL/Discrete_circle_packing.h index e774262..3882eee 100644 --- a/code/include/CGAL/Discrete_circle_packing.h +++ b/code/include/CGAL/Discrete_circle_packing.h @@ -96,6 +96,9 @@ struct Default_cp_euclidean_traits, K> Result of `discrete_circle_packing_euclidean`. Carries face DOFs `ρ_f = log R_f` rather than the vertex DOFs of the classical modes. */ +/// Why the Newton solve terminated (re-exported from the solver; I1 audit). +using Newton_status = ::conformallab::NewtonStatus; + template struct Circle_packing_result { @@ -108,6 +111,14 @@ struct Circle_packing_result FT gradient_norm = FT(0); /// `true` iff `gradient_norm < gradient_tolerance` at exit. bool converged = false; + + /// Why the solve stopped: `Converged` / `MaxIterations` / + /// `LinearSolverFailed` / `LineSearchStalled`. + Newton_status status = Newton_status::MaxIterations; + /// `true` iff any Newton step fell back to SparseQR (gauge singularity). + bool sparse_qr_fallback_used = false; + /// Smallest `|Dᵢᵢ|` of the last LDLT (near-singularity proxy; 0 if none). + FT min_ldlt_pivot = FT(0); }; // ── Entry function ──────────────────────────────────────────────────────────── @@ -188,6 +199,9 @@ auto discrete_circle_packing_euclidean( result.iterations = nr.iterations; result.gradient_norm = nr.grad_inf_norm; result.converged = nr.converged; + result.status = nr.status; + result.sparse_qr_fallback_used = nr.sparse_qr_fallback_used; + result.min_ldlt_pivot = static_cast(nr.min_ldlt_pivot); // ── output_uv_map (Phase 8b-Lite extension) ──────────────────────────── // diff --git a/code/include/CGAL/Discrete_conformal_map.h b/code/include/CGAL/Discrete_conformal_map.h index 98ad104..044189f 100644 --- a/code/include/CGAL/Discrete_conformal_map.h +++ b/code/include/CGAL/Discrete_conformal_map.h @@ -78,6 +78,10 @@ namespace CGAL { // Result type // ════════════════════════════════════════════════════════════════════════════ +/// Why the Newton solve terminated (re-exported from the solver; I1 audit). +/// `Converged` / `MaxIterations` / `LinearSolverFailed` / `LineSearchStalled`. +using Newton_status = ::conformallab::NewtonStatus; + /*! \ingroup PkgConformalMapRef @@ -100,9 +104,18 @@ struct Conformal_map_result /// `true` iff `gradient_norm < gradient_tolerance`. bool converged = false; + /// Why the solve stopped (more informative than `converged` alone): one of + /// `Converged` / `MaxIterations` / `LinearSolverFailed` / `LineSearchStalled`. + Newton_status status = Newton_status::MaxIterations; + /// `true` iff the linear solver used the SparseQR fallback at any /// Newton step (gauge mode on closed mesh without pinned vertex). bool sparse_qr_fallback_used = false; + + /// Smallest `|Dᵢᵢ|` of the last LDLT factorisation — a cheap + /// near-singularity proxy (small ⇒ ill-conditioned solve). `0` if LDLT + /// never succeeded. + FT min_ldlt_pivot = FT(0); }; @@ -272,6 +285,9 @@ auto discrete_conformal_map_euclidean( result.iterations = nr.iterations; result.gradient_norm = nr.grad_inf_norm; result.converged = nr.converged; + result.status = nr.status; + result.sparse_qr_fallback_used = nr.sparse_qr_fallback_used; + result.min_ldlt_pivot = static_cast(nr.min_ldlt_pivot); // ── 8. Optional layout step (Phase 8b-Lite extension) ────────────────── // @@ -410,6 +426,9 @@ auto discrete_conformal_map_spherical( result.iterations = nr.iterations; result.gradient_norm = nr.grad_inf_norm; result.converged = nr.converged; + result.status = nr.status; + result.sparse_qr_fallback_used = nr.sparse_qr_fallback_used; + result.min_ldlt_pivot = static_cast(nr.min_ldlt_pivot); // Optional 3-D layout step (point on S²) auto uv_param = parameters::get_parameter( @@ -459,6 +478,14 @@ struct Hyper_ideal_map_result FT gradient_norm = FT(0); /// `true` iff `gradient_norm < gradient_tolerance` at exit. bool converged = false; + + /// Why the solve stopped: `Converged` / `MaxIterations` / + /// `LinearSolverFailed` / `LineSearchStalled`. + Newton_status status = Newton_status::MaxIterations; + /// `true` iff any Newton step fell back to SparseQR (gauge singularity). + bool sparse_qr_fallback_used = false; + /// Smallest `|Dᵢᵢ|` of the last LDLT (near-singularity proxy; 0 if none). + FT min_ldlt_pivot = FT(0); }; /*! @@ -538,6 +565,9 @@ auto discrete_conformal_map_hyper_ideal( result.iterations = nr.iterations; result.gradient_norm = nr.grad_inf_norm; result.converged = nr.converged; + result.status = nr.status; + result.sparse_qr_fallback_used = nr.sparse_qr_fallback_used; + result.min_ldlt_pivot = static_cast(nr.min_ldlt_pivot); // Optional Poincaré-disk layout (2-D in the unit disk). auto uv_param = parameters::get_parameter( diff --git a/code/include/CGAL/Discrete_inversive_distance.h b/code/include/CGAL/Discrete_inversive_distance.h index 887ec28..dcaebf1 100644 --- a/code/include/CGAL/Discrete_inversive_distance.h +++ b/code/include/CGAL/Discrete_inversive_distance.h @@ -207,6 +207,9 @@ auto discrete_inversive_distance_map( result.iterations = nr.iterations; result.gradient_norm = nr.grad_inf_norm; result.converged = nr.converged; + result.status = nr.status; + result.sparse_qr_fallback_used = nr.sparse_qr_fallback_used; + result.min_ldlt_pivot = static_cast(nr.min_ldlt_pivot); // ── Optional layout step (Phase 8b-Lite extension) ───────────────────── // diff --git a/code/tests/cgal/test_cgal_traits_mvp.cpp b/code/tests/cgal/test_cgal_traits_mvp.cpp index 5767502..36e1ac7 100644 --- a/code/tests/cgal/test_cgal_traits_mvp.cpp +++ b/code/tests/cgal/test_cgal_traits_mvp.cpp @@ -111,6 +111,11 @@ TEST(CGALDiscreteConformalMap, SingleTriangleConverges) EXPECT_GE(result.iterations, 0); EXPECT_EQ(result.u_per_vertex.size(), num_vertices(mesh)); + // I1/N7 propagated through the CGAL layer: status enum + diagnostics. + EXPECT_EQ(result.status, CGAL::Newton_status::Converged); + EXPECT_FALSE(result.sparse_qr_fallback_used); // pinned vertex → no gauge mode + EXPECT_GE(result.min_ldlt_pivot, 0.0); + // 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)