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

ENH: implement the principle branch of the logarithm of Gamma. #5931

Merged
merged 10 commits into from
Mar 28, 2016

Conversation

person142
Copy link
Member

Closes gh-5840.

@person142
Copy link
Member Author

Hm... the test failures are coming from test_basic.py and test_spherical_bessel.py. I'm seeing those failures on master too, so I don't think this code has anything to do with them...

Edit: forgot to rebuild before testing.

@pv
Copy link
Member

pv commented Mar 9, 2016

The failures are real, and they are caused by this PR.

The mechanism is a bit unclear, but I believe it's because you are including "complex.h" which introduces C99 complex numbers --- it should use numpy complex number functions instead, see _complexstuff.pxd. (Whether the failing spherical bessel functions are completely stably implemented if they fail with C99 complex is then a different question.)

A different question is that gammaln(x+0j) does compute some imaginary part --- as it appears to differ by integer multiples, is there a need to reimplement the whole computation from scratch?

@pv pv added needs-work Items that are pending response from the author scipy.special labels Mar 9, 2016
Also cleaned up the code and improved the documentation.
@person142
Copy link
Member Author

Yes, including "complex.h" was the culprit; thanks for the hint.

It's true that one could compute loggamma from gammaln. This implementation of loggamma has a much larger range than gammaln, however. For example, gammaln(-250 + 250j) = (-inf+nan*j), whereas loggamma computes the correct result. (At least the result agrees with mpmath.) In fact, loggamma is correct even at numbers like -1e20 + 1e20j, far beyond where gammaln has conked out. So it might make more sense to compute gammaln(z) from loggamma.

@codecov-io
Copy link

@@            master   #5931   diff @@
======================================
  Files          238     238       
  Stmts        43744   43805    +61
  Branches      8213    8213       
  Methods          0       0       
======================================
+ Hit          34168   34230    +62
- Partial       2602    2604     +2
+ Missed        6974    6971     -3

Review entire Coverage Diff as of e49a067

Powered by Codecov. Updated on successful CI builds.

@pv
Copy link
Member

pv commented Mar 9, 2016

Ok good. Then, perhaps we can just replace the complex version of gammaln (clngamma_wrap: D->D in generate_ufuncs.py) by loggamma. I think the intent of the code was to also implement the primary branch, but there's some implementation mistake for Re z < 0 which gets the branch cuts wrong.

@person142
Copy link
Member Author

Right. Though maybe I'll make a proposal, and if you don't like it I'll merge things into gammaln.

Currently gammaln computes log(|Gamma(x)|) for real inputs and log(Gamma(z)) for complex inputs (or, as you pointed out, maybe a buggy implementation of the principle branch). That behavior is a little strange. There is a precedent of functions doing slightly different things for real vs. complex numbers, but it's things like np.sqrt(-1) returning nan versus np.sqrt(-1 + 0j) returning 1j, i.e. it's only a change of definition of the domain of the function. This is not what gammaln does--it actually computes a different function for real inputs.

This behavior was only documented in 0.18 (before that the documentation said that gammaln always computed log(|Gamma(z)|)), so if there were ever a time to fix the strange behavior now is it. The best fix I can see is to have two functions: loggamma, which computes the principle branch, and logabsgamma (maybe there's a better name), which computes log(|Gamma(x)|) and only takes real inputs. The original gammaln would have to be kept around for back-compat, but we could add a message to its documentation which discourages its use and redirects the user to loggamma or logabsgamma (along with a thoughtful discussion of when you would want to use each).

@pv
Copy link
Member

pv commented Mar 10, 2016

As a data point, in a similar situation with the legendre P function lpmn where the real version computed something quite different from complex, we just dropped the complex implementation and added a new function, cf. 44e7c61. So, in principle, we could also deprecate complex argument in gammaln which leaves loggamma as the canonical choice.

Now gammaln always computes the log of the absolute value of
gamma, so that one should use gammaln and gammasgn for working with
real values an loggamma for working with complex values.
@person142
Copy link
Member Author

Updated in accordance with @pv's comment. I stripped out the complex part of gammln; since the complex valued behavior isn't documented in any release I wasn't sure if adding a deprecation warning was necessary.

@pv
Copy link
Member

pv commented Mar 11, 2016

Perhaps gammaln should raise a deprecationwarning for complex inputs, rather than scrapping it right away. iow, "gammaln -> _gammaln" and add a Python wrapper function def gammaln in basic.py that checks if np.iscomplexobj and raises a warning if so. Then after a few releases we could remove the complex version.

Also changed several incorrect uses of "principle" to "principal".
@person142
Copy link
Member Author

Updated.

Added deprecation warning to gammaln docstring and also fixed
erronious claim that ``loggamma`` is defined by log(gamma(z)) for
Re(z) > 0 and extended to the complex plain by continuation to the
correct claim that it is defined by log(gamma(x)) for x > 0 and
extended to the complex plain by continuation.
@person142
Copy link
Member Author

Updated again to explicitly mention the deprecation in the documentation of gammaln. Also fixed a bug I introduced in the gammaln wrapper and a mistake in the definition of loggamma in the documentation.

Also, @pv is correct that the current behavior of gammaln for complex arguments is a buggy implementation of loggamma and not an implementation of log(gamma(z)). (The source uses the Stirling series for loggamma, plus gammaln returns imaginary parts outside of the interval (-pi, pi]. So I guess that this pull request should really be a BUG instead of an ENH. Edit: for comparison, if you replace loggamma with gammaln in the TestSystematic.test_loggamma_complex test you get 1509/4150 failures.

A point worth noting is that for complex values gamma is computed by taking exp(gammaln(z)). (This happens inside the cgama function in specfun.f.) Some of the errors in gammaln for complex arguments propagate into gamma, which isn't caught by the current tests because gamma is only tested for real arguments (when it's using cephes anyway). If you change the mpmath tests to check ComplexArg() instead of Arg() there are 32 failures, but these can be fixed by using exp(loggamma(z)) instead. But maybe that change is best left for a separate pull request? (I think a good implementation would be a little more complicated than just returning exp(loggamma(z)); since the Gamma function blows up so quickly it would be good to do checks that return nan's right away instead of doing a bunch of unnecessary computations.)

loggamma has zeros at 1 and 2 which were causing a loss in accuracy,
so used Taylor series about those points. Also increased the number of
terms used in the Laurent series to improve accuracy near 0. Added
tests to make sure there aren't jumps in accuracy when switching from
the Taylor/Laurent series regime to the recurrence relation regime.
@person142
Copy link
Member Author

Updated yet again to improve accuracy around the pole at 0 and the zeros at 1 and 2.

Also corrected some terminology in the comments for the series
expansion for loggamma around 0.
Expanded the range of the Taylor series around 1 to a disk of radius
1/2 by using an explicit expression in terms of the zeta
function. Started computing the function around 0 and 2 by using the
recurrence relation to shift to 1 and then using the Taylor series.
This increases accuracy and simplifies the code. There are now no
known regions in the complex plane where the relative error is greater
than 1e-13.
Added extra test coverage of the problem region and am updating to
make Travis run again to figure out what's going wrong.
The added tests show that the accuracy around z = 1 is fine, which
makes it likely that the Taylor series is fine. The accuracy has
problems around z = 2. Results in that region are computed by the
formula log(z - 1) + loggamma(z - 1). The loggamma term is computed by
the Taylor series, so differences in accuracy for log around z = 1 on
different systems are the prime suspects. Add an expansion of log
around 1 to test this hypothesis.
@person142
Copy link
Member Author

Updated with more improvements to accuracy in the z = 0, 1, 2 region and some simplifications to the code. Since there have been a number of changes I'll summarize where things stand:

  • The complex behavior of gammaln has been moved into loggamma. Now gammaln computes log(|Gamma(x)|) and loggamma computes the principle branch of the logarithm of Gamma. A deprecation warning has been added for complex arguments to gammaln.
  • The old specfun.f code computing the principle branch has been rewritten to improve accuracy. As mentioned above, the test TestSystematic.test_loggamma_complex goes from 1509/4150 failures with the old code to 0 failures with the new code.

I ran into a bug which (AFAICT) turned out to be caused by differences in accuracy of npy_clog around z = 1on different systems. (Compare the failures in 076d7a1 to the fix in acec387.) I fixed the issue by adding an expansion for log around z = 1 to the loggamma code, but perhaps there's a better way to do this?

@pv pv merged commit acec387 into scipy:master Mar 28, 2016
pv added a commit that referenced this pull request Mar 28, 2016
ENH: implement the principal branch of the logarithm of Gamma

Add a new function `loggamma` for computing log Gamma on the complex
plane using a consistent branch.

Deprecate complex-valued inputs from `gammaln` --- the definition is
inconsistent with the output from the real-valued function, and moreover
the implementation of the complex-valued function has some bugs
(accuracy issues, unnecessary branch cuts).
@pv
Copy link
Member

pv commented Mar 28, 2016

thanks, merged

@pv pv removed the needs-work Items that are pending response from the author label Mar 28, 2016
@pv
Copy link
Member

pv commented Mar 28, 2016

On the npy_clog issue --- may be a bug in the system libm, or in the numpy implementation of clog (depending of which one is used). So maybe report to numpy?

@pv pv added this to the 0.18.0 milestone Mar 28, 2016
@person142 person142 deleted the loggamma branch March 29, 2016 05:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants