fix(gauss-bonnet): delete HyperIdeal overloads — wrong identity for hyperbolic metrics
Finding-B from doc/reviewer/external-audit-2026-05-30.md. The Euclidean Gauss–Bonnet identity Σ(2π−Θ_v) = 2π·χ is ONLY valid for flat/Euclidean and spherical metrics. For hyperbolic metrics (HyperIdeal) the correct identity is Σ(2π−Θ_v) − Area(M) = 2π·χ. The previously provided gauss_bonnet_sum(HyperIdealMaps) overload would silently pass the wrong LHS to check_gauss_bonnet, which would always throw "deficit = ±Area" for valid hyperbolic targets. Fix: - gauss_bonnet_sum(mesh, HyperIdealMaps) → = delete + explanation comment - enforce_gauss_bonnet(mesh, HyperIdealMaps&) → = delete + explanation comment - Header box comment rewritten with the correct hyperbolic Gauss–Bonnet identity and a clear "HyperIdeal: NOT SUPPORTED" section New test in test_phase6.cpp: - HyperIdeal_EuclideanSumDiscrepancy_DocumentsWhyCheckIsDeleted verifies numerically that the Euclidean sum = 0 but 2π·χ = 4π for a regular tetrahedron, documenting the −4π discrepancy that motivated the deletion - Three compile-time static_asserts (SFINAE) confirm the overload is not invocable with HyperIdealMaps but remains so with Euclidean/SphericalMaps 263/263 CGAL tests pass, 0 failed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -137,6 +137,78 @@ TEST(GaussBonnet, ManuallySetAnalyticalTheta_PassesCheck)
|
||||
EXPECT_NO_THROW(check_gauss_bonnet(m, maps));
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// GaussBonnet — HyperIdeal API guard (Finding-B from external-audit-2026-05-30)
|
||||
//
|
||||
// gauss_bonnet_sum(mesh, HyperIdealMaps) and
|
||||
// enforce_gauss_bonnet(mesh, HyperIdealMaps) are intentionally DELETED.
|
||||
//
|
||||
// Reason: the correct hyperbolic Gauss–Bonnet identity is
|
||||
// Σ_v (2π − Θ_v) − Area(M) = 2π · χ(M)
|
||||
// not the Euclidean form Σ(2π−Θ_v) = 2π·χ. For a regular (Θ_v=2π) genus-2
|
||||
// surface: Σ(2π−2π)=0 but 2π·χ=−4π, so the Euclidean check would always
|
||||
// throw "deficit = 4π" for a perfectly valid HyperIdeal target.
|
||||
//
|
||||
// Compile-time enforcement: gauss_bonnet_sum / enforce_gauss_bonnet with
|
||||
// HyperIdealMaps are = delete, so any accidental call is a compile error.
|
||||
// The static_asserts below confirm this is wired correctly.
|
||||
//
|
||||
// The runtime test shows the discrepancy numerically: even for a regular
|
||||
// tetrahedron where all Θ_v=2π (a valid all-hyper-ideal starting point),
|
||||
// the "Euclidean sum" is 0 but the correct RHS for χ=2 is 4π — the
|
||||
// Euclidean check would be off by 4π.
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// Invocability check via SFINAE: tries to form the call expression in an
|
||||
// unevaluated context; a deleted function causes substitution failure.
|
||||
namespace {
|
||||
template <typename Maps>
|
||||
auto try_gb_sum(int) -> decltype(gauss_bonnet_sum(
|
||||
std::declval<const ConformalMesh&>(),
|
||||
std::declval<const Maps&>()), std::true_type{});
|
||||
template <typename>
|
||||
std::false_type try_gb_sum(...);
|
||||
} // namespace
|
||||
|
||||
static_assert(!decltype(try_gb_sum<HyperIdealMaps>(0))::value,
|
||||
"gauss_bonnet_sum must NOT be invocable with HyperIdealMaps");
|
||||
static_assert( decltype(try_gb_sum<EuclideanMaps>(0))::value,
|
||||
"gauss_bonnet_sum must still be invocable with EuclideanMaps");
|
||||
static_assert( decltype(try_gb_sum<SphericalMaps>(0))::value,
|
||||
"gauss_bonnet_sum must still be invocable with SphericalMaps");
|
||||
|
||||
TEST(GaussBonnet, HyperIdeal_EuclideanSumDiscrepancy_DocumentsWhyCheckIsDeleted)
|
||||
{
|
||||
// On a tetrahedron (χ=2) with all Θ_v = 2π:
|
||||
// Euclidean sum Σ(2π−2π) = 0
|
||||
// but 2π·χ = 4π
|
||||
// deficit (Euclidean formula) = 0 − 4π = −4π ← WRONG check for HyperIdeal
|
||||
//
|
||||
// The HyperIdeal identity is Σ(2π−Θ_v) − Area = 2π·χ.
|
||||
// Area > 0 for any non-degenerate hyperbolic metric, so the real deficit
|
||||
// would be much smaller. This test documents the mismatch numerically
|
||||
// so any future re-introduction of the HyperIdeal overload is caught.
|
||||
auto m = make_tetrahedron();
|
||||
auto hi_maps = setup_hyper_ideal_maps(m);
|
||||
// All theta_v default to 2π (regular vertex target).
|
||||
|
||||
// Access the raw property map directly (not via the deleted bundle overload)
|
||||
// to compute the Euclidean-style sum — just for documentation purposes.
|
||||
double euclid_sum = gauss_bonnet_sum(m, hi_maps.theta_v); // raw map: OK
|
||||
double rhs = gauss_bonnet_rhs(m); // 2π·χ = 4π
|
||||
|
||||
EXPECT_NEAR(euclid_sum, 0.0, 1e-12) // Σ(2π−2π) = 0
|
||||
<< "Expected Euclidean sum = 0 for all-regular HyperIdeal targets";
|
||||
EXPECT_NEAR(rhs, 4.0 * M_PI, 1e-12) // 2π·χ(tetrahedron) = 4π
|
||||
<< "Expected RHS = 4π for tetrahedron (χ=2)";
|
||||
|
||||
// The Euclidean deficit would be 0 − 4π = −4π: completely wrong for HyperIdeal.
|
||||
// If check_gauss_bonnet were called with HyperIdealMaps it would ALWAYS throw
|
||||
// here, even though Θ_v=2π is a valid regular-vertex HyperIdeal target.
|
||||
EXPECT_NEAR(euclid_sum - rhs, -4.0 * M_PI, 1e-10)
|
||||
<< "Euclidean deficit for HyperIdeal target = −4π: confirms the deleted API is correct";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// CutGraph — tree-cotree algorithm
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user