Files
tensor/internal/core/expint_test.go
T

156 lines
4.8 KiB
Go
Raw Normal View History

2026-09-03 10:00:00 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package core
import (
"math"
"testing"
)
// TestExpIntegralE1 checks E1 against tabulated values on both the
// series branch (x ≤ 2) and the continued-fraction branch (x > 2).
func TestExpIntegralE1(t *testing.T) {
x := mustFloats(t, []float64{0.1, 0.5, 1, 2, 5, 10})
got, err := ExpIntegralE1(x)
if err != nil {
t.Fatalf("ExpIntegralE1: %v", err)
}
want := []float64{
1.8229239584193906,
0.5597735947761609,
0.21938393439552027,
0.04890051070806112,
0.0011482955912753257,
4.156968929685324e-06,
}
for i := range want {
if math.Abs(got.FloatAt(i)-want[i]) > 1e-13*(1+math.Abs(want[i])) {
t.Fatalf("E1(%v) = %.16g, want %.16g", x.FloatAt(i), got.FloatAt(i), want[i])
}
}
neg, nerr := ExpIntegralE1(mustFloats(t, []float64{-1}))
if nerr != nil {
t.Fatalf("ExpIntegralE1(−1): %v", nerr)
}
if !math.IsNaN(neg.FloatAt(0)) {
t.Fatalf("E1(−1) = %v, want NaN outside the domain", neg.FloatAt(0))
}
}
// TestExpIntegralEi checks Ei on both branches, including the
// reflection Ei(−x) = −E1(x) for x < 0.
func TestExpIntegralEi(t *testing.T) {
x := mustFloats(t, []float64{-1, -0.5, 0.5, 1, 5})
got, err := ExpIntegralEi(x)
if err != nil {
t.Fatalf("ExpIntegralEi: %v", err)
}
want := []float64{
-0.21938393439552027,
-0.5597735947761609,
0.4542199048631725,
1.8951178163559368,
40.18527535580318,
}
for i := range want {
if math.Abs(got.FloatAt(i)-want[i]) > 1e-13*(1+math.Abs(want[i])) {
t.Fatalf("Ei(%v) = %.16g, want %.16g", x.FloatAt(i), got.FloatAt(i), want[i])
}
}
}
// TestDigamma checks the polygamma family against exact values:
// ψ(1) = −γ, ψ(2) = 1 − γ, ψ(½) = −γ − 2 ln 2, and the same for ψ′.
func TestDigamma(t *testing.T) {
x := mustFloats(t, []float64{0.5, 1, 2, 5})
got, err := Digamma(x)
if err != nil {
t.Fatalf("Digamma: %v", err)
}
want := []float64{
-eulerGamma - 2*math.Log(2),
-eulerGamma,
1 - eulerGamma,
1.5061176684318005,
}
for i := range want {
if math.Abs(got.FloatAt(i)-want[i]) > 1e-11*(1+math.Abs(want[i])) {
t.Fatalf("psi(%v) = %.16g, want %.16g", x.FloatAt(i), got.FloatAt(i), want[i])
}
}
// Negative non-integer via the reflection formula: ψ(−½) =
// ψ(1.5) + π·cot(π/2) = ψ(1.5) = 0.03648997397857652.
neg := mustFloats(t, []float64{-0.5})
got, err = Digamma(neg)
if err != nil {
t.Fatalf("Digamma(−½): %v", err)
}
if want := 0.03648997397857652; math.Abs(got.FloatAt(0)-want) > 1e-11 {
t.Fatalf("psi(−½) = %.16g, want %.16g", got.FloatAt(0), want)
}
// Poles at non-positive integers return NaN, the same IEEE
// convention the gamma family applies.
pole, perr := Digamma(mustFloats(t, []float64{-2}))
if perr != nil {
t.Fatalf("Digamma(−2): %v", perr)
}
if !math.IsNaN(pole.FloatAt(0)) {
t.Fatalf("psi(−2) = %v, want NaN at the pole", pole.FloatAt(0))
}
}
// TestTrigamma checks ψ′ against exact values: ψ′(1) = π²/6,
// ψ′(½) = π²/2, ψ′(2) = 1 − π²/6.
func TestTrigamma(t *testing.T) {
x := mustFloats(t, []float64{0.5, 1, 2, 5})
got, err := Trigamma(x)
if err != nil {
t.Fatalf("Trigamma: %v", err)
}
want := []float64{
math.Pi * math.Pi / 2,
math.Pi * math.Pi / 6,
math.Pi*math.Pi/6 - 1,
0.22132295573718011, // π²/6 − (1 + ¼ + ¹⁄₉ + ¹⁄₁₆)
}
for i := range want {
if math.Abs(got.FloatAt(i)-want[i]) > 1e-11*(1+math.Abs(want[i])) {
t.Fatalf("psi'(%v) = %.16g, want %.16g", x.FloatAt(i), got.FloatAt(i), want[i])
}
}
}
// TestFresnel checks C and S against high-precision series values,
// the odd symmetry and the ½ limits at large argument.
func TestFresnel(t *testing.T) {
x := mustFloats(t, []float64{0.5, 1, 2, 4, 6})
c, err := FresnelC(x)
if err != nil {
t.Fatalf("FresnelC: %v", err)
}
s, err := FresnelS(x)
if err != nil {
t.Fatalf("FresnelS: %v", err)
}
// References at 60-digit precision, all pinned at 1e-12: the plain
// power series covers the first three (x = 4 sits at its boundary)
// and x = 6 sums the same series in extended precision.
wantC := []float64{0.4923442258714464, 0.7798934003768228, 0.4882534060753408, 0.4984260330381776, 0.4995314678555011}
wantS := []float64{0.06473243286000028, 0.4382591473903548, 0.3434156783636982, 0.4205157542469284, 0.4469607612369303}
for i := range wantC {
if math.Abs(c.FloatAt(i)-wantC[i]) > 1e-12 {
t.Fatalf("C(%v) = %.16g, want %.16g", x.FloatAt(i), c.FloatAt(i), wantC[i])
}
if math.Abs(s.FloatAt(i)-wantS[i]) > 1e-12 {
t.Fatalf("S(%v) = %.16g, want %.16g", x.FloatAt(i), s.FloatAt(i), wantS[i])
}
}
// Odd symmetry.
nx := mustFloats(t, []float64{-1})
nc, _ := FresnelC(nx)
if math.Abs(nc.FloatAt(0)+0.7798934003768228) > 1e-12 {
t.Fatalf("C(−1) = %v, want −C(1)", nc.FloatAt(0))
}
}