Files
tensor/internal/core/besselmod_test.go
T

153 lines
4.5 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"
)
// TestModifiedBesselTabulated pins I and K against tabulated values
// at x = 1, the standard reference point.
func TestModifiedBesselTabulated(t *testing.T) {
x := mustFloats(t, []float64{1})
i0, _ := BesselI0(x)
i1, _ := BesselI1(x)
k0, _ := BesselK0(x)
k1, _ := BesselK1(x)
want := []struct {
name string
got float64
ref float64
}{
{"I0(1)", i0.FloatAt(0), 1.2660658777520084},
{"I1(1)", i1.FloatAt(0), 0.5651591039924850},
{"K0(1)", k0.FloatAt(0), 0.4210244382407083},
{"K1(1)", k1.FloatAt(0), 0.6019072301972346},
}
for _, w := range want {
if math.Abs(w.got-w.ref) > 5e-7*(1+math.Abs(w.ref)) {
t.Fatalf("%s = %.16g, want %.16g", w.name, w.got, w.ref)
}
}
}
// TestModifiedBesselSymmetry checks the parity and small-argument
// behaviour: I₀ even with I₀(0) = 1, I₁ odd with I₁(0) = 0, and the
// K divergence to +Inf at the origin.
func TestModifiedBesselSymmetry(t *testing.T) {
x := mustFloats(t, []float64{-1, 1})
i0, _ := BesselI0(x)
if math.Abs(i0.FloatAt(0)-i0.FloatAt(1)) > 1e-14 {
t.Fatalf("I₀ must be even: %v vs %v", i0.FloatAt(0), i0.FloatAt(1))
}
i1, _ := BesselI1(x)
if math.Abs(i1.FloatAt(0)+i1.FloatAt(1)) > 1e-14 {
t.Fatalf("I₁ must be odd: %v vs %v", i1.FloatAt(0), i1.FloatAt(1))
}
i1At0, err := BesselI1(mustFloats(t, []float64{0}))
if err != nil {
t.Fatalf("BesselI1(0): %v", err)
}
if i1At0.FloatAt(0) != 0 {
t.Fatalf("I₁(0) = %v, want 0", i1At0.FloatAt(0))
}
k0, err := BesselK0(mustFloats(t, []float64{0}))
if err != nil {
t.Fatalf("BesselK0(0): %v", err)
}
if !math.IsInf(k0.FloatAt(0), 1) {
t.Fatalf("K₀(0) = %v, want +Inf", k0.FloatAt(0))
}
}
// TestModifiedBesselWronskian checks the Wronskian identity
// I₀·K₁ + I₁·K₀ = 1/x, which couples the two kinds at every point.
func TestModifiedBesselWronskian(t *testing.T) {
x := mustFloats(t, []float64{0.1, 0.5, 1, 5, 20})
i0, _ := BesselI0(x)
i1, _ := BesselI1(x)
k0, _ := BesselK0(x)
k1, _ := BesselK1(x)
for i := range x.Len() {
xv := x.FloatAt(i)
w := i0.FloatAt(i)*k1.FloatAt(i) + i1.FloatAt(i)*k0.FloatAt(i)
if math.Abs(w-1/xv) > 1e-6*(1/xv) {
t.Fatalf("x=%v: wronskian = %.12g, want %.12g", xv, w, 1/xv)
}
}
}
// TestBesselRecurrences checks the three-term recurrences linking
// consecutive integer orders, for both kinds.
func TestBesselRecurrences(t *testing.T) {
x := mustFloats(t, []float64{1, 4, 9})
const n = 3
in, err := BesselIn(n, x)
if err != nil {
t.Fatalf("BesselIn: %v", err)
}
inm1, err := BesselIn(n-1, x)
if err != nil {
t.Fatalf("BesselIn(n−1): %v", err)
}
inm2, err := BesselIn(n-2, x)
if err != nil {
t.Fatalf("BesselIn(n−2): %v", err)
}
kn, err := BesselKn(n, x)
if err != nil {
t.Fatalf("BesselKn: %v", err)
}
knm1, err := BesselKn(n-1, x)
if err != nil {
t.Fatalf("BesselKn(n−1): %v", err)
}
knm2, err := BesselKn(n-2, x)
if err != nil {
t.Fatalf("BesselKn(n−2): %v", err)
}
for i := range x.Len() {
xv := x.FloatAt(i)
// I_{n−1} − I_{n+1} = 2n/x·I_n with n − 1 in the call.
got := inm2.FloatAt(i) - in.FloatAt(i)
want := 2 * float64(n-1) / xv * inm1.FloatAt(i)
if math.Abs(got-want) > 1e-6*(1+math.Abs(want)) {
t.Fatalf("I recurrence at %v: %v, want %v", xv, got, want)
}
// K_{n+1} = K_{n−1} + 2n/x·K_n with n − 1 in the call.
got = knm2.FloatAt(i) + 2*float64(n-1)/xv*knm1.FloatAt(i)
if math.Abs(got-kn.FloatAt(i)) > 1e-6*(1+math.Abs(kn.FloatAt(i))) {
t.Fatalf("K recurrence at %v: %v, want %v", xv, got, kn.FloatAt(i))
}
}
}
// TestModifiedBesselRejects pins the input contracts: negative orders
// are refused for both integer-order entry points, and K diverges to
// +Inf at the origin on the diagonal of its domain.
func TestModifiedBesselRejects(t *testing.T) {
x := mustFloats(t, []float64{1})
if _, err := BesselIn(-1, x); err == nil {
t.Fatal("expected an error for a negative I order")
}
if _, err := BesselKn(-1, x); err == nil {
t.Fatal("expected an error for a negative K order")
}
k0, err := BesselK0(mustFloats(t, []float64{0}))
if err != nil {
t.Fatalf("BesselK0(0): %v", err)
}
if !math.IsInf(k0.FloatAt(0), 1) {
t.Fatalf("K₀(0) = %v, want +Inf", k0.FloatAt(0))
}
k1, err := BesselK1(mustFloats(t, []float64{0}))
if err != nil {
t.Fatalf("BesselK1(0): %v", err)
}
if !math.IsInf(k1.FloatAt(0), 1) {
t.Fatalf("K₁(0) = %v, want +Inf", k1.FloatAt(0))
}
}