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

153 lines
4.5 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"
)
// 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))
}
}