Skip to content
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

Polylogarithm and assorted functions #30

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

mroughan
Copy link

@mroughan mroughan commented Jun 1, 2017

I added function to calculate

  • polylogarithm
  • bernoulli polynomials (and numbers)
  • harmonic numbers

The latter two added because they are needed either in calculations, or in tests, and they are pretty minor. The polylogarithm function is closely related to the Hurwitz zeta, but getting a decent implementation that works over the whole complex plane requires a few steps. The implementations could still do with some optimisation, and I worry about accuracy of the polylog for large imaginary s, as the computations for the number of terms in series were based on Crandall who only presents his algorithm for real s.

There are some tests also added, but I didn't try to combine them into the runall.jl file. I thought that they could be be merged into this if/when this brach gets pulled in.

I seem to have longer help sections than are typical in Julia, but I thought it useful to add a little more detail. Sorry if I have missed out on some other common Julia coding conventions here. I am rather new at writing Julia, and this is my first pull request. It seemed like a useful little piece of code to add to learn my way in, and its useful for calculating zeta distributions, which I plan to try adding to the Distributions.jl package.

I also added an examples directory to put a few examples of them being used, and to check them against a few pictures that are out there.

Anyway, as a newbie I would welcome feedback.

src/bernoulli.jl Outdated
if n <= 34
# direct summation for reasonably small values of coefficients
k = 0:n
return sum( binomial.(n,k) .* bernoulli.(k) .* x.^(n-k) )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just write out the loop, don't use sum of a broadcast.

You should time this to see if it is actually faster than a zeta call.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely faster to use zeta. Have changed.

src/bernoulli.jl Outdated
function bernoulli(n::Int, x::Real)
if n<0
warn("n should be > 0")
throw(DomainError)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just throw an error, don't print a warning.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be throw(DomainError()).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have fixed, and added domain check for harmonics.

src/harmonic.jl Outdated
# γ = euler_mascheroni_const = 0.577215664901532860606512090082402431042 # http://oeis.org/A001620
if n <=10
# get exact values for small n
return sum( 1.0./(1:n) )
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Write out loops, don't use sum of a broadcast. This isn't Matlab.

See Why vectorized code is not as fast as it could be and here for example. You are allocating a temporary array and then looping over it with sum.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. Too much history programming matlab.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have fixed. Very interesting reading. Not sure I understand everything yet, but I get the main point.

It sounds like there are new things happening for v0.6, which could be cool.

src/li.jl Outdated
if abs(z) > 1 || ( abs(z) ≈ 1 && real(s) <= 2)
warn("Should have |z| < 1 or (|z|==1 and Re(s)>2)")
throw(DomainError)
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above about domainerrors

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. But this is something I am getting used to here. IMHO quite a few Julia functions could provide better feedback about what is causing a problem in certain circumstances. There doesn't seem to be a way to give more detail of information than DomainError?. Anyway I will change my code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, since this is a restriction on the arguments to the function, you can (and should) use ArgumentError. That is,

if abs(z) > 1 || (abs(z)  1 && real(s) <= 2)
    throw(ArgumentError("|z| < 1 or (|z| == 1 and Re(s) > 2) not satisfied"))
end

Most typed exception constructors accept strings which can provide specific messages. However, for whatever reason, DomainError does not.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually, in mathematical functions we throw domainerror...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have fixed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be more precise, I have thrown DomainErrors, but the ArgumentError sounds like a good idea? Is there a reason to avoid it?

src/li.jl Outdated
total = 0.0
L = ceil(-log10(accuracy)*log2(10)) # summation limit from Crandall, which is conservative
for n=1:L
a = z^n / n^s
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a terrible way to compute a power series. You should compute each term recursively from the previous one by multipling by f = z/n

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, this is not (z/k)^n, it is z^n / n^s. I guess you can reduce the number of exponentiations by one, with
c *= c * z * (n/(n+1))^s
so I will change it, but we don't get to avoid all exponentiations. :(

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, right.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed.

src/li.jl Outdated
# equivalent of Crandalls 1.4 for s non-integer
total = gamma(1-s) * (-μ)^(s-1)
for k=0:L
total += μ^k * zeta(s-k)/factorial(Float64(k))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More terrible ways to compute power series. You should never have to call factorial or gamma etcetera in an inner loop.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a fair call. Can definitely improve that one.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

test/li_test.jl Outdated
@test SF.Li(1, 0) ≈ 0.0
x = collect(0.0:0.1:0.9)
@test all([SF.Li.(1, x)[i] ≈ SF.Li(1, x[i]) for i=1:length(x)])
@test SF.Li(Complex(-1.0), Complex(0.3)) ≈ SF.Li(-1.0, 0.3)
Copy link
Member

@stevengj stevengj Jun 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is not sufficient for checking a special-function implementation; it only checks that about half of the digits are correct. Use , defined in runtests.jl, instead: it checks that both the real and imaginary parts have about 13 digits.

And it is not sufficient to check symmetries and simple values like this. You need to check points just on either side of the thresholds between the different approximations that you use, points close to the real or imaginary axes (e.g. 1 + 1e-15*im) etcetera. Use e.g. WolframAlpha to get accurate to test against.

Another good thing to try would be to run e.g. 10^5 random points in the complex plane against some known implementation (e.g. the one in Python's mpmath) and check the maximum relerrc (defined in runtests.jl). (We probably don't want a Pkg.test dependency on PyCall, but you should still run this test separately.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw you used ≅ but I didn't know the distinction, so thanks!

I will add additional checks.

I did pretty much exactly as suggested -- I had some code checking against mpmath. There are errors, and the grow as Im(s) increases.

However, despite its claims I lost a little faith in mpmaths accuracy for problems in this domain when it failed a couple of simple Bernoulli polynomial tests (e.g., one of the zeros wasn't)

mpmath is the only easy way I had to mass produce values. I will pull a few off alpha, but how many is enough?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you usually want to test at the boundaries between different approximations (the crossover points of your if statements), very large arguments (near overflow, e.g. 1e300), very small arguments, inf, nan, arguments with very small real or imaginary parts...

this is in addition to random sampling, which is a good fallback test to help catch any corner cases you forgot about.

When you compare to mpmath, what is the maximum relative error you see in the real or imaginary parts?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, now I see that it is defined in runtests.jl, and you use it sometimes. I take it that it is primarily to be used for testing the output of complex functions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Basically, with the default tolerance is a sanity check for finding crude bugs. If you really want to carefully check the accuracy, however, you need to look at both the real and imaginary parts separately, and with a lower tolerance, which is why I defined .

src/bernoulli.jl Outdated
"""
bernoulli(n)

created: Tue May 23 2017
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically don't include this information in docstrings, as its readily available in the Git history.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Will delete.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

src/gamma.jl Outdated
"""
hurwitz_zeta(s, z)

An alias for zeta(s, z)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No indent here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will fix.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

src/gamma.jl Outdated

An alias for zeta(s, z)
"""
hurwitz_zeta(s::ComplexOrReal{Float64}, z::ComplexOrReal{Float64}) = zeta(s, z)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this necessary?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is an issue with how Julia does docs, but in my initial reading I hadn't realised that Julia had a Hurwitz zeta function already. I know the information is there, but it wasn't obvious for a newbie (like me). If its not standard for Julia to include aliases like this let me know, but it seemed like a trivial way to make life easier for users?

I did the same with Li=polylog. I realised after writing code that the naming convention for Julia would usually give us something like polylog?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, aliases like this aren't typically provided. If it isn't obvious what something is, that means the documentation should be better. 🙂

Typical Julia conventions prefer more descriptive names for functions, and function names are typically all lowercase. Since polylog is the more descriptive name (and jives with polygamma), I think we should just stick to that one.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed Li to polylog.

@ararslan
Copy link
Member

ararslan commented Jun 1, 2017

Hey @mroughan, thanks for the contribution! I'd prefer not to include all the examples and PDFs in this repository. The visualizations would be neat as a notebook, perhaps posted in a blog somewhere.

@mroughan
Copy link
Author

mroughan commented Jun 2, 2017

OK, I have addressed most of the comments. I will be adding additonal test cases, but at the moment its fairly laborious, so it will happen, but a little more slowly. Early additional tests show that near positive integer s (particularly s=1.0), we have some numerical issues in the series expansion around z=1. These are known numerical problems, and it looks as if Wood has a fix in equation (9.4), but so many of the symbols are garbled in his text, that it will be a trick to add this in. This will have to wait until next week.

Thanks for all the suggestions. I have learned a good deal today.

@stevengj
Copy link
Member

stevengj commented Jun 2, 2017

Out of curiosity, how does it compare (in performance and accuracy) to a naive definition using the relationship to the Hurwitz zeta?

function Li(s,z)
    twopi = 2π
    x = im * (log(complex(-z)) / 2π)
    ip = im^(1-s)
    return gamma(1-s)/twopi^(1-s) * (ip * zeta(1-s, 0.5-x) + conj(ip) * zeta(1-s, 0.5+x))
end

(My impression is that this formula suffers from cancellation problems for some regions of the parameter space where you'd want to switch to an alternative formula, but is otherwise okay.)

For performance testing, I would suggest using the BenchmarkTools package, e.g.

using BenchmarkTools
@btime Li(3.2, 4.7)

@mroughan
Copy link
Author

mroughan commented Jun 5, 2017

Hi, re direct computation from the Hurwitz zeta, I did try this out initially, and have just redone a few more tests. For reasons I don't completely understand it doesn't do that well. Errors are at best similar, but sometimes orders of magnitude worst on (only slightly) challenging parts of the parameter space. Problems seem to manifest for complex s. I have added a function for this anyway, and will do a bit more testing to see if I can understand.

@stevengj
Copy link
Member

stevengj commented Jun 5, 2017

@mroughan, how does the simplistic Li compare performance-wise? (Presumably, a specialized polylog routine should be faster; if it isn't, something may be wrong.)

src/polylog.jl Outdated
@@ -126,6 +126,16 @@ function Dbeta(s::Number)
β = 4.0^(-s) * ( zeta(s,0.25) - zeta(s,0.75) )
end

function polylog_zeta(s::Number, z::Number, accuracy=1.0e-18)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the accuracy argument isn't being used

@albi3ro
Copy link

albi3ro commented Jan 29, 2018

I'm needing to use polylog for my research code, which is in Julia. Otherwise I'll have to interface what I've done with Mathematica. Just need Li_2(-x) for x between 0 and 1, but if I could just implement this in Julia, I'd be wonderfully happy :)

@BoundaryValueProblems
Copy link

Is someone working on this pull request? It would be quite useful if SpecialFunctions.jl has polylogarithms and related functions!

@mroughan
Copy link
Author

mroughan commented Feb 1, 2019 via email

@bghagar
Copy link

bghagar commented Apr 19, 2019

I'm looking at calculating Fermi Dirac integrals using polylogarithms, it would be great if this was implemented!

@dilaraabdel
Copy link

I'm looking at calculating Fermi Dirac integrals using polylogarithms, it would be great if this was implemented!

I am doing the same and need it for my thesis. @mroughan did you go back on working it?

@orialb
Copy link

orialb commented Aug 4, 2020

I also needed polylog function today (ended up using Mathematica because I am on a tight deadline right now) so +1 for this. Does anyone know how much work is needed to get this working (seems the code is quite old by now) or what were the challenges that blocked it before?

@mroughan
Copy link
Author

OK, it's been so long since I started this, rebooting wasn't trivial. But a few people have wanted it, so I decided tgo finish it off. In the end, I rewrote a lot of it, and rather than keep pushing what is still somewhat experimental code into a production package, I have split it off into its own separate place:
https://github.com/mroughan/Polylogarithms.jl

It isn't registered yet, and at the moment it only seems to work against Julia 1.4 so I have a few more things to do, but at least there is something up for people to try out.

It includes a much larger set of tests than I had before using Mathematica results as a benchmark.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants