feat(cgal): propagate Newton status + conditioning diagnostics into CGAL results
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 <noreply@anthropic.com>
This commit is contained in:
@@ -96,6 +96,9 @@ struct Default_cp_euclidean_traits<CGAL::Surface_mesh<typename K::Point_3>, 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 <typename FT = double>
|
||||
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<FT>(nr.min_ldlt_pivot);
|
||||
|
||||
// ── output_uv_map (Phase 8b-Lite extension) ────────────────────────────
|
||||
//
|
||||
|
||||
@@ -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<FT>(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<FT>(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<FT>(nr.min_ldlt_pivot);
|
||||
|
||||
// Optional Poincaré-disk layout (2-D in the unit disk).
|
||||
auto uv_param = parameters::get_parameter(
|
||||
|
||||
@@ -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<FT>(nr.min_ldlt_pivot);
|
||||
|
||||
// ── Optional layout step (Phase 8b-Lite extension) ─────────────────────
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user