fix(num): N5 stable triangle area (Kahan) in euclidean_cot_weights

The cotangent weights divided by 2·√(t12·t23·t31·l123).  For a needle/cap
(sliver) triangle one of the t-values is the difference of near-equal edge
lengths → catastrophic cancellation, and the area under the sqrt loses
precision, feeding large relative error into every cotangent weight, the
Hessian, and the linear solve (numerical-stability audit N5).

Replace the area computation with Kahan's stable side-length formula
(sort a≥b≥c, evaluate ¼·√[(a+(b+c))(c−(a−b))(c+(a−b))(a+(b−c))]).  The
denominator is still exactly 8·Area for well-shaped triangles but accurate
for slivers.  The triangle-inequality guard and the cotangent numerators are
unchanged.

Test: CotWeights_SliverMatchesHighPrecisionReference cross-checks a thin
triangle (apex ≈ 0.01 rad) against a long-double law-of-cosines reference.
292/292 CGAL tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tarik Moussa
2026-05-31 16:07:35 +02:00
parent 2dc4ddcc32
commit a1a7f216e0
2 changed files with 63 additions and 6 deletions

View File

@@ -61,6 +61,47 @@ TEST(EuclideanHessian, CotWeights_RightIsoscelesTriangle)
EXPECT_NEAR(cw.cot3, 1.0, 1e-12); // 45° at v3
}
// ════════════════════════════════════════════════════════════════════════════
// N5: cotangent weights on a SLIVER triangle match a high-precision reference.
//
// The area is now computed by Kahan's stable side-length formula instead of
// the naive 2·√(t12·t23·t31·l123). On a thin (sliver) triangle the naive
// product of near-equal-length differences loses precision, biasing every
// cotangent weight. Here we cross-check against an independent law-of-cosines
// reference in long double (80-bit on x86 CI — a genuine high-precision oracle;
// equal to double on ARM64, where it still serves as a regression cross-check).
// ════════════════════════════════════════════════════════════════════════════
TEST(EuclideanHessian, CotWeights_SliverMatchesHighPrecisionReference)
{
// Thin isosceles: short base l12, two near-unit legs (apex angle ≈ 0.01 rad).
const double l12 = 0.01, l23 = 1.0, l31 = 1.0;
auto cw = euclidean_cot_weights(l12, l23, l31);
ASSERT_TRUE(cw.valid);
// cot at the vertex opposite side `opp`, with adjacent sides s1, s2:
// cos = (s1² + s2² opp²) / (2·s1·s2), sin = √((1cos)(1+cos)).
auto cot_ref = [](long double opp, long double s1, long double s2) {
long double cosA = (s1 * s1 + s2 * s2 - opp * opp) / (2.0L * s1 * s2);
long double sinA = std::sqrt((1.0L - cosA) * (1.0L + cosA));
return static_cast<double>(cosA / sinA);
};
const double c1 = cot_ref(l23, l12, l31); // v1 opposite l23
const double c2 = cot_ref(l31, l12, l23); // v2 opposite l31
const double c3 = cot_ref(l12, l23, l31); // v3 opposite l12 (needle angle)
auto rel = [](double a, double b) {
return std::abs(a - b) / std::max(1.0, std::abs(b));
};
EXPECT_LT(rel(cw.cot1, c1), 1e-9);
EXPECT_LT(rel(cw.cot2, c2), 1e-9);
EXPECT_LT(rel(cw.cot3, c3), 1e-9);
EXPECT_TRUE(std::isfinite(cw.cot1) &&
std::isfinite(cw.cot2) &&
std::isfinite(cw.cot3));
}
// ════════════════════════════════════════════════════════════════════════════
// Hessian is symmetric: H[i,j] == H[j,i]
// ════════════════════════════════════════════════════════════════════════════