Files
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

156 lines
4.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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))
}
}