Skip to content

Commit 6dfcf3d

Browse files
authored
Merge pull request #33 from JuliaMath/hankel
Support Hankel functions and efficient methods to get Jv and Yv
2 parents 142e823 + d07f5d4 commit 6dfcf3d

File tree

10 files changed

+284
-22
lines changed

10 files changed

+284
-22
lines changed

NEWS.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Changelog
2+
All notable changes to this project will be documented in this file.
3+
4+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6+
7+
For pre-1.0.0 releases we will increment the minor version when we export any new functions or alter exported API.
8+
For bug fixes, performance enhancements, or fixes to unexported functions we will increment the patch version.
9+
**Note**: The exported API can be considered very stable and likely will not change without serious consideration.
10+
11+
# Unreleased
12+
13+
### Added
14+
- add an unexport method (`Bessels.besseljy(nu, x)`) for faster computation of `besselj` and `bessely` (#33)
15+
- add exported methods for Hankel functions `besselh(nu, k, x)`, `hankelh1(nu, x)`, `hankelh2(nu, x)` (#33)
16+
17+
### Fixed
18+
- fix cutoff in `bessely` to not return error for integer orders and small arguments (#33)
19+
20+
# Version 0.1.0
21+
22+
Initial release of Bessels.jl
23+
24+
### Added
25+
26+
- support bessel functions (`besselj`, `bessely`, `besseli`, `besselk`) for real arguments
27+
- provide a gamma function (`Bessels.gamma`) for real arguments

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
[![Build Status](https://github.com/heltonmc/Bessels.jl/actions/workflows/CI.yml/badge.svg?branch=master)](https://github.com/heltonmc/Bessels.jl/actions/workflows/CI.yml?query=branch%3Amaster)
33
[![Coverage](https://codecov.io/gh/heltonmc/Bessels.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/heltonmc/Bessels.jl)
44

5-
Numerical routines for computing Bessel functions and modified Bessel functions of the first and second kind. These routines are written in the Julia programming language and are self contained without any external dependencies.
5+
Numerical routines for computing Bessel and Hankel functions for real arguments. These routines are written in the Julia programming language and are self contained without any external dependencies.
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 only supports Bessel functions and modified 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.
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.
1010

1111
# Quick start
1212

@@ -206,12 +206,14 @@ Benchmarks were run using Julia Version 1.7.2 on an Apple M1 using Rosetta.
206206
- `besselk0(x)`
207207
- `besselk1(x)`
208208
- `besselk(nu, x)`
209+
- `besselh(nu, k, x)`
210+
- `hankelh1(nu, x)`
211+
- `hankelh2(nu, x)`
209212
- `Bessels.gamma(x)`
210213

211214
# Current Development Plans
212215

213216
- Support for higher precision `Double64`, `Float128`
214-
- Hankel functions
215217
- Support for complex arguments (`x` and `nu`)
216218
- Airy functions
217219
- Support for derivatives with respect to argument and order

src/Bessels.jl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,23 @@ export besselk0x
2222
export besselk1
2323
export besselk1x
2424

25+
export besselh
26+
export hankelh1
27+
export hankelh2
28+
2529
include("besseli.jl")
2630
include("besselj.jl")
2731
include("besselk.jl")
2832
include("bessely.jl")
29-
include("constants.jl")
33+
include("hankel.jl")
3034

3135
include("Float128/besseli.jl")
3236
include("Float128/besselj.jl")
3337
include("Float128/besselk.jl")
3438
include("Float128/bessely.jl")
3539
include("Float128/constants.jl")
3640

41+
include("constants.jl")
3742
include("math_constants.jl")
3843
include("U_polynomials.jl")
3944
include("recurrence.jl")
@@ -42,6 +47,4 @@ include("Polynomials/besselj_polys.jl")
4247
include("asymptotics.jl")
4348
include("gamma.jl")
4449

45-
#include("hankel.jl")
46-
4750
end

src/U_polynomials.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function besseljy_debye(v, x)
4141
return coef_Jn * Uk_Jn, coef_Yn * Uk_Yn
4242
end
4343

44-
besseljy_debye_cutoff(nu, x) = nu > 2.0 + 1.00035*x + Base.Math._approx_cbrt(Float64(302.681)*x) && x > 15
44+
besseljy_debye_cutoff(nu, x) = nu > 2.0 + 1.00035*x + Base.Math._approx_cbrt(Float64(302.681)*x) && nu > 15
4545

4646
"""
4747
hankel_debye(nu, x::T)

src/bessely.jl

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,13 @@ end
228228
#####
229229
##### Generic routine for `bessely`
230230
#####
231+
231232
"""
232233
bessely(nu, x::T) where T <: Float64
233234
234-
Bessel function of the first kind of order nu, ``Y_{nu}(x)``.
235+
Bessel function of the second kind of order nu, ``Y_{nu}(x)``.
235236
nu and x must be real where nu and x can be positive or negative.
236237
"""
237-
238238
function bessely(nu::Real, x::T) where T
239239
isinteger(nu) && return bessely(Int(nu), x)
240240
abs_nu = abs(nu)
@@ -310,10 +310,10 @@ function bessely_positive_args(nu, x::T) where T
310310
hankel_debye_cutoff(nu, x) && return imag(hankel_debye(nu, x))
311311

312312
# use power series for small x and for when nu > x
313-
bessely_series_cutoff(nu, x) && return bessely_power_series(nu, x)
313+
bessely_series_cutoff(nu, x) && return bessely_power_series(nu, x)[1]
314314

315315
# for x ∈ (6, 19) we use Chebyshev approximation and forward recurrence
316-
besseljy_chebyshev_cutoff(nu, x) && return bessely_chebyshev(nu, x)
316+
besseljy_chebyshev_cutoff(nu, x) && return bessely_chebyshev(nu, x)[1]
317317

318318
# at this point x > 19.0 (for Float64) and fairly close to nu
319319
# shift nu down and use the debye expansion for Hankel function (valid x > nu) then use forward recurrence
@@ -340,6 +340,7 @@ end
340340
341341
Computes ``Y_{nu}(x)`` using the power series when nu is not an integer.
342342
In general, this is most accurate for small arguments and when nu > x.
343+
Outpus both (Y_{nu}(x), J_{nu}(x)).
343344
"""
344345
function bessely_power_series(v, x::T) where T
345346
MaxIter = 3000
@@ -358,7 +359,7 @@ function bessely_power_series(v, x::T) where T
358359
b *= -inv((-v + i + one(T)) * (i + one(T))) * t2
359360
end
360361
s, c = sincospi(v)
361-
return (out*c - out2) / s
362+
return (out*c - out2) / s, out
362363
end
363364
bessely_series_cutoff(v, x) = (x < 7.0) || v > 1.35*x - 4.5
364365

@@ -375,7 +376,7 @@ Forward recurrence is used to fill orders starting at low orders ν ∈ (0, 2).
375376
function bessely_chebyshev(v, x)
376377
v_floor, _ = modf(v)
377378
Y0, Y1 = bessely_chebyshev_low_orders(v_floor, x)
378-
return besselj_up_recurrence(x, Y1, Y0, v_floor + 1, v)[1]
379+
return besselj_up_recurrence(x, Y1, Y0, v_floor + 1, v)
379380
end
380381

381382
# only implemented for Float64 so far

src/hankel.jl

Lines changed: 200 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,207 @@
1-
#= Aren't tested yet
2-
function besselh(nu::Float64, k::Integer, x::AbstractFloat)
1+
# Hankel functions
2+
#
3+
# besseljy(nu, x)
4+
# besselh(nu, x), hankelh1(nu, x), hankelh2(nu, x)
5+
#
6+
# A numerical routine to compute both Bessel functions of the first J_{ν}(x) and second kind Y_{ν}(x)
7+
# for real orders and arguments of positive or negative value. Please see notes in src/besselj.jl and src/bessely.jl
8+
# for details on implementation as most of the routine is similar. A key difference is when the methods to compute bessely
9+
# don't also give besselj. We then rely on a continued fraction approach to compute J_{ν}(x)/J_{ν-1}(x) to get J_{ν}(x)
10+
# from Y_{ν}(x) and Y_{ν-1}(x)[1]. The continued fraction approach is more quickly converging when nu and x are small in magnitude.
11+
# When x and nu are large and nu = x + ϵ, we fall back to computing J_{ν}(x) and Y_{ν}(x) separately as this was found to be more efficient.
12+
#
13+
# [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."
14+
# Computer physics communications 76.3 (1993): 381-388.
15+
#
16+
17+
#####
18+
##### Generic routine for `besseljy`
19+
#####
20+
21+
"""
22+
besseljy(nu, x::T) where T <: Float64
23+
24+
Return both Bessel functions of the first ``J_{nu}(x)`` and
25+
second ``Y_{nu}(x)`` kind. This method will be faster than calling (besselj(nu, x), bessely(nu, x)) separately,
26+
unless nu is slightly larger than x.
27+
28+
Results may be slightly different than individual functions in some domains due to using different algorithms.
29+
"""
30+
function besseljy(nu::Real, x::T) where T
31+
isinteger(nu) && return besseljy(Int(nu), x)
32+
abs_nu = abs(nu)
33+
abs_x = abs(x)
34+
35+
Jnu, Ynu = besseljy_positive_args(abs_nu, abs_x)
36+
37+
if nu >= zero(T)
38+
if x >= zero(T)
39+
return Jnu, Ynu
40+
else
41+
return throw(DomainError(x, "Complex result returned for real arguments. Complex arguments are currently not supported"))
42+
#return Ynu * cispi(-nu) + 2im * besselj_positive_args(abs_nu, abs_x) * cospi(abs_nu)
43+
end
44+
else
45+
spi, cpi = sincospi(abs_nu)
46+
out = Jnu * cpi - Ynu * spi
47+
if x >= zero(T)
48+
return out, Ynu * cpi + Jnu * spi
49+
else
50+
return throw(DomainError(x, "Complex result returned for real arguments. Complex arguments are currently not supported"))
51+
#return cpi * (Ynu * cispi(nu) + 2im * Jnu * cpi) + Jnu * spi * cispi(abs_nu)
52+
end
53+
end
54+
end
55+
56+
function besseljy(nu::Integer, x::T) where T
57+
abs_nu = abs(nu)
58+
abs_x = abs(x)
59+
sg = iseven(abs_nu) ? 1 : -1
60+
61+
Jnu, Ynu = besseljy_positive_args(abs_nu, abs_x)
62+
63+
if nu >= zero(T)
64+
if x >= zero(T)
65+
return Jnu, Ynu
66+
else
67+
return throw(DomainError(x, "Complex result returned for real arguments. Complex arguments are currently not supported"))
68+
#return Jnu * sg, Ynu * sg + 2im * sg * besselj_positive_args(abs_nu, abs_x)
69+
end
70+
else
71+
if x >= zero(T)
72+
return Jnu * sg, Ynu * sg
73+
else
74+
return throw(DomainError(x, "Complex result returned for real arguments. Complex arguments are currently not supported"))
75+
#spi, cpi = sincospi(abs_nu)
76+
#return (cpi*Jnu - spi*Ynu) * sg, Ynu + 2im * besselj_positive_args(abs_nu, abs_x)
77+
end
78+
end
79+
end
80+
81+
#####
82+
##### `besseljy` for positive arguments and orders
83+
#####
84+
85+
function besseljy_positive_args(nu::Real, x::T) where T
86+
nu == 0 && return (besselj0(x), bessely0(x))
87+
nu == 1 && return (besselj1(x), bessely1(x))
88+
iszero(x) && return (besselj_positive_args(nu, x), bessely_positive_args(nu, x))
89+
90+
# x < ~nu branch see src/U_polynomials.jl
91+
besseljy_debye_cutoff(nu, x) && return besseljy_debye(nu, x)
92+
93+
# large argument branch see src/asymptotics.jl
94+
besseljy_large_argument_cutoff(nu, x) && return besseljy_large_argument(nu, x)
95+
96+
# x > ~nu branch see src/U_polynomials.jl on computing Hankel function
97+
if hankel_debye_cutoff(nu, x)
98+
H = hankel_debye(nu, x)
99+
return real(H), imag(H)
100+
end
101+
# use forward recurrence if nu is an integer up until the continued fraction becomes inefficient
102+
if isinteger(nu) && nu < 150
103+
Y0 = bessely0(x)
104+
Y1 = bessely1(x)
105+
Ynm1, Yn = besselj_up_recurrence(x, Y1, Y0, 1, nu-1)
106+
107+
ratio_Jv_Jvm1 = besselj_ratio_jnu_jnum1(nu, x)
108+
Jn = 2 /*x * (Ynm1 - Yn / ratio_Jv_Jvm1))
109+
return Jn, Yn
110+
end
111+
112+
# use power series for small x and for when nu > x
113+
if bessely_series_cutoff(nu, x) && besselj_series_cutoff(nu, x)
114+
Yn, Jn = bessely_power_series(nu, x)
115+
return Jn, Yn
116+
end
117+
118+
# for x ∈ (6, 19) we use Chebyshev approximation and forward recurrence
119+
if besseljy_chebyshev_cutoff(nu, x)
120+
Yn, Ynp1 = bessely_chebyshev(nu, x)
121+
ratio_Jvp1_Jv = besselj_ratio_jnu_jnum1(nu+1, x)
122+
Jnp1 = 2 /*x * (Yn - Ynp1 / ratio_Jvp1_Jv))
123+
return Jnp1 / ratio_Jvp1_Jv, Yn
124+
end
125+
126+
# at this point x > 19.0 (for Float64) and fairly close to nu
127+
# 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))
129+
v2 = maximum((nu - nu_shift, modf(nu)[1] + 1))
130+
131+
Hnu = hankel_debye(v2, x)
132+
Hnum1 = hankel_debye(v2 - 1, x)
133+
134+
# forward recurrence is stable for Hankel when x >= nu
135+
if x >= nu
136+
H = besselj_up_recurrence(x, Hnu, Hnum1, v2, nu)[1]
137+
return real(H), imag(H)
138+
else
139+
# At this point besselj can not be calculated with forward recurrence
140+
# We could calculate it from bessely using the continued fraction approach like the following
141+
# Yn, Ynp1 = besselj_up_recurrence(x, imag(Hnu), imag(Hnum1), v2, nu)
142+
# ratio_Jvp1_Jv = besselj_ratio_jnu_jnum1(nu+1, x)
143+
# Jnp1 = 2 / (π*x * (Yn - Ynp1 / ratio_Jvp1_Jv))
144+
# return Jnp1 / ratio_Jvp1_Jv, Yn
145+
# However, the continued fraction approach is slowly converging for large arguments
146+
# We will fall back to computing besselj separately instead
147+
return besselj_positive_args(nu, x), besselj_up_recurrence(x, imag(Hnu), imag(Hnum1), v2, nu)[1]
148+
end
149+
end
150+
151+
#####
152+
##### Continued fraction for J_{ν}(x)/J_{ν-1}(x)
153+
#####
154+
155+
# implements continued fraction to compute ratio of J_{ν}(x)/J_{ν-1}(x)
156+
# using equation 22 and 24 of [1]
157+
# in general faster converging for small magnitudes of x and nu and nu >> x
158+
function besselj_ratio_jnu_jnum1(n, x::T) where T
159+
MaxIter = 5000
160+
xinv = inv(x)
161+
xinv2 = 2 * xinv
162+
d = x / (n + n)
163+
a = d
164+
h = a
165+
b = muladd(2, n, 2) * xinv
166+
for _ in 1:MaxIter
167+
d = inv(b - d)
168+
a *= muladd(b, d, -1)
169+
h = h + a
170+
b = b + xinv2
171+
abs(a / h) <= eps(T) && break
172+
end
173+
return h
174+
end
175+
176+
#####
177+
##### Hankel functions
178+
#####
179+
180+
"""
181+
besselh(nu, [k=1,] x)
182+
183+
Bessel function of the third kind of order `nu` (the Hankel function). `k` is either 1 or 2,
184+
selecting [`hankelh1`](@ref) or [`hankelh2`](@ref), respectively.
185+
"""
186+
function besselh(nu::Real, k::Integer, x)
187+
Jn, Yn = besseljy(nu, x)
3188
if k == 1
4-
return complex(besselj(nu, x), bessely(nu, x))
189+
return complex(Jn, Yn)
5190
elseif k == 2
6-
return complex(besselj(nu, x), -bessely(nu, x))
191+
return complex(Jn, -Yn)
7192
else
8193
throw(ArgumentError("k must be 1 or 2"))
9194
end
10195
end
11196

12-
hankelh1(nu, z) = besselh(nu, 1, z)
13-
hankelh2(nu, z) = besselh(nu, 2, z)
14-
=#
197+
"""
198+
hankelh1(nu, x)
199+
Bessel function of the third kind of order `nu`, ``H^{(1)}_\\nu(x)``.
200+
"""
201+
hankelh1(nu, x) = besselh(nu, 1, x)
202+
203+
"""
204+
hankelh2(nu, x)
205+
Bessel function of the third kind of order `nu`, ``H^{(2)}_\\nu(x)``.
206+
"""
207+
hankelh2(nu, x) = besselh(nu, 2, x)

0 commit comments

Comments
 (0)