Skip to content

Commit acc0893

Browse files
authored
Merge pull request #38 from JuliaMath/sphericalbessel
Add spherical bessel functions
2 parents 6dfcf3d + fe0a044 commit acc0893

File tree

10 files changed

+235
-6
lines changed

10 files changed

+235
-6
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ For bug fixes, performance enhancements, or fixes to unexported functions we wil
1313
### Added
1414
- add an unexport method (`Bessels.besseljy(nu, x)`) for faster computation of `besselj` and `bessely` (#33)
1515
- add exported methods for Hankel functions `besselh(nu, k, x)`, `hankelh1(nu, x)`, `hankelh2(nu, x)` (#33)
16+
- add exported methods for spherical bessel function `sphericalbesselj(nu, x)`, `sphericalbesselj(nu, x)`, (#38)
1617

1718
### Fixed
1819
- fix cutoff in `bessely` to not return error for integer orders and small arguments (#33)

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Numerical routines for computing Bessel and Hankel functions for real arguments.
66

77
The goal of the library is to provide high quality numerical implementations of Bessel functions with high accuracy without comprimising on computational time. In general, we try to match (and often exceed) the accuracy of other open source routines such as those provided by [SpecialFunctions.jl](https://github.com/JuliaMath/SpecialFunctions.jl). There are instances where we don't quite match that desired accuracy (within a digit or two) but in general will provide implementations that are 5-10x faster (see [benchmarks](https://github.com/heltonmc/Bessels.jl/edit/update_readme/README.md#benchmarks)).
88

9-
The library currently supports Bessel functions, modified Bessel functions and Hankel functions of the first and second kind for positive real arguments and integer and noninteger orders. Negative arguments are also supported only if the return value is real. We plan to support complex arguments in the future. An unexported gamma function is also provided.
9+
The library currently supports Bessel functions, modified Bessel functions, Hankel functions and spherical Bessel functions of the first and second kind for positive real arguments and integer and noninteger orders. Negative arguments are also supported only if the return value is real. We plan to support complex arguments in the future. An unexported gamma function is also provided.
1010

1111
# Quick start
1212

@@ -209,6 +209,8 @@ Benchmarks were run using Julia Version 1.7.2 on an Apple M1 using Rosetta.
209209
- `besselh(nu, k, x)`
210210
- `hankelh1(nu, x)`
211211
- `hankelh2(nu, x)`
212+
- `sphericalbesselj(nu, x)`
213+
- `sphericalbessely(nu, x)`
212214
- `Bessels.gamma(x)`
213215

214216
# Current Development Plans

src/Bessels.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export bessely0
88
export bessely1
99
export bessely
1010

11+
export sphericalbesselj
12+
export sphericalbessely
13+
1114
export besseli
1215
export besselix
1316
export besseli0
@@ -31,6 +34,7 @@ include("besselj.jl")
3134
include("besselk.jl")
3235
include("bessely.jl")
3336
include("hankel.jl")
37+
include("sphericalbessel.jl")
3438

3539
include("Float128/besseli.jl")
3640
include("Float128/besselj.jl")

src/besselj.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,8 @@ function besselj_positive_args(nu::Real, x::T) where T
272272
# Shifting the order up decreases the value substantially for high orders and results in a stable forward recurrence
273273
# as the values rapidly increase
274274

275-
debye_cutoff = 2.0 + 1.00035*x + Base.Math._approx_cbrt(302.681*Float64(x))
276-
nu_shift = ceil(Int, debye_cutoff - nu)
275+
debye_cutoff = ceil(2.0 + 1.00035*x + Base.Math._approx_cbrt(302.681*Float64(x)))
276+
nu_shift = ceil(Int, debye_cutoff - floor(nu))
277277
v = nu + nu_shift
278278
jnu = besseljy_debye(v, x)[1]
279279
jnup1 = besseljy_debye(v+1, x)[1]

src/bessely.jl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ Bessel function of the second kind of order nu, ``Y_{nu}(x)``.
236236
nu and x must be real where nu and x can be positive or negative.
237237
"""
238238
function bessely(nu::Real, x::T) where T
239+
isnan(nu) || isnan(x) && return NaN
239240
isinteger(nu) && return bessely(Int(nu), x)
240241
abs_nu = abs(nu)
241242
abs_x = abs(x)
@@ -317,8 +318,8 @@ function bessely_positive_args(nu, x::T) where T
317318

318319
# at this point x > 19.0 (for Float64) and fairly close to nu
319320
# shift nu down and use the debye expansion for Hankel function (valid x > nu) then use forward recurrence
320-
nu_shift = floor(nu) - ceil(Int, -1.5 + x + Base.Math._approx_cbrt(-411*x))
321-
v2 = nu - maximum((nu_shift, modf(nu)[1] + 1))
321+
nu_shift = ceil(nu) - floor(Int, -1.5 + x + Base.Math._approx_cbrt(-411*x)) + 2
322+
v2 = maximum((nu - nu_shift, modf(nu)[1] + 1))
322323
return besselj_up_recurrence(x, imag(hankel_debye(v2, x)), imag(hankel_debye(v2 - 1, x)), v2, nu)[1]
323324
end
324325

src/hankel.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ function besseljy_positive_args(nu::Real, x::T) where T
125125

126126
# at this point x > 19.0 (for Float64) and fairly close to nu
127127
# shift nu down and use the debye expansion for Hankel function (valid x > nu) then use forward recurrence
128-
nu_shift = floor(nu) - ceil(Int, -1.5 + x + Base.Math._approx_cbrt(-411*x))
128+
nu_shift = ceil(nu) - floor(Int, -1.5 + x + Base.Math._approx_cbrt(-411*x)) + 2
129129
v2 = maximum((nu - nu_shift, modf(nu)[1] + 1))
130130

131131
Hnu = hankel_debye(v2, x)

src/math_constants.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ const PIO4(::Type{Float32}) = 0.78539816339744830962f0
2121
const TWOOPI(::Type{Float32}) = 0.636619772367581343075535f0
2222
const THPIO4(::Type{Float32}) = 2.35619449019234492885f0
2323
const SQ2O2(::Type{Float32}) = 0.7071067811865476f0
24+
const SQPIO2(::Type{Float32}) = 1.25331413731550025f0

src/sphericalbessel.jl

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Spherical Bessel functions
2+
#
3+
# sphericalbesselj(nu, x), sphericalbessely(nu, x)
4+
#
5+
# A numerical routine to compute the spherical bessel functions of the first and second kind.
6+
# For small arguments, the power series series is used for sphericalbesselj. If nu is not a big integer
7+
# then forward recurrence is used if x >= nu. If x < nu, forward recurrence for sphericalbessely is used
8+
# and then a continued fraction and wronskian is used to compute sphericalbesselj [1]. For all other values,
9+
# we directly call besselj and bessely routines.
10+
#
11+
# [1] Ratis, Yu L., and P. Fernández de Córdoba. "A code to calculate (high order) Bessel functions based on the continued fractions method."
12+
# Computer physics communications 76.3 (1993): 381-388.
13+
#
14+
15+
#####
16+
##### Generic routine for `sphericalbesselj`
17+
#####
18+
19+
"""
20+
sphericalbesselj(nu, x)
21+
22+
Spherical bessel function of the first kind of order `nu`, ``j_ν(x)``. This is the non-singular
23+
solution to the radial part of the Helmholz equation in spherical coordinates.
24+
"""
25+
sphericalbesselj(nu::Real, x::Real) = _sphericalbesselj(nu, float(x))
26+
27+
_sphericalbesselj(nu, x::Float16) = Float16(_sphericalbesselj(nu, Float32(x)))
28+
29+
function _sphericalbesselj(nu::Real, x::T) where T <: Union{Float32, Float64}
30+
x < zero(T) && return throw(DomainError(x, "Complex result returned for real arguments. Complex arguments are currently not supported"))
31+
if ~isfinite(x)
32+
isnan(x) && return x
33+
isinf(x) && return zero(x)
34+
end
35+
return nu < zero(T) ? sphericalbesselj_generic(nu, x) : sphericalbesselj_positive_args(nu, x)
36+
end
37+
38+
sphericalbesselj_generic(nu, x::T) where T = SQPIO2(T) * besselj(nu + one(T)/2, x) / sqrt(x)
39+
40+
#####
41+
##### Positive arguments for `sphericalbesselj`
42+
#####
43+
44+
function sphericalbesselj_positive_args(nu::Real, x::T) where T
45+
isinteger(nu) && return sphericalbesselj_positive_args(Int(nu), x)
46+
return sphericalbesselj_small_args_cutoff(nu, x) ? sphericalbesselj_small_args(nu, x) : sphericalbesselj_generic(nu, x)
47+
end
48+
49+
function sphericalbesselj_positive_args(nu::Integer, x::T) where T
50+
sphericalbesselj_small_args_cutoff(nu, x) && return sphericalbesselj_small_args(nu, x)
51+
return (x >= nu && nu < 250) || (x < nu && nu < 60) ? sphericalbesselj_recurrence(nu, x) : sphericalbesselj_generic(nu, x)
52+
end
53+
54+
#####
55+
##### Power series expansion for small arguments
56+
#####
57+
58+
sphericalbesselj_small_args_cutoff(nu, x::T) where T = x^2 / (4*nu + 110) < eps(T)
59+
60+
function sphericalbesselj_small_args(nu, x::T) where T
61+
iszero(x) && return iszero(nu) ? one(T) : zero(T)
62+
x2 = x^2 / 4
63+
coef = evalpoly(x2, (1, -inv(T(3)/2 + nu), -inv(5 + nu), -inv(T(21)/2 + nu), -inv(18 + nu)))
64+
a = SQPIO2(T) / (gamma(T(3)/2 + nu) * 2^(nu + T(1)/2))
65+
return x^nu * a * coef
66+
end
67+
68+
#####
69+
##### Integer recurrence and/or wronskian with continued fraction
70+
#####
71+
72+
# very accurate approach however need to consider some performance issues
73+
# if recurrence is stable (x>=nu) can generate very fast up to orders around 250
74+
# for larger orders it is more efficient to use other expansions
75+
# if (x<nu) we can use forward recurrence from sphericalbessely_recurrence and
76+
# then use a continued fraction approach. However, for largish orders (>60) the
77+
# continued fraction is slower converging and more efficient to use other methods
78+
function sphericalbesselj_recurrence(nu::Integer, x::T) where T
79+
if x >= nu
80+
# forward recurrence if stable
81+
xinv = inv(x)
82+
s, c = sincos(x)
83+
sJ0 = s * xinv
84+
sJ1 = (sJ0 - c) * xinv
85+
86+
nu_start = one(T)
87+
while nu_start < nu + 0.5
88+
sJ0, sJ1 = sJ1, muladd((2*nu_start + 1)*xinv, sJ1, -sJ0)
89+
nu_start += 1
90+
end
91+
return sJ0
92+
elseif x < nu
93+
# compute sphericalbessely with forward recurrence and use continued fraction
94+
sYnm1, sYn = sphericalbessely_forward_recurrence(nu, x)
95+
H = besselj_ratio_jnu_jnum1(nu + T(3)/2, x)
96+
return inv(x^2 * (H*sYnm1 - sYn))
97+
end
98+
end
99+
100+
#####
101+
##### Generic routine for `sphericalbessely`
102+
#####
103+
104+
"""
105+
sphericalbessely(nu, x)
106+
107+
Spherical bessel function of the second kind at order `nu`, ``y_ν(x)``. This is the singular
108+
solution to the radial part of the Helmholz equation in spherical coordinates. Sometimes
109+
known as a spherical Neumann function.
110+
"""
111+
sphericalbessely(nu::Real, x::Real) = _sphericalbessely(nu, float(x))
112+
113+
_sphericalbessely(nu, x::Float16) = Float16(_sphericalbessely(nu, Float32(x)))
114+
115+
function _sphericalbessely(nu::Real, x::T) where T <: Union{Float32, Float64}
116+
x < zero(T) && return throw(DomainError(x, "Complex result returned for real arguments. Complex arguments are currently not supported"))
117+
if ~isfinite(x)
118+
isnan(x) && return x
119+
isinf(x) && return zero(x)
120+
end
121+
return nu < zero(T) ? sphericalbessely_generic(nu, x) : sphericalbessely_positive_args(nu, x)
122+
end
123+
124+
sphericalbessely_generic(nu, x::T) where T = SQPIO2(T) * bessely(nu + one(T)/2, x) / sqrt(x)
125+
126+
#####
127+
##### Positive arguments for `sphericalbesselj`
128+
#####
129+
130+
sphericalbessely_positive_args(nu::Real, x) = isinteger(nu) ? sphericalbessely_positive_args(Int(nu), x) : sphericalbessely_generic(nu, x)
131+
132+
function sphericalbessely_positive_args(nu::Integer, x::T) where T
133+
if besseljy_debye_cutoff(nu, x)
134+
# for very large orders use expansion nu >> x to avoid overflow in recurrence
135+
return SQPIO2(T) * besseljy_debye(nu + one(T)/2, x)[2] / sqrt(x)
136+
elseif nu < 250
137+
return sphericalbessely_forward_recurrence(nu, x)[1]
138+
else
139+
return sphericalbessely_generic(nu, x)
140+
end
141+
end
142+
143+
#####
144+
##### Integer recurrence
145+
#####
146+
147+
function sphericalbessely_forward_recurrence(nu::Integer, x::T) where T
148+
xinv = inv(x)
149+
s, c = sincos(x)
150+
sY0 = -c * xinv
151+
sY1 = xinv * (sY0 - s)
152+
153+
nu_start = one(T)
154+
while nu_start < nu + 0.5
155+
sY0, sY1 = sY1, muladd((2*nu_start + 1)*xinv, sY1, -sY0)
156+
nu_start += 1
157+
end
158+
# need to check if NaN resulted during loop
159+
# this could happen if x is very small and nu is large which eventually results in overflow (-> -Inf)
160+
# NaN inputs were checked in top level function so if sY0 is NaN we should return -infinity
161+
return isnan(sY0) ? (-T(Inf), -T(Inf)) : (sY0, sY1)
162+
end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ import SpecialFunctions
88
@time @testset "bessely" begin include("bessely_test.jl") end
99
@time @testset "hankel" begin include("hankel_test.jl") end
1010
@time @testset "gamma" begin include("gamma_test.jl") end
11+
@time @testset "sphericalbessel" begin include("sphericalbessel_test.jl") end

test/sphericalbessel_test.jl

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# test very small inputs
2+
x = 1e-15
3+
@test Bessels.sphericalbesselj(0, x) SpecialFunctions.sphericalbesselj(0, x)
4+
@test Bessels.sphericalbesselj(1, x) SpecialFunctions.sphericalbesselj(1, x)
5+
@test Bessels.sphericalbesselj(5.5, x) SpecialFunctions.sphericalbesselj(5.5, x)
6+
@test Bessels.sphericalbesselj(10, x) SpecialFunctions.sphericalbesselj(10, x)
7+
@test Bessels.sphericalbessely(0, x) SpecialFunctions.sphericalbessely(0, x)
8+
@test Bessels.sphericalbessely(1, x) SpecialFunctions.sphericalbessely(1, x)
9+
@test Bessels.sphericalbessely(5.5, x) SpecialFunctions.sphericalbessely(5.5, x)
10+
@test Bessels.sphericalbessely(10, x) SpecialFunctions.sphericalbessely(10, x)
11+
12+
# test zero
13+
@test isone(Bessels.sphericalbesselj(0, 0.0))
14+
@test iszero(Bessels.sphericalbesselj(3, 0.0))
15+
@test iszero(Bessels.sphericalbesselj(10.4, 0.0))
16+
@test iszero(Bessels.sphericalbesselj(100.6, 0.0))
17+
18+
@test Bessels.sphericalbessely(0, 0.0) == -Inf
19+
@test Bessels.sphericalbessely(1.8, 0.0) == -Inf
20+
@test Bessels.sphericalbessely(10, 0.0) == -Inf
21+
@test Bessels.sphericalbessely(290, 0.0) == -Inf
22+
23+
# test Inf
24+
@test iszero(Bessels.sphericalbesselj(1, Inf))
25+
@test iszero(Bessels.sphericalbesselj(10.2, Inf))
26+
@test iszero(Bessels.sphericalbessely(3, Inf))
27+
@test iszero(Bessels.sphericalbessely(4.5, Inf))
28+
29+
# test NaN
30+
@test isnan(Bessels.sphericalbesselj(1.4, NaN))
31+
@test isnan(Bessels.sphericalbesselj(4.0, NaN))
32+
@test isnan(Bessels.sphericalbessely(1.4, NaN))
33+
@test isnan(Bessels.sphericalbessely(4.0, NaN))
34+
35+
# test Float16 types
36+
@test Bessels.sphericalbesselj(1.4, Float16(1.2)) isa Float16
37+
@test Bessels.sphericalbessely(1.4, Float16(1.2)) isa Float16
38+
39+
for x in 0.5:1.0:100.0, v in [0, 1, 5.5, 8.2, 10]
40+
@test isapprox(Bessels.sphericalbesselj(v, x), SpecialFunctions.sphericalbesselj(v, x), rtol=1e-12)
41+
@test isapprox(Bessels.sphericalbessely(v, x), SpecialFunctions.sphericalbessely(v, x), rtol=1e-12)
42+
end
43+
44+
for x in 5.5:4.0:160.0, v in [20, 25.0, 32.4, 40.0, 45.12, 50.0, 55.2, 60.124, 70.23, 75.0, 80.0, 92.3, 100.0, 120.0]
45+
@test isapprox(Bessels.sphericalbesselj(v, x), SpecialFunctions.sphericalbesselj(v, x), rtol=3e-12)
46+
@test isapprox(Bessels.sphericalbessely(v, x), SpecialFunctions.sphericalbessely(v, x), rtol=3e-12)
47+
end
48+
49+
@test isapprox(Bessels.sphericalbessely(270, 240.0), SpecialFunctions.sphericalbessely(270, 240.0), rtol=3e-12)
50+
51+
v, x = -4.0, 5.6
52+
@test isapprox(Bessels.sphericalbesselj(v, x), 0.07774965105230025584537, rtol=3e-12)
53+
@test isapprox(Bessels.sphericalbessely(v, x), -0.1833997131521190346258, rtol=3e-12)
54+
55+
v, x = -6.8, 15.6
56+
@test isapprox(Bessels.sphericalbesselj(v, x), 0.04386355397884301866595, rtol=3e-12)
57+
@test isapprox(Bessels.sphericalbessely(v, x), 0.05061013363904335437354, rtol=3e-12)

0 commit comments

Comments
 (0)