145 lines
4.6 KiB
Go
145 lines
4.6 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
|
|
// SPDX-License-Identifier: MIT
|
|||
|
|
|
|||
|
|
package core
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"math"
|
|||
|
|
"strings"
|
|||
|
|
"testing"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// TestBesselJTabulated pins J against independently computed values
|
|||
|
|
// (mpmath, 30 significant digits) at points covering the power-series
|
|||
|
|
// branch, the downward Miller branch and both parity laws.
|
|||
|
|
func TestBesselJTabulated(t *testing.T) {
|
|||
|
|
cases := []struct {
|
|||
|
|
name string
|
|||
|
|
n int
|
|||
|
|
x float64
|
|||
|
|
want float64
|
|||
|
|
}{
|
|||
|
|
{"J_0(0)", 0, 0, 1},
|
|||
|
|
{"J_1(0)", 1, 0, 0},
|
|||
|
|
{"J_0(1)", 0, 1, 0.765197686557966551},
|
|||
|
|
{"J_1(1)", 1, 1, 0.440050585744933516},
|
|||
|
|
{"J_2(1)", 2, 1, 0.11490348493190048},
|
|||
|
|
{"J_5(1)", 5, 1, 0.000249757730211234431},
|
|||
|
|
{"J_7(2)", 7, 2, 0.000174944074868274169},
|
|||
|
|
{"J_0(5)", 0, 5, -0.177596771314338304},
|
|||
|
|
{"J_3(4)", 3, 4, 0.43017147387562194},
|
|||
|
|
{"J_2(10)", 2, 10, 0.254630313685120623},
|
|||
|
|
{"J_0(15.5)", 0, 15.5, -0.109230650900050168},
|
|||
|
|
{"J_2(15.5)", 2, 15.5, 0.130806545138985284},
|
|||
|
|
{"J_0(25)", 0, 25, 0.0962667832759581162},
|
|||
|
|
{"J_5(25)", 5, 25, -0.0660079953984229934},
|
|||
|
|
{"J_1(30)", 1, 30, -0.118751062616622937},
|
|||
|
|
{"J_1(-1)", 1, -1, -0.440050585744933516},
|
|||
|
|
{"J_2(-1)", 2, -1, 0.11490348493190048},
|
|||
|
|
{"J_-1(2)", -1, 2, -0.576724807756873387},
|
|||
|
|
{"J_-3(-2)", -3, -2, 0.128943249474402051},
|
|||
|
|
}
|
|||
|
|
for _, c := range cases {
|
|||
|
|
if got := BesselJ(c.n, c.x); math.Abs(got-c.want) > 1e-9*math.Abs(c.want) {
|
|||
|
|
t.Errorf("%s = %.16g, want %.16g", c.name, got, c.want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBesselYTabulated pins Y against independently computed values
|
|||
|
|
// (mpmath, 30 significant digits): the series seeds at small x, the
|
|||
|
|
// asymptotic seeds above the crossover and the upward recurrence
|
|||
|
|
// between them, plus the negative-order parity law.
|
|||
|
|
func TestBesselYTabulated(t *testing.T) {
|
|||
|
|
cases := []struct {
|
|||
|
|
name string
|
|||
|
|
n int
|
|||
|
|
x float64
|
|||
|
|
want float64
|
|||
|
|
}{
|
|||
|
|
{"Y_0(0.05)", 0, 0.05, -1.97931100081720967},
|
|||
|
|
{"Y_3(0.5)", 3, 0.5, -42.0594943047238827},
|
|||
|
|
{"Y_0(1)", 0, 1, 0.088256964215676958},
|
|||
|
|
{"Y_1(1)", 1, 1, -0.781212821300288717},
|
|||
|
|
{"Y_2(1)", 2, 1, -1.65068260681625439},
|
|||
|
|
{"Y_-2(1)", -2, 1, -1.65068260681625439},
|
|||
|
|
{"Y_3(3)", 3, 3, -0.538541616105031618},
|
|||
|
|
{"Y_5(2)", 5, 2, -9.93598912848197498},
|
|||
|
|
{"Y_7(2)", 7, 2, -271.54802536799367},
|
|||
|
|
{"Y_10(0.7)", 10, 0.7, -4244719426.07038669},
|
|||
|
|
{"Y_0(10)", 0, 10, 0.0556711672835993914},
|
|||
|
|
{"Y_0(15.5)", 0, 15.5, 0.170644911229434617},
|
|||
|
|
{"Y_2(15.5)", 2, 15.5, -0.155833796066422704},
|
|||
|
|
{"Y_5(15.5)", 5, 15.5, 0.204463657248615881},
|
|||
|
|
{"Y_0(20)", 0, 20, 0.0626405968093838312},
|
|||
|
|
{"Y_4(25)", 4, 25, -0.0910410709900928362},
|
|||
|
|
{"Y_1(30)", 1, 30, 0.0844255706617472349},
|
|||
|
|
{"Y_-1(3)", -1, 3, -0.324674424791799978},
|
|||
|
|
}
|
|||
|
|
for _, c := range cases {
|
|||
|
|
got, err := BesselY(c.n, c.x)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Errorf("%s: %v", c.name, err)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if math.Abs(got-c.want) > 1e-9*math.Abs(c.want) {
|
|||
|
|
t.Errorf("%s = %.16g, want %.16g", c.name, got, c.want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBesselJRecurrence checks the three-term recurrence
|
|||
|
|
// J_{ν−1} + J_{ν+1} = 2ν/x·J_ν (ν = 4) across both evaluation
|
|||
|
|
// branches, which any branch inconsistency would break.
|
|||
|
|
func TestBesselJRecurrence(t *testing.T) {
|
|||
|
|
for _, x := range []float64{1.5, 8, 15.5, 30} {
|
|||
|
|
nu := 4
|
|||
|
|
jm1 := BesselJ(nu-1, x)
|
|||
|
|
j0 := BesselJ(nu, x)
|
|||
|
|
jp1 := BesselJ(nu+1, x)
|
|||
|
|
got := jm1 + jp1
|
|||
|
|
want := 2 * float64(nu) / x * j0
|
|||
|
|
if math.Abs(got-want) > 1e-9*math.Abs(want) {
|
|||
|
|
t.Errorf("x=%v: J_%d + J_%d = %.16g, want %.16g", x, nu-1, nu+1, got, want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBesselYRecurrence checks the same recurrence for the second
|
|||
|
|
// kind, tying the series seeds to the recurrence-climbed orders at
|
|||
|
|
// both small and large arguments.
|
|||
|
|
func TestBesselYRecurrence(t *testing.T) {
|
|||
|
|
for _, x := range []float64{0.7, 2, 15.5} {
|
|||
|
|
nu := 4
|
|||
|
|
at := func(k int) float64 {
|
|||
|
|
v, err := BesselY(k, x)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("BesselY(%d, %v): %v", k, x, err)
|
|||
|
|
}
|
|||
|
|
return v
|
|||
|
|
}
|
|||
|
|
got := at(nu-1) + at(nu+1)
|
|||
|
|
want := 2 * float64(nu) / x * at(nu)
|
|||
|
|
if math.Abs(got-want) > 1e-9*math.Abs(want) {
|
|||
|
|
t.Errorf("x=%v: Y_%d + Y_%d = %.16g, want %.16g", x, nu-1, nu+1, got, want)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TestBesselYRejects pins the domain contract: Yₙ is defined for
|
|||
|
|
// x > 0 only and reports the violation as an error with the package
|
|||
|
|
// prefix, never as a NaN.
|
|||
|
|
func TestBesselYRejects(t *testing.T) {
|
|||
|
|
for _, x := range []float64{0, -1, -1e-300, math.NaN()} {
|
|||
|
|
v, err := BesselY(2, x)
|
|||
|
|
if err == nil {
|
|||
|
|
t.Errorf("BesselY(2, %g): expected an error, got %v", x, v)
|
|||
|
|
} else if !strings.Contains(err.Error(), "tensor: BesselY") {
|
|||
|
|
t.Errorf("BesselY(2, %g): error %q lacks the prefixed name", x, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if _, err := BesselY(2, 1); err != nil {
|
|||
|
|
t.Errorf("BesselY(2, 1): %v", err)
|
|||
|
|
}
|
|||
|
|
}
|