Skip to content

Commit 1020511

Browse files
committed
Add logwrightbessel
Needed to avoid overflow when computing Tweedie PDF in some cases. Also sync with latest code from SciPy.
1 parent 61ad698 commit 1020511

File tree

4 files changed

+196
-76
lines changed

4 files changed

+196
-76
lines changed

docs/src/functions_list.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ besselk
137137
besselkx
138138
jinc
139139
wrightbessel
140+
logwrightbessel
140141
```
141142

142143
## Elliptic Integrals

src/SpecialFunctions.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export
4444
bessely1,
4545
besselyx,
4646
wrightbessel,
47+
logwrightbessel,
4748
jinc,
4849
dawson,
4950
ellipk,

src/wrightbessel.jl

Lines changed: 94 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,12 @@ const exp_inf = 709.7827128933841
4848
# Compute reciprocal gamma via loggamma
4949
@inline rgamma(y::Real) = exp(-loggamma(y))
5050

51-
# Compute exp(x) / Γ(y) safely via loggamma to avoid overflow
52-
@inline _exp_rgamma(x::Real, y::Real) = exp(x - loggamma(y))
53-
5451

5552
# 1. Taylor series expansion in x=0, for x <= 1
5653
# Φ(a, b, x) = sum_k x^k / k! / Γ(a*k+b)
5754
# Note that every term, and therefore also Φ(a, b, x), is monotone
5855
# decreasing with increasing a or b.
59-
function _wb_series(a::Float64, b::Float64, x::Float64, nstart::Int, nstop::Int)
56+
function _wb_series(a::Float64, b::Float64, x::Float64, nstart::Int, nstop::Int, logret::Bool)
6057
xk_k = x^nstart * rgamma(nstart + 1) # x^k/k!
6158
res = xk_k * rgamma(nstart * a + b)
6259
# term k=nstart+1 , +2 +3, ...
@@ -71,27 +68,37 @@ function _wb_series(a::Float64, b::Float64, x::Float64, nstart::Int, nstop::Int)
7168
res += xk_k * rgamma(a * k + b)
7269
end
7370
end
74-
return res
71+
return logret ? log(res) : res
7572
end
7673

7774

7875
# 2. Taylor series expansion in x=0, for large a.
7976
# Φ(a, b, x) = sum_k x^k / k! / Γ(a*k+b)
8077
# Use Stirling formula to find k=k_max, the maximum term.
8178
# Then use n terms of Taylor series around k_max.
82-
function _wb_large_a(a::Float64, b::Float64, x::Float64, n::Int)
79+
function _wb_large_a(a::Float64, b::Float64, x::Float64, n::Int, logret::Bool)
8380
k_max = floor(Int, (a^(-a) * x)^(1 / (1 + a)))
8481
n_start = k_max - (n ÷ 2)
8582
if n_start < 0
8683
n_start = 0
8784
end
8885

89-
res = 0.0
9086
lnx = log(x)
87+
88+
# For numerical stability, we factor out the maximum term exp(..) with k=k_max
89+
# but only if it is larger than 0.
90+
max_exponent = max(0.0, k_max * lnx - loggamma(k_max + 1.0) - loggamma(a * k_max + b))
91+
res = 0.0
92+
9193
for k in n_start:(n_start + n - 1)
92-
res += exp(k * lnx - loggamma(k + 1.0) - loggamma(a * k + b))
94+
res += exp(k * lnx - loggamma(k + 1.0) - loggamma(a * k + b) - max_exponent)
95+
end
96+
97+
if logret
98+
return max_exponent + log(res)
99+
else
100+
return exp(max_exponent) * res
93101
end
94-
return res
95102
end
96103

97104

@@ -111,7 +118,7 @@ end
111118
# For small b, i.e. b <= 1e-3, cancellation of poles of Ψ(b)/Γ(b)
112119
# and polygamma needs to be carried out => series expansion in a=0 to order 5
113120
# and in b=0 to order 4.
114-
function _wb_small_a(a::Float64, b::Float64, x::Float64, order::Int)
121+
function _wb_small_a(a::Float64, b::Float64, x::Float64, order::Int, logret::Bool)
115122
# A: coefficients of a^k (1, -x * Ψ(b), ...)
116123
# B: powers of b^k/k! or terms in polygamma functions
117124
# C: coefficients of a^k1 * b^k2
@@ -144,7 +151,9 @@ function _wb_small_a(a::Float64, b::Float64, x::Float64, order::Int)
144151
X5 / 24.0 * (C4 + C5 * b),
145152
X6 / 120.0 * (C5)
146153
)
147-
res = exp(x) * evalpoly(a, A)
154+
155+
res = evalpoly(a, A)
156+
return logret ? x + log(res) : exp(x) * res
148157
else
149158
# Φ(a, b, x) = exp(x)/Γ(b) * sum(A[i] * X[i] * B[i], i=0..5)
150159
# A[n] = a^n/n!
@@ -198,10 +207,13 @@ function _wb_small_a(a::Float64, b::Float64, x::Float64, order::Int)
198207
res = evalpoly(a, (A1, A2, A3, A4, A5, A6)[1:(order+1)])
199208
end
200209

201-
# res *= exp(x) * rgamma(b)
202-
res *= _exp_rgamma(x, b)
210+
if logret
211+
return log(res) + x - loggamma(b)
212+
else
213+
# Compute exp(x) / Γ(b) via loggamma to avoid overflow
214+
return res * exp(x - loggamma(b))
215+
end
203216
end
204-
return res
205217
end
206218

207219

@@ -210,7 +222,7 @@ end
210222
# Φ(a, b, x) ~ Z^(1/2-b) * exp((1+a)/a * Z) * sum_k (-1)^k * C_k / Z^k
211223
#
212224
# with Z = (a*x)^(1/(1+a)).
213-
function _wb_asymptotic(a::Float64, b::Float64, x::Float64)
225+
function _wb_asymptotic(a::Float64, b::Float64, x::Float64, logret::Bool)
214226
A = ntuple(i -> a^(i-1), 15) # powers of a
215227
B = ntuple(i -> b^(i-1), 17) # powers of b
216228
Ap1 = ntuple(i -> (1 + a)^(i-1), 9) # powers of (1+a)
@@ -540,16 +552,25 @@ function _wb_asymptotic(a::Float64, b::Float64, x::Float64)
540552
Zp /= Z
541553
res += (-1)^(k + 1) * C[k] * Zp
542554
end
543-
res *= Z^(0.5 - b) * exp(Ap1[2] / a * Z)
544-
return res
555+
if logret
556+
return log(res) + log(Z) * (0.5 - b) + Ap1[2] / a * Z
557+
else
558+
return res * Z^(0.5 - b) * exp(Ap1[2] / a * Z)
559+
end
545560
end
546561

547562

548563
# Compute integrand Kmod(eps, a, b, x, r) for Gauss-Laguerre quadrature.
549564
# K(a, b, x, r+eps) = exp(-r-eps) * Kmod(eps, a, b, x, r)
550-
@inline function _Kmod(eps::Float64, a::Float64, b::Float64, x::Float64, r::Float64)
565+
#
566+
# Kmod(eps, a, b, x, r) = exp(x * (r+eps)^(-a) * cos(pi*a)) * (r+eps)^(-b)
567+
# * sin(x * (r+eps)^(-a) * sin(pi*a) + pi * b)
568+
#
569+
# Note that we additionally factor out exp(exp_term) which helps with large
570+
# terms in the exponent of exp(...)
571+
@inline function _Kmod(exp_term::Float64, eps::Float64, a::Float64, b::Float64, x::Float64, r::Float64)
551572
x_r_a = x * (r + eps)^(-a)
552-
return exp(x_r_a * cospi(a)) * (r + eps)^(-b) * sin(x_r_a * sinpi(a) + π * b)
573+
return exp(x_r_a * cospi(a) + exp_term) * (r + eps)^(-b) * sin(x_r_a * sinpi(a) + π * b)
553574
end
554575

555576

@@ -558,9 +579,9 @@ end
558579
# * exp(eps * cos(ϕ) + x * eps^(-a) * cos(a*ϕ))
559580
# * cos(eps * sin(ϕ) - x * eps^(-a) * sin(a*ϕ)
560581
# + (1-b)*ϕ)
561-
@inline function _P(eps::Float64, a::Float64, b::Float64, x::Float64, ϕ::Float64)
582+
@inline function _P(exp_term::Float64, eps::Float64, a::Float64, b::Float64, x::Float64, ϕ::Float64)
562583
x_eps_a = x * eps^(-a)
563-
return exp(eps * cos(ϕ) + x_eps_a * cos(a * ϕ)) *
584+
return exp(eps * cos(ϕ) + x_eps_a * cos(a * ϕ) + exp_term) *
564585
cos(eps * sin(ϕ) - x_eps_a * sin(a * ϕ) + (1.0 - b) * ϕ)
565586
end
566587

@@ -584,7 +605,10 @@ end
584605
# the end of the second line should read +(1-b)*ϕ.
585606
# This integral representation introduced the free parameter eps (from the
586607
# radius of complex contour integration). We try to choose eps such that
587-
# the integrand behaves smoothly.
608+
# the integrand behaves smoothly. Note that this is quite diffrent from how
609+
# Luchko (2008) deals with eps: he is either looking for the limit eps -> 0
610+
# or he sets (silently) eps=1. But having the freedom to set eps is much more
611+
# powerful for numerical evaluation.
588612
#
589613
# As K has a leading exp(-r), we factor this out and apply Gauss-Laguerre
590614
# quadrature rule:
@@ -681,7 +705,7 @@ const w_legendre = [
681705
0.006759799195745401, 0.002908622553155141
682706
]
683707

684-
function _wb_integral(a::Float64, b::Float64, x::Float64)
708+
function _wb_integral(a::Float64, b::Float64, x::Float64, logret::Bool)
685709
# Fitted parameters for optimal choice of eps
686710
A = (0.41037, 0.30833, 6.9952, 18.382, -2.8566, 2.1122)
687711

@@ -714,19 +738,38 @@ function _wb_integral(a::Float64, b::Float64, x::Float64)
714738
eps = min(eps, 150.0)
715739
eps = max(eps, 3.0) # Note: 3 seems to be a pretty good choice in general.
716740

741+
# We factor out exp(-exp_term) from _Kmod and _P to avoid overflow of exp(..).
742+
exp_term = 0.0
743+
# From the exponent of K:
744+
r = x_laguerre[end] # largest value of x used in _Kmod
745+
x_r_a = x * (r + eps)^(-a)
746+
exp_term = max(exp_term, x_r_a * cospi(a))
747+
# From the exponent of P:
748+
x_eps_a = x * eps^(-a)
749+
# phi = 0 => cos(phi) = cos(a * phi) = 1
750+
exp_term = max(exp_term, eps + x_eps_a)
751+
# phi = pi => cos(phi) = -1
752+
exp_term = max(exp_term, -eps + x_eps_a * cospi(a))
753+
717754
res1 = 0.0
718755
res2 = 0.0
719756
for k in eachindex(x_laguerre, w_laguerre, x_legendre)
720-
res1 += w_laguerre[k] * _Kmod(eps, a, b, x, x_laguerre[k])
757+
res1 += w_laguerre[k] * _Kmod(-exp_term, eps, a, b, x, x_laguerre[k])
721758
# y = (b-a)*(x+1)/2 + a for integration from a=0 to b=π
722759
y = π * (x_legendre[k] + 1.0) / 2.0
723-
res2 += w_legendre[k] * _P(eps, a, b, x, y)
760+
res2 += w_legendre[k] * _P(-exp_term, eps, a, b, x, y)
724761
end
725762
res1 *= exp(-eps)
726763
res2 *= π / 2.0
727764
res2 *= eps^(1.0 - b)
728765

729-
return (res1 + res2) / π
766+
# Remember the factored out exp_term from _Kmod and _P
767+
if logret
768+
res = res1 + res2
769+
return res > 0 ? exp_term + log(res) - log(π) : NaN
770+
else
771+
return exp(exp_term) / π * (res1 + res2)
772+
end
730773
end
731774

732775

@@ -759,7 +802,17 @@ One of 5 different computation methods is used depending on the ranges of the ar
759802
Accuracy is generally higher than 1e-11, though for some parameter values it can
760803
be as low as 1e-8.
761804
"""
762-
function wrightbessel(a::Float64, b::Float64, x::Float64)
805+
wrightbessel(a::Float64, b::Float64, x::Float64) = _wrightbessel(a, b, x, false)
806+
807+
"""
808+
logwrightbessel(a::Float64, b::Float64, x::Float64)
809+
810+
Compute the logarithm of Wright's generalized Bessel function, i.e., `log(wrightbessel(a, b, x))`.
811+
See [`wrightbessel`](@ref) for details.
812+
"""
813+
logwrightbessel(a::Float64, b::Float64, x::Float64) = _wrightbessel(a, b, x, true)
814+
815+
function _wrightbessel(a::Float64, b::Float64, x::Float64, logret::Bool)
763816
if isnan(a) || isnan(b) || isnan(x)
764817
throw(ArgumentError("arguments cannot be NaN (got a=$a, b=$b, x=$x)"))
765818
elseif isinf(a) || isinf(b)
@@ -771,13 +824,13 @@ function wrightbessel(a::Float64, b::Float64, x::Float64)
771824
elseif a >= rgamma_zero || b >= rgamma_zero
772825
return NaN
773826
elseif x == 0.0
774-
return rgamma(b)
827+
return logret ? -loggamma(b) : rgamma(b)
775828
elseif a == 0.0
776-
# return exp(x) * rgamma(b)
777-
return _exp_rgamma(x, b)
829+
# Compute exp(x) / Γ(b) via loggamma to avoid overflow
830+
return logret ? x - loggamma(b) : exp(x - loggamma(b))
778831
elseif (a <= 1e-3 && b <= 50.0 && x <= 9.0) ||
779832
(a <= 1e-4 && b <= 70.0 && x <= 100.0) ||
780-
(a <= 1e-5 && b <= 170.0 && x < exp_inf)
833+
(a <= 1e-5 && b <= 170.0 && (x < exp_inf || (logret && x <= 1e3)))
781834
# Taylor Series expansion in a=0 to order=order => precision <= 1e-11
782835
# If beta is also small => precision <= 1e-11.
783836
# max order = 5
@@ -812,13 +865,13 @@ function wrightbessel(a::Float64, b::Float64, x::Float64)
812865
order = 5
813866
end
814867
end
815-
return _wb_small_a(a, b, x, order)
868+
return _wb_small_a(a, b, x, order, logret)
816869
elseif x <= 1.0
817870
# 18 term Taylor Series => error mostly smaller 5e-14
818-
return _wb_series(a, b, x, 0, 18)
871+
return _wb_series(a, b, x, 0, 18, logret)
819872
elseif x <= 2.0
820873
# 20 term Taylor Series => error mostly smaller 1e-12 to 1e-13
821-
return _wb_series(a, b, x, 0, 20)
874+
return _wb_series(a, b, x, 0, 20, logret)
822875
elseif a >= 5.0
823876
# Taylor series around the approximate maximum term.
824877
# Set number of terms=order.
@@ -839,18 +892,20 @@ function wrightbessel(a::Float64, b::Float64, x::Float64)
839892
order = min(floor(Int, 6 * log10(x) - 36), 100)
840893
end
841894
end
842-
return _wb_large_a(a, b, x, order)
843-
elseif (0.5 <= a) && (a <= 1.8) && (100.0 <= b) && (1e5 <= x)
844-
return NaN
895+
return _wb_large_a(a, b, x, order, logret)
845896
elseif (a * x)^(1 / (1 + a)) >= 14 + b * b / (2 * (1 + a))
846897
# Asymptotic expansion in Z = (a*x)^(1/(1+a)) up to 8th term 1/Z^8.
847898
# For 1/Z^k, the highest term in b is b^(2*k) * a0 / (2^k k! (1+a)^k).
848899
# As a0 is a common factor to all orders, this explains a bit the
849900
# domain of good convergence set above.
850901
# => precision ~ 1e-11 but can go down to ~1e-8 or 1e-7
851902
# Note: We ensured a <= 5 as this is a bad approximation for large a.
852-
return _wb_asymptotic(a, b, x)
903+
return _wb_asymptotic(a, b, x, logret)
904+
elseif (0.5 <= a) && (a <= 1.8) && (100.0 <= b) && (1e5 <= x)
905+
# This is a very hard domain. This condition is placed after wb_asymptotic.
906+
# TODO: Explore ways to cover this domain.
907+
return NaN
853908
else
854-
return _wb_integral(a, b, x)
909+
return _wb_integral(a, b, x, logret)
855910
end
856911
end

0 commit comments

Comments
 (0)