156 lines
4.8 KiB
Go
156 lines
4.8 KiB
Go
// 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))
|
||
}
|
||
}
|