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 4021796..9e81359 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)