// Copyright (c) 2026 Petr Balvín (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)) } }