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

@@ -17,7 +17,8 @@
// │ side lengths lij = exp(Λ̃ij/2): │
// │ │
// │ t12 = l12+l23+l31, t23 = l12l23+l31, t31 = l12+l23l31 │
// │ l123 = l12+l23+l31, denom2 = 2·sqrt(t12·t23·t31·l123) = 8·Area
// │ l123 = l12+l23+l31, denom2 = 8·Area (Area via Kahan's stable
// │ side-length formula — see euclidean_cot_weights)│
// │ │
// │ Cotangent at vertex k (opposite t_opp, adjacent t_a and t_b): │
// │ cot_k = (t_opp · l123 t_a · t_b) / denom2 │
@@ -48,6 +49,7 @@
#include <vector>
#include <array>
#include <cmath>
#include <algorithm>
#include <stdexcept>
namespace conformallab {
@@ -85,12 +87,26 @@ inline EuclCotWeights euclidean_cot_weights(double l12, double l23, double l31)
if (t12 <= 0.0 || t23 <= 0.0 || t31 <= 0.0)
return {0.0, 0.0, 0.0, false};
const double l123 = l12 + l23 + l31;
const double denom2_sq = t12 * t23 * t31 * l123;
if (denom2_sq <= 0.0) return {0.0, 0.0, 0.0, false};
const double l123 = l12 + l23 + l31;
// denom2 = 2·sqrt(t12·t23·t31·l123) = 8·Area
const double denom2 = 2.0 * std::sqrt(denom2_sq);
// Triangle area via Kahan's numerically stable formula. Sort the side
// lengths a ≥ b ≥ c and evaluate with the cancellation-avoiding grouping
// Area = ¼·√[ (a+(b+c))·(c(ab))·(c+(ab))·(a+(bc)) ].
// This replaces the naive 2·√(t12·t23·t31·l123): that product of
// near-equal-length differences loses precision for needle/cap triangles,
// where it would feed large relative error straight into the cotangent
// weights and the linear solve. The result denom2 = 8·Area is identical
// up to rounding for well-shaped triangles, but accurate for slivers.
double a = l12, b = l23, c = l31;
if (a < b) std::swap(a, b);
if (a < c) std::swap(a, c);
if (b < c) std::swap(b, c); // now a ≥ b ≥ c
const double kahan = (a + (b + c)) * (c - (a - b))
* (c + (a - b)) * (a + (b - c));
if (kahan <= 0.0) return {0.0, 0.0, 0.0, false};
const double denom2 = 8.0 * (0.25 * std::sqrt(kahan)); // = 8·Area
// Formula: cot_k = (t_opp · l123 t_a · t_b) / denom2
// cot1: t_opp=t23, t_a=t12, t_b=t31 (v1 opposite l23)