Skip to content

Commit abc819f

Browse files
authored
Merge pull request #47 from JuliaMath/cgeoga/master
Update sphericalbesselk and add sphericalbesseli
2 parents e6f0ac3 + 26ae05c commit abc819f

File tree

7 files changed

+148
-37
lines changed

7 files changed

+148
-37
lines changed

NEWS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ For bug fixes, performance enhancements, or fixes to unexported functions we wil
1111
# Unreleased
1212

1313
### Added
14-
- Add more optimized methods for Float32 calculations that are faster([PR #43](https://github.com/JuliaMath/Bessels.jl/pull/43))
14+
- Add more optimized methods for Float32 calculations that are faster ([PR #43](https://github.com/JuliaMath/Bessels.jl/pull/43))
15+
- Add methods for computing modified spherical bessel function of second ([PR #46](https://github.com/JuliaMath/Bessels.jl/pull/46) contributed by @cgeoga and first ([PR #47](https://github.com/JuliaMath/Bessels.jl/pull/47))) kind. These functions are currently not exported. Closes ([Issue #25](https://github.com/JuliaMath/Bessels.jl/issues/25))
16+
- Asymptotic expansion for x >> nu was added ([PR #48](https://github.com/JuliaMath/Bessels.jl/pull/48)) that decreases computation time for large arguments. Contributed by @cgeoga
1517

1618
### Fixed
1719
- Reduce compile time and time to first call of besselj and bessely ([PR #42](https://github.com/JuliaMath/Bessels.jl/pull/42))

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "Bessels"
22
uuid = "0e736298-9ec6-45e8-9647-e4fc86a2fe38"
33
authors = ["Michael Helton <[email protected]> and contributors"]
4-
version = "0.2.0"
4+
version = "0.2.1"
55

66
[compat]
77
julia = "1.6"

src/besseli.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ _besseli(nu, x::Float16) = Float16(_besseli(nu, Float32(x)))
170170

171171
function _besseli(nu, x::T) where T <: Union{Float32, Float64}
172172
isinteger(nu) && return _besseli(Int(nu), x)
173+
~isfinite(x) && return x
173174
abs_nu = abs(nu)
174175
abs_x = abs(x)
175176

@@ -192,6 +193,7 @@ function _besseli(nu, x::T) where T <: Union{Float32, Float64}
192193
end
193194
end
194195
function _besseli(nu::Integer, x::T) where T <: Union{Float32, Float64}
196+
~isfinite(x) && return x
195197
abs_nu = abs(nu)
196198
abs_x = abs(x)
197199
sg = iseven(abs_nu) ? 1 : -1
@@ -211,7 +213,6 @@ Modified Bessel function of the first kind of order nu, ``I_{nu}(x)`` for positi
211213
function besseli_positive_args(nu, x::T) where T <: Union{Float32, Float64}
212214
iszero(nu) && return besseli0(x)
213215
isone(nu) && return besseli1(x)
214-
isinf(x) && return T(Inf)
215216

216217
# use large argument expansion if x >> nu
217218
besseli_large_argument_cutoff(nu, x) && return besseli_large_argument(nu, x)

src/besselk.jl

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ end
160160
#
161161
# The identities are computed by calling the `besseli_positive_args(nu, x)` function which computes K_{ν}(x)
162162
# for positive arguments and orders. For large orders, Debye's uniform asymptotic expansions are used.
163+
# For large arguments x >> nu, large argument expansion is used [9].
163164
# For small value and when nu > ~x the power series is used. The rest of the values are computed using slightly different methods.
164165
# The power series for besseli is modified to give both I_{v} and I_{v-1} where the ratio K_{v+1} / K_{v} is computed using continued fractions [8].
165166
# The wronskian connection formula is then used to compute K_v.
@@ -178,6 +179,7 @@ end
178179
# arXiv preprint arXiv:2201.00090 (2022).
179180
# [8] Cuyt, A. A., Petersen, V., Verdonk, B., Waadeland, H., & Jones, W. B. (2008).
180181
# Handbook of continued fractions for special functions. Springer Science & Business Media.
182+
# [9] http://dlmf.nist.gov/10.40.E2
181183
#
182184

183185
"""
@@ -226,17 +228,14 @@ function besselk_positive_args(nu, x::T) where T <: Union{Float32, Float64}
226228
# dispatch to avoid uniform expansion when nu = 0
227229
iszero(nu) && return besselk0(x)
228230

229-
# pre-compute the uniform asymptotic expansion cutoff:
230-
debye_cut = besselik_debye_cutoff(nu, x)
231+
# check if nu is a half-integer
232+
(isinteger(nu-1/2) && sphericalbesselk_cutoff(nu)) && return sphericalbesselk_int(Int(nu-1/2), x)*SQPIO2(T)*sqrt(x)
231233

232-
# check if nu is a half-integer:
233-
(isinteger(nu-1/2) && !debye_cut) && return sphericalbesselk(nu-1/2, x)*SQRT_PID2(T)*sqrt(x)
234-
235-
# check if the standard asymptotic expansion can be used:
236-
besselk_asexp_cutoff(nu, x) && return besselk_large_argument(nu, x)
234+
# check if the standard asymptotic expansion can be used
235+
besseli_large_argument_cutoff(nu, x) && return besselk_large_argument(nu, x)
237236

238237
# use uniform debye expansion if x or nu is large
239-
debye_cut && return besselk_large_orders(nu, x)
238+
besselik_debye_cutoff(nu, x) && return besselk_large_orders(nu, x)
240239

241240
# for integer nu use forward recurrence starting with K_0 and K_1
242241
isinteger(nu) && return besselk_up_recurrence(x, besselk1(x), besselk0(x), 1, nu)[1]
@@ -260,6 +259,9 @@ function _besselkx(nu, x::T) where T <: Union{Float32, Float64}
260259
# dispatch to avoid uniform expansion when nu = 0
261260
iszero(nu) && return besselk0x(x)
262261

262+
# check if the standard asymptotic expansion can be used
263+
besseli_large_argument_cutoff(nu, x) && return besselk_large_argument_scaled(nu, x)
264+
263265
# use uniform debye expansion if x or nu is large
264266
besselik_debye_cutoff(nu, x) && return besselk_large_orders_scaled(nu, x)
265267

@@ -434,13 +436,13 @@ end
434436
besselk_power_series_cutoff(nu, x::Float64) = x < 2.0 || nu > 1.6x - 1.0
435437
besselk_power_series_cutoff(nu, x::Float32) = x < 10.0f0 || nu > 1.65f0*x - 8.0f0
436438

439+
#####
440+
##### Large argument expansion for K_{nu}(x)
441+
#####
437442

438-
"""
439-
besselk_asymptoticexp(v, x, order)
440-
441-
Computes the asymptotic expansion of K_ν w.r.t. argument. Accurate for large x, and faster than uniform asymptotic expansion for small to small-ish orders. The default order of the expansion in `Bessels.besselk` is 10.
442-
"""
443-
besselk_asexp_cutoff(nu, x::T) where T = (nu < 20.0) && (x > ASEXP_CUTOFF(T))
443+
# Computes the asymptotic expansion of K_ν w.r.t. argument.
444+
# Accurate for large x, and faster than uniform asymptotic expansion for small to small-ish orders
445+
# See http://dlmf.nist.gov/10.40.E2
444446

445447
function besselk_large_argument(v, x::T) where T
446448
a = exp(-x / 2)

src/constants.jl

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,3 @@ const Q3_k1(::Type{Float64}) = (
284284
2.88448064302447607e1, 2.27912927104139732e0,
285285
2.50358186953478678e-2
286286
)
287-
288-
const SQRT_PID2(::Type{Float64}) = 1.2533141373155003
289-
const SQRT_PID2(::Type{Float32}) = 1.2533141f0
290-
291-
const ASEXP_CUTOFF(::Type{Float64}) = 35.0
292-
const ASEXP_CUTOFF(::Type{Float32}) = 35.0f0 # temporary

src/modifiedsphericalbessel.jl

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,98 @@
1-
1+
# Modified Spherical Bessel functions
2+
#
3+
# sphericalbesseli(nu, x), sphericalbesselk(nu, x)
4+
#
5+
# A numerical routine to compute the modified spherical bessel functions of the first and second kind.
6+
# The modified spherical bessel function of the first kind is computed using the power series for small arguments,
7+
# explicit formulas for (nu=0,1,2), and using its relation to besseli for other arguments [1].
8+
# The modified bessel function of the second kind is computed for small to moderate integer orders using forward recurrence starting from explicit formulas for k0(x) = exp(-x) / x and k1(x) = k0(x) * (x+1) / x [2].
9+
# Large orders are determined from the uniform asymptotic expansions (see src/besselk.jl for details)
10+
# For non-integer orders, we directly call the besselk routine using the relation k_{n}(x) = sqrt(pi/(2x))*besselk(n+1/2, x) [2].
11+
#
12+
# [1] https://mathworld.wolfram.com/ModifiedBesselFunctionoftheFirstKind.html
13+
# [2] https://mathworld.wolfram.com/ModifiedSphericalBesselFunctionoftheSecondKind.html
14+
#
215
"""
316
sphericalbesselk(nu, x::T) where T <: {Float32, Float64}
417
518
Computes `k_{ν}(x)`, the modified second-kind spherical Bessel function, and offers special branches for integer orders.
619
"""
7-
sphericalbesselk(nu, x) = _sphericalbesselk(nu, float(x))
20+
sphericalbesselk(nu::Real, x::Real) = _sphericalbesselk(nu, float(x))
21+
22+
_sphericalbesselk(nu, x::Float16) = Float16(_sphericalbesselk(nu, Float32(x)))
823

9-
function _sphericalbesselk(nu, x::T) where T
10-
isnan(x) && return NaN
11-
if isinteger(nu) && nu < 41.5
24+
function _sphericalbesselk(nu, x::T) where T <: Union{Float32, Float64}
25+
if ~isfinite(x)
26+
isnan(x) && return x
27+
isinf(x) && return zero(x)
28+
end
29+
if isinteger(nu) && sphericalbesselk_cutoff(nu)
1230
if x < zero(x)
1331
return throw(DomainError(x, "Complex result returned for real arguments. Complex arguments are currently not supported"))
1432
end
15-
# using ifelse here to hopefully cut out a branch on nu < 0 or not. The
16-
# symmetry here is that
33+
# using ifelse here to cut out a branch on nu < 0 or not.
34+
# The symmetry here is that
1735
# k_{-n} = (...)*K_{-n + 1/2}
1836
# = (...)*K_{|n| - 1/2}
1937
# = (...)*K_{|n|-1 + 1/2}
20-
# = k_{|n|-1}
38+
# = k_{|n|-1}
2139
_nu = ifelse(nu<zero(nu), -one(nu)-nu, nu)
2240
return sphericalbesselk_int(Int(_nu), x)
2341
else
24-
return inv(SQRT_PID2(T)*sqrt(x))*besselk(nu+1/2, x)
42+
return inv(SQPIO2(T)*sqrt(x))*besselk(nu+1/2, x)
2543
end
2644
end
45+
sphericalbesselk_cutoff(nu) = nu < 41.5
2746

2847
function sphericalbesselk_int(v::Int, x)
29-
b0 = inv(x)
30-
b1 = (x+one(x))/(x*x)
31-
iszero(v) && return b0*exp(-x)
48+
xinv = inv(x)
49+
b0 = exp(-x) * xinv
50+
b1 = b0 * (x + one(x)) * xinv
51+
iszero(v) && return b0
3252
_v = one(v)
3353
invx = inv(x)
3454
while _v < v
3555
_v += one(_v)
3656
b0, b1 = b1, b0 + (2*_v - one(_v))*b1*invx
3757
end
38-
exp(-x)*b1
58+
b1
3959
end
4060

61+
"""
62+
sphericalbesseli(nu, x::T) where T <: {Float32, Float64}
63+
64+
Computes `i_{ν}(x)`, the modified first-kind spherical Bessel function.
65+
"""
66+
sphericalbesseli(nu::Real, x::Real) = _sphericalbesseli(nu, float(x))
67+
68+
_sphericalbesseli(nu, x::Float16) = Float16(_sphericalbesseli(nu, Float32(x)))
69+
70+
function _sphericalbesseli(nu, x::T) where T <: Union{Float32, Float64}
71+
isinf(x) && return x
72+
x < zero(x) && throw(DomainError(x, "Complex result returned for real arguments. Complex arguments are currently not supported"))
73+
74+
sphericalbesselj_small_args_cutoff(nu, x::T) && return sphericalbesseli_small_args(nu, x)
75+
isinteger(nu) && return _sphericalbesseli_small_orders(Int(nu), x)
76+
return SQPIO2(T)*besseli(nu+1/2, x) / sqrt(x)
77+
end
78+
79+
function _sphericalbesseli_small_orders(nu::Integer, x::T) where T
80+
# prone to cancellation in the subtraction
81+
# best to expand and group
82+
nu_abs = abs(nu)
83+
x2 = x*x
84+
sinhx = sinh(x)
85+
coshx = cosh(x)
86+
nu_abs == 0 && return sinhx / x
87+
nu_abs == 1 && return (x*coshx - sinhx) / x2
88+
nu_abs == 2 && return (x2*sinhx + 3*(sinhx - x*coshx)) / (x2*x)
89+
return SQPIO2(T)*besseli(nu+1/2, x) / sqrt(x)
90+
end
91+
92+
function sphericalbesseli_small_args(nu, x::T) where T
93+
iszero(x) && return iszero(nu) ? one(T) : x
94+
x2 = x^2 / 4
95+
coef = evalpoly(x2, (1, inv(T(3)/2 + nu), inv(5 + nu), inv(T(21)/2 + nu), inv(18 + nu)))
96+
a = SQPIO2(T) / (gamma(T(3)/2 + nu) * 2^(nu + T(1)/2))
97+
return x^nu * a * coef
98+
end

test/sphericalbessel_test.jl

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ x = 1e-15
88
@test Bessels.sphericalbessely(1, x) SpecialFunctions.sphericalbessely(1, x)
99
@test Bessels.sphericalbessely(5.5, x) SpecialFunctions.sphericalbessely(5.5, x)
1010
@test Bessels.sphericalbessely(10, x) SpecialFunctions.sphericalbessely(10, x)
11+
@test Bessels.sphericalbesselk(5.5, x) SpecialFunctions.besselk(5.5 + 1/2, x) * sqrt( 2 / (x*pi))
12+
@test Bessels.sphericalbesselk(10, x) SpecialFunctions.besselk(10 + 1/2, x) * sqrt( 2 / (x*pi))
13+
14+
x = 1e-20
15+
@test Bessels.sphericalbesseli(0, x) SpecialFunctions.besseli(0 + 1/2, x) * sqrt( pi / (x*2))
16+
@test Bessels.sphericalbesseli(1, x) SpecialFunctions.besseli(1 + 1/2, x) * sqrt( pi / (x*2))
17+
@test Bessels.sphericalbesseli(2, x) SpecialFunctions.besseli(2 + 1/2, x) * sqrt( pi / (x*2))
18+
@test Bessels.sphericalbesseli(3, x) SpecialFunctions.besseli(3 + 1/2, x) * sqrt( pi / (x*2))
19+
@test Bessels.sphericalbesseli(4, x) SpecialFunctions.besseli(4 + 1/2, x) * sqrt( pi / (x*2))
20+
@test Bessels.sphericalbesseli(6.5, x) SpecialFunctions.besseli(6.5 + 1/2, x) * sqrt( pi / (x*2))
1121

1222
# test zero
1323
@test isone(Bessels.sphericalbesselj(0, 0.0))
@@ -20,33 +30,77 @@ x = 1e-15
2030
@test Bessels.sphericalbessely(10, 0.0) == -Inf
2131
@test Bessels.sphericalbessely(290, 0.0) == -Inf
2232

33+
@test isinf(Bessels.sphericalbesselk(0, 0.0))
34+
@test isinf(Bessels.sphericalbesselk(4, 0.0))
35+
@test isinf(Bessels.sphericalbesselk(10.2, 0.0))
36+
37+
x = 0.0
38+
@test isone(Bessels.sphericalbesseli(0, x))
39+
@test iszero(Bessels.sphericalbesseli(1, x))
40+
@test iszero(Bessels.sphericalbesseli(2, x))
41+
@test iszero(Bessels.sphericalbesseli(3, x))
42+
@test iszero(Bessels.sphericalbesseli(4, x))
43+
@test iszero(Bessels.sphericalbesseli(6.4, x))
44+
2345
# test Inf
2446
@test iszero(Bessels.sphericalbesselj(1, Inf))
2547
@test iszero(Bessels.sphericalbesselj(10.2, Inf))
2648
@test iszero(Bessels.sphericalbessely(3, Inf))
2749
@test iszero(Bessels.sphericalbessely(4.5, Inf))
2850

51+
@test iszero(Bessels.sphericalbesselk(0, Inf))
52+
@test iszero(Bessels.sphericalbesselk(4, Inf))
53+
@test iszero(Bessels.sphericalbesselk(10.2, Inf))
54+
55+
x = Inf
56+
@test isinf(Bessels.sphericalbesseli(0, x))
57+
@test isinf(Bessels.sphericalbesseli(1, x))
58+
@test isinf(Bessels.sphericalbesseli(2, x))
59+
@test isinf(Bessels.sphericalbesseli(3, x))
60+
@test isinf(Bessels.sphericalbesseli(4, x))
61+
@test isinf(Bessels.sphericalbesseli(6.4, x))
62+
2963
# test NaN
3064
@test isnan(Bessels.sphericalbesselj(1.4, NaN))
3165
@test isnan(Bessels.sphericalbesselj(4.0, NaN))
3266
@test isnan(Bessels.sphericalbessely(1.4, NaN))
3367
@test isnan(Bessels.sphericalbessely(4.0, NaN))
3468

69+
@test isnan(Bessels.sphericalbesselk(1.4, NaN))
70+
@test isnan(Bessels.sphericalbesselk(4.0, NaN))
71+
72+
x = NaN
73+
@test isnan(Bessels.sphericalbesseli(0, x))
74+
@test isnan(Bessels.sphericalbesseli(1, x))
75+
@test isnan(Bessels.sphericalbesseli(2, x))
76+
@test isnan(Bessels.sphericalbesseli(3, x))
77+
@test isnan(Bessels.sphericalbesseli(4, x))
78+
@test isnan(Bessels.sphericalbesseli(6.4, x))
79+
3580
# test Float16, Float32 types
3681
@test Bessels.sphericalbesselj(Float16(1.4), Float16(1.2)) isa Float16
3782
@test Bessels.sphericalbessely(Float16(1.4), Float16(1.2)) isa Float16
3883
@test Bessels.sphericalbesselj(1.4f0, 1.2f0) isa Float32
3984
@test Bessels.sphericalbessely(1.4f0, 1.2f0) isa Float32
4085

86+
@test Bessels.sphericalbesselk(Float16(1.4), Float16(1.2)) isa Float16
87+
@test Bessels.sphericalbesselk(1.0f0, 1.2f0) isa Float32
88+
89+
@test Bessels.sphericalbesseli(Float16(1.4), Float16(1.2)) isa Float16
90+
@test Bessels.sphericalbesseli(1.0f0, 1.2f0) isa Float32
4191

42-
for x in 0.5:1.0:100.0, v in [0, 1, 5.5, 8.2, 10]
92+
for x in 0.5:1.5:100.0, v in [0, 1, 2, 3, 4, 5.5, 8.2, 10]
4393
@test isapprox(Bessels.sphericalbesselj(v, x), SpecialFunctions.sphericalbesselj(v, x), rtol=1e-12)
4494
@test isapprox(Bessels.sphericalbessely(v, x), SpecialFunctions.sphericalbessely(v, x), rtol=1e-12)
95+
@test isapprox(Bessels.sphericalbesselk(v, x), SpecialFunctions.besselk(v+1/2, x) * sqrt( 2 / (x*pi)), rtol=1e-12)
96+
@test isapprox(Bessels.sphericalbesseli(v, x), SpecialFunctions.besseli(v+1/2, x) * sqrt( pi / (x*2)), rtol=1e-12)
4597
end
4698

4799
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]
48100
@test isapprox(Bessels.sphericalbesselj(v, x), SpecialFunctions.sphericalbesselj(v, x), rtol=3e-12)
49101
@test isapprox(Bessels.sphericalbessely(v, x), SpecialFunctions.sphericalbessely(v, x), rtol=3e-12)
102+
@test isapprox(Bessels.sphericalbesselk(v, x), SpecialFunctions.besselk(v+1/2, x) * sqrt( 2 / (x*pi)), rtol=1e-12)
103+
@test isapprox(Bessels.sphericalbesseli(v, x), SpecialFunctions.besseli(v+1/2, x) * sqrt( pi / (x*2)), rtol=1e-12)
50104
end
51105

52106
@test isapprox(Bessels.sphericalbessely(270, 240.0), SpecialFunctions.sphericalbessely(270, 240.0), rtol=3e-12)

0 commit comments

Comments
 (0)