src/pylib/Lib/math_impl/patch/lgamma

Source   Edit  

Notice

This module was hand-translated from FreeBSD's lgamma implementation;

and with some modification:

  • no static data is used, so each function is thread-safe.

The following copyright, license, and long comment were part of the original implementation available as part of FreeBSD

Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.

Developed at SunPro, a Sun Microsystems, Inc. business.
Permission to use, copy, modify, and distribute this
software is freely granted, provided that this notice
is preserved.

Procs

func lgamma[F: SomeFloat](x: F): F

Evaluates the natural logarithm of the absolute value of gamma function.

The following was formatted as Nim-Favor Markdown from FreeBSD `lgamma` source with some minor amendment.

Method:

1. Argument Reduction for 0 < x <= 8

Since gamma(1+s)=s*gamma(s), for x in [0,8], we may
reduce x to a number in [1.5,2.5] by
        lgamma(1+s) = log(s) + lgamma(s)
for example,
        lgamma(7.3) = log(6.3) + lgamma(6.3)
                    = log(6.3*5.3) + lgamma(5.3)
                    = log(6.3*5.3*4.3*3.3*2.3) + lgamma(2.3)

2. A polynomial approximation of lgamma

Create a polynomial approximation of lgamma around its minimun ymin=1.461632144968362245 to maintain monotonicity.

On [ymin-0.23, ymin+0.27] (i.e., [1.23164,1.73163]), use
        Let z = x-ymin;
        lgamma(x) = -1.214862905358496078218 + z^2*poly(z)
where
        poly(z) is a 14 degree polynomial.

3. Rational approximation in the primary interval [2,3]

We use the following approximation:
        s = x-2.0;
        lgamma(x) = 0.5*s + s*P(s)/Q(s)
with accuracy
        |P/Q - (lgamma(x)-0.5s)| < 2**-61.71
Our algorithms are based on the following observation
                            
                            zeta(2)-1    2    zeta(3)-1    3
lgamma(2+s) = s*(1-Euler) + --------- * s  -  --------- * s  + ...
                                2                 3

where Euler = 0.5771... is the Euler constant, which is very close to 0.5.

4. For x>=8, ...

For x>=8, we have
  lgamma(x) ~ (x-0.5)log(x)-x+0.5*log(2pi)+1/(12x)-1/(360x**3)+....

(better formula:
   lgamma(x) ~ (x-0.5)*(log(x)-1)-.5*(log(2pi)-1) + ...)

Let z = 1/x, then we approximation
        f(z) = lgamma(x) - (x-0.5)(log(x)-1)
by
                            3       5             11
        w = w0 + w1*z + w2*z  + w3*z  + ... + w6*z
where
        |w - f(z)| < 2**-58.74

5. For negative x, ...

For negative x, since (G is gamma function)
        -x*G(-x)*G(x) = pi/sin(pi*x),
we have
        G(x) = pi/(sin(pi*x)*(-x)*G(-x))
since G(-x) is positive, sign(G(x)) = sign(sin(pi*x)) for x<0
Hence, for x<0, signgam = sign(sin(pi*x)) and
        lgamma(x) = log(|Gamma(x)|)
                  = log(pi/(|x*sin(pi*x)|)) - lgamma(-x);

Note: one should avoid computing pi*(-x) directly in the computation of sin(pi*(-x)) but using sinpi(-x)

Special Cases

        lgamma(2+s) ~ s*(1-Euler) for tiny s
        lgamma(1) = lgamma(2) = 0
        lgamma(x) ~ -log(|x|) for tiny x
        lgamma(0) = lgamma(neg.integer) = inf and raise divide-by-zero
        lgamma(inf) = inf
        lgamma(-inf) = inf  # see below
For lgamma(-inf),

  • some implementations, like CPython's math, R

and C/C++ returns +inf; which is not suitable, as gamma(x) where x < about -200 is always truncated to 0.0 at ieee754 float domain. This behavior was said for bug compatible with C99, and is readlly documented by POSIX man and cppreference.com

  • While others like scipy.special.gammaln, Go's math.Lgamma, returns

In my option, ln(|gamma(-oo)|) -[ieee754 float trunc]-> ln(0+) -> -inf

But in this function it returns +inf to keep consistent with Python,

Example:

from std/math import isNaN
assert lgamma(1.0) == 0.0
assert lgamma(Inf) == Inf
assert lgamma(NaN).isNaN
Source   Edit  
func lgamma[F: SomeFloat](x: F; res: var F): GammaError
currently do not return geOverFlow, geUnderFlow Source   Edit  
func rLgamma[F: SomeFloat](x: F): F {....raises: [].}
Source   Edit  
func scipyGammaLn[F: SomeFloat](x: F): F {....raises: [].}
Note: this returns -inf for -inf argument, and raises no math error, just like scipy.special.gammaln
Source   Edit  
func stdlibJsLgamma[F: SomeFloat](x: F): F {....raises: [].}
Source   Edit