-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
complex polygamma #7033
Comments
There is a digamma for complex argument in Naval Surface Warfare library which a Julia implementation could be based on, but no trigamma or general polygamma. |
Some references on computing the polygamma function for complex arguments can be found in Lozier and Olver, although when I look at the cited references it looks like most of them are only for the gamma or digamma function of complex arguments. I'm not seeing any recent publications on this, but these continued-fraction expansions may be helpful. |
There is also the CPSIPG program. And this technical report, if you can find it (or order it through your library). |
The following code computes the import Base: digamma, Math.@horner
function digamma(z::Complex{Float64})
# Based on eq. (12), without looking at the accompanying source code, of:
# K. S. Kölbig, "Programs for computing the logarithm of the gamma function,
# and the digamma function, for complex argument," Computer Phys. Commun.
# vol. 4, pp. 221–226 (1972).
x, y = reim(z)
if x < 0 # reflection formula
ψ = -π * cot(π*z)
x = 1 - x; y = -y
z = Complex(x, y)
else
ψ = zero(z)
end
if x < 7 # shift using recurrence formula
n = 7 - ifloor(x)
for ν = 0:n-1
ψ -= inv(z + ν)
end
z = Complex(x + n, y)
end
t = inv(z)
ψ += log(z) - 0.5*t
t *= t # 1/z^2
ψ -= t * @horner(t,0.08333333333333333,-0.008333333333333333,0.003968253968253968,-0.004166666666666667,0.007575757575757576,-0.02109279609279609,0.08333333333333334)
end
digamma{T <: Integer}(z::Complex{T}) = digamma(float(z)) Note that I used a slightly different version of the reflection formula for The
|
Gotta love that Horner macro. If we're not exporting that thing, we should. |
As described in Knuth TAOCP vol. 2, sec. 4.6.4, there is actually an algorithm even better than Horner's rule for evaluating polynomials |
(I should have a general polygamma function implemented soon if nothing unexpected happens.) |
Is that the right link to the Horner alternative? |
Yes. There is a description of the algorithm near the end of the thread. I couldn't find a better description online. |
Here's a # evaluate p[1] + z * (p[2] + z * (....)) for complex z and real p.
# Instead of Horner's rule, we use the algorithm in Knuth TAOCP
# vol. 2, sec. 4.6.4, which has lower arithmetic costs. Assumes
# degree(p) > 0 and that p consists of real numeric literals.
macro chorner(z, p...)
a = p[end]
b = p[end-1]
as = {}
for i = length(p)-2:-1:1
ai = symbol(string("a", i))
push!(as, :($ai = $a))
a = :($b + r*$ai)
b = :($(p[i]) - s * $ai)
end
ai = :a0
push!(as, :($ai = $a))
Expr(:block,
:(x = real($(esc(z)))),
:(y = imag($(esc(z)))),
:(r = x + x),
:(s = x*x + y*y),
as...,
:(Complex($ai * x + $b, $ai * y)))
end |
This is probably less efficient for non-complex arguments though. Or does the fact that the imaginary part is zero allow code generation to eliminate any extra code? |
My Although a part of the speed difference could be due to the fact that I don't think you'd want to use it for real arguments, though; as written, it always produces a |
Hmm, it would be nice to be able to support |
That's a phenomenal speed up. Good code generation is pretty amazing. I wonder if there's some way to make the Horner macro work for complex or real arguments without the user needing to know. Maybe generate both versions with a conditional on the type? Type inference should typically eliminate the branch. |
Type-inference apparently does not currently eliminate the branch, unfortunately; see #7060. |
Hmm, looks like Julia's |
It is based on the SLATEC function |
…be used without penalty instead of factorial for generic code that works for integers and non-integers (e.g. for #7033)
Even for polynomial evaluation for real arguments, Knuth sec. 4.6.4 describes ways to save a few operations (or in some cases just to trade off multiplies for adds) compared to Horner for polynomials of degree 4 or higher. If we suppose that multiplication and addition have approximately the same cost (which is not a bad assumption these days), you can save 1 flop for evaluating polynomials of degree 5 or 6. Asymptotically, you can save 1/4 of the flops, but I'm not sure we care about |
As a warmup for a general import Base: Math.@horner
macro chorner(z, p...)
a = p[end]
b = p[end-1]
as = {}
for i = length(p)-2:-1:1
ai = symbol(string("a", i))
push!(as, :($ai = $a))
a = :($b + r*$ai)
b = :($(p[i]) - s * $ai)
end
ai = :a0
push!(as, :($ai = $a))
C = Expr(:block,
:(x = real($(esc(z)))),
:(y = imag($(esc(z)))),
:(r = x + x),
:(s = x*x + y*y),
as...,
:(Complex($ai * x + $b, $ai * y)))
R = Expr(:macrocall, symbol("@horner"), z, p...)
:(isa($(esc(z)), Real) ? $R : $C)
end
function trigamma2(z::Union(Float64,Complex{Float64}))
# via the derivative of the Kölbig digamma formulation
x = real(z)
if x < 0 # reflection formula
return 9.869604401089358 * csc(π*z)^2 - # π² csc²(πz)
trigamma2(1 - z)
end
ψ = zero(z)
if x < 7 # shift using recurrence formula
n = 7 - ifloor(x)
ψ += inv(z)^2
for ν = 1:n-1
ψ += inv(z + ν)^2
end
z += n
end
t = inv(z)
w = t * t # 1/z^2
ψ += t + 0.5*w
ψ += t*w * @chorner(w,0.16666666666666666,-0.03333333333333333,0.023809523809523808,-0.03333333333333333,0.07575757575757576,-0.2531135531135531,1.1666666666666667,-7.092156862745098)
end
trigamma2{T <: Integer}(z::Union(T,Complex{T})) = trigamma2(float(z))
@vectorize_1arg Number trigamma2 |
Also, it looks like our current The SciPy |
Okay, I have a working Still working on support for arguments with negative real parts, and for non-integer order. |
Hmm, it's somewhat disturbing that Mathematica and Maple don't agree on the value of |
Oops. I can see that I have divided with the scaling instead of multiplying in the code for
i.e. the same as SciPy. |
Thanks @andreasnoack, that's good to know! I'll patch it now, although hopefully shortly I'll have a complete replacement. |
Oddly, both both Maple and Mathematica are giving real (and different!) results for @alanedelman points out that there are, unfortunately, multiple extensions of the polygamma function to arbitrary complex orders Probably the best thing to do for now is to continue to restrict |
Regarding evaluation of polynomials efficiently (more efficiently than Horner's rule)... The transformed expression will in general evaluate to different values. Maybe it doesn't matter. Compile-time reorganization of code for all kinds of things is possible -- not only for Horner, but e.g. FFT where there is some known collection of zero elements. RJF |
This is a good point – one that I'm sure @stevengj is quite aware of. It's worth considering both accuracy and speed, but my impression is that these optimizations are not bad for accuracy – fewer operations tends to lead to better accuracy since there are fewer opportunities for cancelation and round off errors. The specifics matter quite a lot, of course. |
The Knuth scheme for complex polynomial evaluation is based on the Goertzel algorithm, which has O(n^2) mean error growth. This is indeed worse than Horner, which is O(n). But these asymptotic considerations aren't very relevant for n < 10. The error growth also depends on the point z being evaluated---I think the worst case is for z on the unit circle with comparable coefficients, where the abovementioned growth rates are achieved. Things should be much better in applications like the one here, where the terms have rapidly decreasing weight For large n >> 10 (evaluating polynomials of high degree), this difference is important, but then there are algorithms with even better error growth at some cost in arithmetic. The most famous case of high degree polynomial evaluation is an FFT, where the typical algorithm has O(sqrt log n) error growth |
We have complex gamma and real polygamma but not complex polygamma – or digamma or trigamma. If anyone wants to tackle this, we really ought to have these. While we're at it, it might be good to readjust the definitions so that if you provide polygamma for a type, you get digamma and trigamma "for free". Currently you have to explicitly add methods for those too for any new type.
The text was updated successfully, but these errors were encountered: