test: add degenerate-triangle limiting-angle and edge-DOF guard tests

Finding-F and Finding-G from doc/reviewer/external-audit-2026-05-30.md
(java-port-audit missing-test items 1 and 2).

Finding-F — Degenerate triangle: limiting angles (item 1)
  test_euclidean_functional.cpp:
    DegenerateTriangle_LimitingAngles_L{12,23,31}TooLong
      — verifies α_opposite = π, other two = 0, valid = false
      for all three edge-over-long cases
    DegenerateTriangle_GradientPicksUpPiCorner
      — end-to-end mesh test: forces effective l12 >> l23+l31 via
        lambda0, evaluates gradient, asserts G_v3 = π (not 2π from
        a skipped degenerate face) and G_v1=G_v2 = 2π

  test_spherical_functional.cpp:
    DegenerateTriangle_LimitingAngles_S{12,23,31}TooLong
      — same coverage for spherical_angles()

Finding-G — euclidean_hessian edge-DOF guard (item 2)
  test_euclidean_hessian.cpp:
    EdgeDOFGuard_Throws
      — assign_euclidean_all_dof_indices + euclidean_hessian → throw
    EdgeDOFGuard_VertexOnlyDoesNotThrow
      — vertex-only layout → no throw (regression guard)

275/275 CGAL tests pass, 0 failed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-31 00:31:07 +02:00
parent 7534c62c3d
commit adbf682f0f
4 changed files with 182 additions and 3 deletions

View File

@@ -652,3 +652,52 @@ TEST(SphericalGoldenJava, FullMeshEdgeDofGradient_Tetrahedron)
EXPECT_NEAR(G[static_cast<std::size_t>(maps.e_idx[eAB])], -0.35189517043413690, 1e-12);
EXPECT_NEAR(G[static_cast<std::size_t>(maps.e_idx[eCD])], -0.44101986058895950, 1e-12);
}
// ════════════════════════════════════════════════════════════════════════════
// Degenerate spherical triangle — limiting angles (Finding-F, java-port-audit item 1)
//
// spherical_angles() must return the limiting angles (π opposite the
// over-long edge, 0/0 elsewhere) when the spherical triangle inequality
// is violated, with valid = false. This mirrors the Euclidean behaviour
// and is required for the convex C¹ BPS extension.
// ════════════════════════════════════════════════════════════════════════════
TEST(SphericalFunctional, DegenerateTriangle_LimitingAngles_S12TooLong)
{
// s12 > s23 + s31: s12 = 2.5, s23 = s31 = 0.5 (all < π so valid arc lengths)
// s23 < 0 → actually use s-based check
// Easier: use s12 = π ε (nearly degenerate hemisphere edge)
// and very short s23, s31 so s12 > s23 + s31.
const double s12 = 2.0, s23 = 0.4, s31 = 0.4; // s12 > s23+s31 = 0.8
auto fa = spherical_angles(s12, s23, s31);
EXPECT_FALSE(fa.valid);
// s12 is the edge opposite v3 → α3 = π
EXPECT_NEAR(fa.alpha3, PI, 1e-12)
<< "corner opposite over-long s12 must be π";
EXPECT_NEAR(fa.alpha1, 0.0, 1e-12);
EXPECT_NEAR(fa.alpha2, 0.0, 1e-12);
}
TEST(SphericalFunctional, DegenerateTriangle_LimitingAngles_S23TooLong)
{
// s23 > s12 + s31 → α1 = π
const double s12 = 0.4, s23 = 2.0, s31 = 0.4;
auto fa = spherical_angles(s12, s23, s31);
EXPECT_FALSE(fa.valid);
EXPECT_NEAR(fa.alpha1, PI, 1e-12)
<< "corner opposite over-long s23 must be π";
EXPECT_NEAR(fa.alpha2, 0.0, 1e-12);
EXPECT_NEAR(fa.alpha3, 0.0, 1e-12);
}
TEST(SphericalFunctional, DegenerateTriangle_LimitingAngles_S31TooLong)
{
// s31 > s12 + s23 → α2 = π
const double s12 = 0.4, s23 = 0.4, s31 = 2.0;
auto fa = spherical_angles(s12, s23, s31);
EXPECT_FALSE(fa.valid);
EXPECT_NEAR(fa.alpha2, PI, 1e-12)
<< "corner opposite over-long s31 must be π";
EXPECT_NEAR(fa.alpha1, 0.0, 1e-12);
EXPECT_NEAR(fa.alpha3, 0.0, 1e-12);
}