Skip to content

Commit 72064db

Browse files
augustt198stevengj
andauthored
Add exponentially scaled exponential integral (expintx) (#270)
* Add exponentially scaled exponential integral (expintx) * More expintx changes * Doc * Add back fastabs * inv, reduntant check * fix change clobbered in merge * oh Co-authored-by: Steven G. Johnson <[email protected]>
1 parent 5d23882 commit 72064db

File tree

5 files changed

+114
-69
lines changed

5 files changed

+114
-69
lines changed

docs/src/functions_list.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ SpecialFunctions.erfinv
1616
SpecialFunctions.erfcinv
1717
SpecialFunctions.expint
1818
SpecialFunctions.expinti
19+
SpecialFunctions.expintx
1920
SpecialFunctions.sinint
2021
SpecialFunctions.cosint
2122
SpecialFunctions.digamma

docs/src/functions_overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Here the *Special Functions* are listed according to the structure of [NIST Digi
2929
|:-------- |:----------- |
3030
| [`expint(ν, z)`](@ref SpecialFunctions.expint) | [exponential integral](https://en.wikipedia.org/wiki/Exponential_integral) ``\operatorname{E}_\nu(z)`` |
3131
| [`expinti(x)`](@ref SpecialFunctions.expinti) | [exponential integral](https://en.wikipedia.org/wiki/Exponential_integral) ``\operatorname{Ei}(z)`` |
32+
| [`expintx(x)`](@ref SpecialFunctions.expintx) | [exponential integral](https://en.wikipedia.org/wiki/Exponential_integral) ``exp(z) \operatorname{E}_\nu(z)`` |
3233
| [`sinint(x)`](@ref SpecialFunctions.sinint) | [sine integral](https://en.wikipedia.org/wiki/Trigonometric_integral#Sine_integral) ``\operatorname{Si}(x)`` |
3334
| [`cosint(x)`](@ref SpecialFunctions.cosint) | [cosine integral](https://en.wikipedia.org/wiki/Trigonometric_integral#Cosine_integral) ``\operatorname{Ci}(x)`` |
3435

src/SpecialFunctions.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export
6060
zeta,
6161
expint,
6262
expinti,
63+
expintx,
6364
sinint,
6465
cosint,
6566
lbinomial

src/expint.jl

Lines changed: 97 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ macro E₁_cf64(x, n::Integer)
4848
append!(num_expr.args, Float64.(p.coeffs))
4949
den_expr = :(@evalpoly $xesc)
5050
append!(den_expr.args, Float64.(q.coeffs))
51-
:( exp(-$xesc) * $num_expr / $den_expr )
51+
:( $num_expr / $den_expr )
5252
end
5353

5454
function E₁_taylor_coefficients(::Type{T}, n::Integer) where {T<:Number}
@@ -75,75 +75,88 @@ macro E₁_taylor64(z, n::Integer)
7575
end
7676

7777
# adapted from Steven G Johnson's initial implementation: issue #19
78-
function expint_opt(x::Float64)
78+
function expint_opt(x::Float64, ::Val{expscaled}=Val{false}()) where {expscaled}
7979
x < 0 && throw(DomainError(x, "negative argument, convert to complex first"))
8080
x == 0 && return Inf
81+
# E_1(inf) * exp(inf) is indeterminate
82+
x == Inf && return expscaled ? NaN : zero(x)
8183
if x > 2.15
84+
mult = expscaled ? 1 : exp(-x)
8285
# minimax rational approximations to E_1(x)/exp(-x):
83-
x < 4.0 && return exp(-x) *
86+
x < 4.0 && return mult *
8487
@evalpoly(x, 3.600530862438501481559423277418128014798, 28.73031134165011013783185685393062481126, 46.04314409968065653003548224846863877481, 21.47189493062368074985000918414086604187, 2.719957622921613321844755385973197500235, 1.508750885580864752293599048121678438568e-6) /
8588
@evalpoly(x, 1.0, 18.06743589038646654075831055159865459831, 61.19456872238615922515377354442679999566, 64.81772518730116701207299231777089576859, 24.19034591054828198408354214931112970741, 2.720026796991567940556850921390829046015)
86-
x < 10.0 && return exp(-x) *
89+
x < 10.0 && return mult *
8790
@evalpoly(x, 3.149019890512432117647119992448352099575, 14.77395058090815888166486507990452627136, 14.78214309058953358717796744960600201013, 4.559401130686434886620102186841739864936, 0.4027858394909585103775445204576054721422, 2.302781920509468929446800686773538387432e-9) /
8891
@evalpoly(x, 1.0, 11.65960373479520446458792926669115987821, 26.20023773503894444535165299118172674849, 18.93899893550582921168134979000319186841, 4.962178168140565906794561070524079194193, 0.4027860481050182948737116109665534358805)
89-
x < 20.0 && return exp(-x) *
92+
x < 20.0 && return mult *
9093
@evalpoly(x, 2.564801308922428705318296668924369454617, 5.482252510134574167659359513298970499768, 2.379528224853089764405551768869103662657, 0.2523431276282591480166881146593592650031, 1.444719769329975045925053905197199934930e-9, -8.977332061106791470409502623202677045468e-12) /
9194
@evalpoly(x, 1.0, 6.421214405318108272004472721910023284626, 7.609584052290707052877352911548283916247, 2.631866613851763364839413823973711355890, 0.2523432345539146248733539983749722854603)
92-
x < 200.0 && return @E₁_cf64(x, 8) # fallback continued fraction
93-
return x < 740.0 ? @E₁_cf64(x, 4) : 0.0 # underflow
95+
x < 200.0 && return mult * @E₁_cf64(x, 8) # fallback continued fraction
96+
return mult * @E₁_cf64(x, 4)
9497
else
9598
# crossover point to taylor should be tuned more
96-
return x 0.6 ? (x 0.053 ? (x 4.4e-3 ? @E₁_taylor64(x,4) :
99+
mult = expscaled ? exp(x) : 1
100+
return mult * (x 0.6 ? (x 0.053 ? (x 4.4e-3 ? @E₁_taylor64(x,4) :
97101
@E₁_taylor64(x,8)) :
98102
@E₁_taylor64(x,15)) :
99-
@E₁_taylor64(x,37)
103+
@E₁_taylor64(x,37))
100104
end
101105
end
102106

103-
function expint_opt(z::Complex{Float64})
107+
function expint_opt(z::Complex{Float64}, ::Val{expscaled}=Val{false}()) where {expscaled}
104108
= real(z)^2
105109
= imag(z)^2
106110
if+ 0.233* 7.84 # use cf expansion, ≤ 30 terms
107-
if (x² 546121) & (real(z) > 0) # underflow
108-
return zero(z)
109-
elseif+ 0.401* 58.0 # ≤ 15 terms
111+
mult = expscaled ? 1 : exp(-z)
112+
#if (x² ≥ 546121) & (real(z) > 0) # underflow
113+
# return zero(z)
114+
if+ 0.401* 58.0 # ≤ 15 terms
110115
if+ 0.649* 540.0 # ≤ 8 terms
111-
+ 4e4 && return @E₁_cf64(z, 4)
112-
return @E₁_cf64(z, 8)
116+
+ 4e4 && return mult * @E₁_cf64(z, 4)
117+
return mult * @E₁_cf64(z, 8)
113118
end
114-
return @E₁_cf64(z, 15)
119+
return mult * @E₁_cf64(z, 15)
115120
end
116-
return @E₁_cf64(z, 30)
121+
return mult * @E₁_cf64(z, 30)
117122
else # use Taylor expansion, ≤ 37 terms
118123
=+
119-
return 0.36 ? (r² 2.8e-3 ? (r² 2e-7 ? @E₁_taylor64(z,4) :
124+
mult = expscaled ? exp(z) : 1
125+
return mult * (r² 0.36 ? (r² 2.8e-3 ? (r² 2e-7 ? @E₁_taylor64(z,4) :
120126
@E₁_taylor64(z,8)) :
121127
@E₁_taylor64(z,15)) :
122-
@E₁_taylor64(z,37)
128+
@E₁_taylor64(z,37))
123129
end
124130
end
125131

126-
function expint(z::Complex{Float64})
132+
function _expint(z::Complex{Float64}, ::Val{expscaled}=Val{false}()) where {expscaled}
127133
if real(z) < 0
128-
return expint(1, z)
134+
return _expint(1, z, 1000, Val{expscaled}())
129135
else
130-
return expint_opt(z)
136+
return expint_opt(z, Val{expscaled}())
131137
end
132138
end
139+
expint(z::Complex{Float64}) = _expint(z)
140+
expintx(z::Complex{Float64}) = _expint(z, Val{true}())
133141
expint(x::Float64) = expint_opt(x)
142+
expintx(x::Float64) = expint_opt(x, Val{true}())
134143

135144
function expint(x::BigFloat)
136145
iszero(x) && return Inf
137146
signbit(x) && throw(DomainError(x, "negative argument, convert to complex first"))
138147
return -expinti(-x)
139148
end
140149

141-
expint(z::Union{T,Complex{T},Rational{T},Complex{Rational{T}}}) where {T<:Integer} = expint(float(z))
142150
expint(x::Number) = expint(1, x)
143-
expint(z::Float32) = Float32(expint(Float64(z)))
144-
expint(z::ComplexF32) = ComplexF32(expint(ComplexF64(z)))
145-
expint(z::Float16) = Float16(expint(Float64(z)))
146-
expint(z::ComplexF16) = ComplexF16(expint(ComplexF64(z)))
151+
expint(z::Union{T,Complex{T},Rational{T},Complex{Rational{T}}}) where {T<:Integer} = expint(float(z))
152+
for f in (:expint, :expintx)
153+
for T in (Float16, Float32)
154+
@eval $f(x::$T) = $T($f(Float64(x)))
155+
end
156+
for CT in (ComplexF16, ComplexF32)
157+
@eval $f(x::$CT) = $CT($f(ComplexF64(x)))
158+
end
159+
end
147160

148161
# Continued fraction for En(ν, z) that doesn't use a term with
149162
# the gamma function: https://functions.wolfram.com/GammaBetaErf/ExpIntegralE/10/0001/
@@ -186,17 +199,7 @@ function En_cf_nogamma(ν::Number, z::Number, n::Int=1000)
186199
end
187200
end
188201

189-
exppart = exp(-z)
190-
if isinf(exppart)
191-
if A isa Real
192-
return sign(A)*sign(B)*exp(-z + log(abs(A)) - log(abs(B))), iters
193-
else
194-
exp(-z + log(A) - log(B)), iters
195-
end
196-
else
197-
cfpart = A/B
198-
return cfpart * exppart, iters
199-
end
202+
return A/B, iters
200203
end
201204

202205
# Calculate Γ(1 - ν) * z^(ν-1) safely
@@ -243,14 +246,7 @@ function En_cf_gamma(ν::Number, z::Number, n::Int=1000)
243246
end
244247

245248
gammapart = En_safe_gamma_term(ν, z)
246-
cfpart = exp(-z)
247-
if isinf(cfpart)
248-
factor = sign(real(cfpart)) + sign(imag(cfpart))*im
249-
cfpart = exp(-z + log(B) - log(A))
250-
else
251-
cfpart *= B/A
252-
end
253-
return gammapart, -cfpart, iters
249+
return gammapart, -B/A, iters
254250
end
255251

256252
# picks between continued fraction representations in
@@ -262,10 +258,10 @@ function En_cf(ν::Number, z::Number, niter::Int=1000)
262258
gammaabs, cfabs = abs(gammapart), abs(cfpart)
263259
if gammaabs != Inf && gammaabs > 1.0 && gammaabs > cfabs
264260
# significant gamma part, use this
265-
return gammapart + cfpart, iters, true
261+
return gammapart, cfpart, iters, true
266262
end
267263
end
268-
return En_cf_nogamma(ν, z, niter)..., false
264+
return zero(z), En_cf_nogamma(ν, z, niter)..., false
269265
end
270266

271267
# Compute expint(ν, z₀+Δ) given start = expint(ν, z₀), as described by [Amos 1980].
@@ -360,16 +356,16 @@ function En_imagbranchcut(ν::Number, z::Number)
360356
return -2 * lgammasign * e1 * π * e2 * exp((ν-1)*log(complex(a)) - lgamma) * im
361357
end
362358

363-
"""
364-
expint(z)
365-
expint(ν, z)
366-
367-
Computes the exponential integral ``\\operatorname{E}_\\nu(z) = \\int_0^\\infty \\frac{e^{-zt}}{t^\\nu} dt``.
368-
If ``\\nu`` is not specified, ``\\nu=1`` is used. Arbitrary complex ``\\nu`` and ``z`` are supported.
359+
function En_safeexpmult(z, a)
360+
zexp = exp(z)
361+
if isinf(zexp) || iszero(zexp)
362+
return a isa Real ? sign(a) * exp(z + log(abs(a))) : exp(z + log(a))
363+
else
364+
return zexp*a
365+
end
366+
end
369367

370-
External links: [DLMF](https://dlmf.nist.gov/8.19), [Wikipedia](https://en.wikipedia.org/wiki/Exponential_integral)
371-
"""
372-
function expint::Number, z::Number, niter::Int=1000)
368+
function _expint::Number, z::Number, niter::Int=1000, ::Val{expscaled}=Val{false}()) where {expscaled}
373369
if abs(ν) > 50 && !(isreal(ν) && real(ν) > 0)
374370
throw(ArgumentError("Unsupported order |ν| > 50 off the positive real axis"))
375371
end
@@ -389,23 +385,32 @@ function expint(ν::Number, z::Number, niter::Int=1000)
389385
end
390386

391387
if ν == 0
392-
return exp(-z) / z
388+
return expscaled ? inv(z) : exp(-z)/z
393389
elseif ν == 1 && real(z) > 0 && z isa Union{Float64, Complex{Float64}}
394-
return expint_opt(z)
390+
return expint_opt(z, Val{expscaled}())
395391
end
396392
# asymptotic test for underflow when Re z → ∞
397393
# https://functions.wolfram.com/GammaBetaErf/ExpIntegralE/06/02/0003/
398-
if real(z) > -log(nextfloat(zero(real(z))))+1 # exp(-z) is zero
394+
if !expscaled && real(z) > -log(nextfloat(zero(real(z))))+1 # exp(-z) is zero
399395
return zero(z)
400396
end
401397

402398
if abs2(z) < 9
403399
# use Taylor series about the origin for small z
404-
return En_expand_origin(ν, z)
400+
mult = expscaled ? exp(z) : 1
401+
return mult * En_expand_origin(ν, z)
405402
end
406403

407404
if z isa Real || real(z) > 0
408-
return first(real(z) > 0 ? En_cf(ν, z, niter) : En_cf_nogamma(ν, z, niter))
405+
gmult = expscaled ? exp(z) : 1
406+
if real(z) > 0
407+
g, cf, _ = En_cf(ν, z, niter)
408+
else
409+
g, cf, _ = zero(z), En_cf_nogamma(ν, z, niter)...
410+
end
411+
g != 0 && (g *= gmult)
412+
cf = expscaled ? cf : En_safeexpmult(-z, cf)
413+
return g + cf
409414
else
410415
# Procedure for z near the negative real axis based on
411416
# (without looking at the accompanying source code):
@@ -421,7 +426,8 @@ function expint(ν::Number, z::Number, niter::Int=1000)
421426
# start with small imaginary part if exactly on negative real axis
422427
imstart = (imz == 0) ? abs(z)*sqrt(eps(typeof(real(z)))) : imz
423428
z₀ = rez + imstart*im
424-
E_start, i, _ = En_cf(ν, z₀, quick_niter)
429+
g_start, cf_start, i, _ = En_cf(ν, z₀, quick_niter)
430+
E_start = g_start + En_safeexpmult(-z₀, cf_start)
425431
isnan(E_start) && return E_start
426432
if imz > 0 && i < nmax
427433
# didn't need to take any steps
@@ -431,7 +437,8 @@ function expint(ν::Number, z::Number, niter::Int=1000)
431437
# double imaginary part until in region with fast convergence
432438
imstart *= 2
433439
z₀ = rez + imstart*im
434-
E_start, i, _ = En_cf(ν, z₀, quick_niter)
440+
g_start, cf_start, i, _ = En_cf(ν, z₀, quick_niter)
441+
E_start = g_start + En_safeexpmult(-z₀, cf_start)
435442
end
436443

437444
# nsteps chosen so |Δ| ≤ 0.5
@@ -453,17 +460,38 @@ function expint(ν::Number, z::Number, niter::Int=1000)
453460
sign = bit ? 1 : -1
454461
if isreal(ν)
455462
# can separate real/im in case of real ν
456-
return real(En) - sign * imag(bc)/2 * im
463+
En = real(En) - sign * imag(bc)/2 * im
457464
else
458-
return bit ? En : En + bc
465+
En = bit ? En : En + bc
459466
end
460-
else
461-
return En
462467
end
468+
return expscaled ? exp(z) * En : En
463469
end
464-
throw("unreachable")
465470
end
466471

472+
"""
473+
expint(z)
474+
expint(ν, z)
475+
476+
Computes the exponential integral ``\\operatorname{E}_\\nu(z) = \\int_0^\\infty \\frac{e^{-zt}}{t^\\nu} dt``.
477+
If ``\\nu`` is not specified, ``\\nu=1`` is used. Arbitrary complex ``\\nu`` and ``z`` are supported.
478+
479+
External links: [DLMF](https://dlmf.nist.gov/8.19), [Wikipedia](https://en.wikipedia.org/wiki/Exponential_integral)
480+
"""
481+
expint::Number, z::Number, niter::Int=1000) = _expint(ν, z, niter, Val{false}())
482+
483+
484+
"""
485+
expintx(z)
486+
expintx(ν, z)
487+
488+
Computes the scaled exponential integral ``\\exp(z) \\operatorname{E}_\\nu(z) = \\exp(z) \\int_0^\\infty \\frac{e^{-zt}}{t^\\nu} dt``.
489+
If ``\\nu`` is not specified, ``\\nu=1`` is used. Arbitrary complex ``\\nu`` and ``z`` are supported.
490+
491+
See also: [`expint(ν, z)`](@ref SpecialFunctions.expint)
492+
"""
493+
expintx::Number, z::Number, niter::Int=1000) = _expint(ν, z, niter, Val{true}())
494+
467495
##############################################################################
468496
# expinti function Ei
469497

test/expint.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ using Base.MathConstants
8484
@test expint(-4.3+2im, -10+0.0im) -1473.52621376586428327992147406861600035297263354271049988247182 - 210.712752794950368400059514411922967744944073260391136148642008im
8585
@test expint(-4.3+2im, -10-0.0im) -1473.47816311715810169554659685200228583538709701122499753349598 - 210.761182017526309473296237005277228612276963738826032570499367im
8686

87+
@test expint(2.000001, 4.3) 0.0022470372637239675640390482141893761501016099509821867746
88+
@test expint(1.999999, 4.3) 0.0022470379319936678143503120160911823783417969321242814713
89+
8790
@test isnan(expint(NaN))
8891
@test isnan(expint(NaN+NaN*im))
8992
@test isnan(expint(NaN, 1.0))
@@ -124,6 +127,17 @@ using Base.MathConstants
124127
@test expint(+4, big"5.7") big"0.00035851619983733810063957822090566023258844652925986297947736355731994111784745604432" rtol=1e-74
125128
end
126129

130+
131+
@testset "expintx" begin
132+
@test expintx(1e-10) 22.448635267383787506197038031047821505309344119883144316155006107
133+
@test expintx(1e-100) 229.68129363450303554119263337835401832906798952693737400452702286
134+
@test expintx(1e10) 9.999999999000000000199999999940000000023999999988000000007e-11
135+
@test_broken expintx(1e10, 1e10im) 4.99999999999999999995000000001499999999700000000000000000e-11 - 4.99999999950000000004999999999999999999700000000149999999e-11im
136+
@test expintx(2, 1e10) 9.999999998000000000599999999760000000119999999928000000050e-11
137+
@test expintx(5+5im, 1e10+1e10im) 4.99999999750000000124999999940000000023624999998587499986e-11 - 4.99999999750000000149999999897500000075624999942337500043e-11im
138+
@test expintx(2, 1e5+1e5im) 4.999999998500059998500000003149748011339999937637483913514e-6 - 4.999900001499999998500089996850000011338866062369999513582e-6im
139+
end
140+
127141
# issue #272: more cases with real results
128142
@test expint(-2, 2.2) 0.11696351427428933590118175681379776 rtol=1e-14
129143
@test expint(-2, -2.2) -2.0680909972407264331884989 rtol=1e-14

0 commit comments

Comments
 (0)