fix(stats): hold the Student-t tails past t-squared overflow

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-27 17:03:36 +02:00
parent 2f9358c512
commit 275de79123
6 changed files with 113 additions and 10 deletions
+53
View File
@@ -276,3 +276,56 @@ func TestGammaLowerLargeShape(t *testing.T) {
}
}
}
// TestStudentTCDFSquareOverflowTail pins the Student laws past the square
// overflow: a t beyond √MaxFloat64 drives z = df/(df+t²) through Inf/Inf
// to the NaN and 0 the closed form answers, while the heavy df = 1 and
// df = 2 tails are still representable there. The lower tail and the
// two-sided tail must answer their asymptotic forms, exactly as the
// one-sided upper tail already does.
func TestStudentTCDFSquareOverflowTail(t *testing.T) {
const huge = 1e155
// df = 1 (Cauchy): the one-sided tail is 1/(π·t), two-sided 2/(π·t).
one, err := StudentTCDF(-huge, 1)
if err != nil {
t.Fatalf("StudentTCDF(-1e155, 1): %v", err)
}
if want := 1 / (math.Pi * huge); math.Abs(one-want) > 1e-12*want {
t.Fatalf("StudentTCDF(-1e155, 1) = %.17g, want the Cauchy tail %.17g", one, want)
}
two, err := twoSidedT(huge, 1)
if err != nil {
t.Fatalf("twoSidedT(1e155, 1): %v", err)
}
if want := 2 / (math.Pi * huge); math.Abs(two-want) > 1e-12*want {
t.Fatalf("twoSidedT(1e155, 1) = %.17g, want the Cauchy tail %.17g", two, want)
}
// df = 2: the two-sided tail is 1 − t/√(t²+2), the one-sided half of
// it, both ≈ 1/t² here and still inside the subnormal range. The
// reference is assembled as u/(√(1+u)+1) with u = 2/t², the form
// that never squares t.
const u = 2e-310 // 2/t² at t = 1e155
two2, err := twoSidedT(huge, 2)
if err != nil {
t.Fatalf("twoSidedT(1e155, 2): %v", err)
}
if want := u / (math.Sqrt(1+u) + 1); math.Abs(two2-want) > 1e-6*want {
t.Fatalf("twoSidedT(1e155, 2) = %.17g, want the df 2 tail %.17g", two2, want)
}
one2, err := StudentTCDF(-huge, 2)
if err != nil {
t.Fatalf("StudentTCDF(-1e155, 2): %v", err)
}
if want := 0.5 * u / (math.Sqrt(1+u) + 1); math.Abs(one2-want) > 1e-6*want {
t.Fatalf("StudentTCDF(-1e155, 2) = %.17g, want the df 2 tail %.17g", one2, want)
}
// The upper half of the axis keeps answering 1, and a df whose tail
// genuinely underflows keeps answering 0: both are the honest
// roundings there.
if v, err := StudentTCDF(huge, 1); err != nil || v != 1 {
t.Fatalf("StudentTCDF(1e155, 1) = %v (%v), want 1", v, err)
}
if v, err := StudentTCDF(-huge, 4); err != nil || v != 0 {
t.Fatalf("StudentTCDF(-1e155, 4) = %v (%v), want the underflowed 0", v, err)
}
}