Skip to content

Commit e5b0af8

Browse files
authored
BUG: special: Fix behavior of gamma and gammasgn at poles of the Gamma function (scipy#21827)
Fixes the behavior of `special.gamma` and `special.gammasgn` at poles of the `gamma` function. As described in scipy#21810, we previously had `special.gamma(n) == np.inf` for every non-positive integer `n`, and bizarrely `special.gamma(-np.inf) == -np.inf`. Similarly, as described in scipy#19710, we've had `gammasgn(n) == 0.0` for every non-positive integer `n` and even more bizarrely, `gammasgn(-np.inf) == 1.0`. The sign of $\Gamma(x)$ at each pole depends on the direction in which $x$ approaches the pole, so it doesn't make sense to return $+\infty$ at all poles. Accordingly, the C99 standard recommends the following behavior for the gamma function in special cases. 1. If the argument is $\pm 0$, $\pm \infty$ is returned. 2. If the argument is a negative integer, NaN is returned. 3. If the argument is $-\infty$, NaN is returned. 4. If the argument is $+\infty$, $+\infty$ is returned 5. If the argument is NaN, NaN is returned. This updates `special.gamma` to follow this standard. Note that signed zeros offer the ability to specify the sign of infinity of `gamma(x)` at `x == 0`. This also updates `gammasgn` to be consistent with the above. Additionally, improve the accuracy of the `rgamma` implementation, and use that as a replacement where we relied on the previous behaviour; document this in the docstring.
1 parent c45b3ba commit e5b0af8

File tree

13 files changed

+181
-85
lines changed

13 files changed

+181
-85
lines changed

scipy/special/_special_ufuncs_docs.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1572,10 +1572,35 @@ const char *gamma_doc = R"(
15721572
which, combined with the fact that :math:`\Gamma(1) = 1`, implies
15731573
the above identity for :math:`z = n`.
15741574
1575+
The gamma function has poles at non-negative integers and the sign
1576+
of infinity as z approaches each pole depends upon the direction in
1577+
which the pole is approached. For this reason, the consistent thing
1578+
is for gamma(z) to return NaN at negative integers, and to return
1579+
-inf when x = -0.0 and +inf when x = 0.0, using the signbit of zero
1580+
to signify the direction in which the origin is being approached. This
1581+
is for instance what is recommended for the gamma function in annex F
1582+
entry 9.5.4 of the Iso C 99 standard [isoc99]_.
1583+
1584+
Prior to SciPy version 1.15, ``scipy.special.gamma(z)`` returned ``+inf``
1585+
at each pole. This was fixed in version 1.15, but with the following
1586+
consequence. Expressions where gamma appears in the denominator
1587+
such as
1588+
1589+
``gamma(u) * gamma(v) / (gamma(w) * gamma(x))``
1590+
1591+
no longer evaluate to 0 if the numerator is well defined but there is a
1592+
pole in the denominator. Instead such expressions evaluate to NaN. We
1593+
recommend instead using the function `rgamma` for the reciprocal gamma
1594+
function in such cases. The above expression could for instance be written
1595+
as
1596+
1597+
``gamma(u) * gamma(v) * (rgamma(w) * rgamma(x))``
1598+
15751599
References
15761600
----------
15771601
.. [dlmf] NIST Digital Library of Mathematical Functions
15781602
https://dlmf.nist.gov/5.2#E1
1603+
.. [isoc99] https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf
15791604
15801605
Examples
15811606
--------

scipy/special/tests/test_basic.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -398,8 +398,13 @@ def test_gammaln(self):
398398
cephes.gammaln(10)
399399

400400
def test_gammasgn(self):
401-
vals = np.array([-4, -3.5, -2.3, 1, 4.2], np.float64)
402-
assert_array_equal(cephes.gammasgn(vals), np.sign(cephes.rgamma(vals)))
401+
vals = np.array(
402+
[-np.inf, -4, -3.5, -2.3, -0.0, 0.0, 1, 4.2, np.inf], np.float64
403+
)
404+
reference = np.array(
405+
[np.nan, np.nan, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, 1.0], np.float64
406+
)
407+
assert_array_equal(cephes.gammasgn(vals), reference)
403408

404409
def test_gdtr(self):
405410
assert_equal(cephes.gdtr(1,1,0),0.0)
@@ -2715,9 +2720,26 @@ def test_rgamma(self):
27152720
assert_almost_equal(rgam,rlgam,8)
27162721

27172722
def test_infinity(self):
2718-
assert_(np.isinf(special.gamma(-1)))
27192723
assert_equal(special.rgamma(-1), 0)
27202724

2725+
@pytest.mark.parametrize(
2726+
"x,expected",
2727+
[
2728+
# infinities
2729+
([-np.inf, np.inf], [np.nan, np.inf]),
2730+
# negative and positive zero
2731+
([-0.0, 0.0], [-np.inf, np.inf]),
2732+
# small poles
2733+
(range(-32, 0), np.full(32, np.nan)),
2734+
# medium sized poles
2735+
(range(-1024, -32, 99), np.full(11, np.nan)),
2736+
# large pole
2737+
([-4.141512231792294e+16], [np.nan]),
2738+
]
2739+
)
2740+
def test_poles(self, x, expected):
2741+
assert_array_equal(special.gamma(x), expected)
2742+
27212743

27222744
class TestHankel:
27232745

@@ -3690,7 +3712,7 @@ def test_pbdv_seq(self):
36903712
def test_pbdv_points(self):
36913713
# simple case
36923714
eta = np.linspace(-10, 10, 5)
3693-
z = 2**(eta/2)*np.sqrt(np.pi)/special.gamma(.5-.5*eta)
3715+
z = 2**(eta/2)*np.sqrt(np.pi)*special.rgamma(.5-.5*eta)
36943716
assert_allclose(special.pbdv(eta, 0.)[0], z, rtol=1e-14, atol=1e-14)
36953717

36963718
# some points

scipy/special/tests/test_hyp2f1.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2488,6 +2488,27 @@ def test_region6(self, hyp2f1_test_case):
24882488
)
24892489
assert_allclose(hyp2f1(a, b, c, z), expected, rtol=rtol)
24902490

2491+
2492+
@pytest.mark.parametrize(
2493+
"hyp2f1_test_case",
2494+
[
2495+
# Broke when fixing gamma pole behavior in gh-21827
2496+
pytest.param(
2497+
Hyp2f1TestCase(
2498+
a=1.3,
2499+
b=-0.2,
2500+
c=0.3,
2501+
z=-2.1,
2502+
expected=1.8202169687521206,
2503+
rtol=5e-15,
2504+
),
2505+
),
2506+
]
2507+
)
2508+
def test_miscellaneous(self, hyp2f1_test_case ):
2509+
a, b, c, z, expected, rtol = hyp2f1_test_case
2510+
assert_allclose(hyp2f1(a, b, c, z), expected, rtol=rtol)
2511+
24912512
@pytest.mark.slow
24922513
@check_version(mpmath, "1.0.0")
24932514
def test_test_hyp2f1(self):

scipy/special/xsf/cephes/beta.h

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
#include "../config.h"
5454
#include "const.h"
5555
#include "gamma.h"
56+
#include "rgamma.h"
5657

5758
namespace xsf {
5859
namespace cephes {
@@ -155,17 +156,18 @@ namespace cephes {
155156
return (sign * std::exp(y));
156157
}
157158

158-
y = Gamma(y);
159+
y = rgamma(y);
159160
a = Gamma(a);
160161
b = Gamma(b);
161-
if (y == 0.0)
162+
if (std::isinf(y)) {
162163
goto overflow;
164+
}
163165

164-
if (std::abs(std::abs(a) - std::abs(y)) > std::abs(std::abs(b) - std::abs(y))) {
165-
y = b / y;
166+
if (std::abs(std::abs(a*y) - 1.0) > std::abs(std::abs(b*y) - 1.0)) {
167+
y = b * y;
166168
y *= a;
167169
} else {
168-
y = a / y;
170+
y = a * y;
169171
y *= b;
170172
}
171173

@@ -228,20 +230,20 @@ namespace cephes {
228230
return (y);
229231
}
230232

231-
y = Gamma(y);
233+
y = rgamma(y);
232234
a = Gamma(a);
233235
b = Gamma(b);
234-
if (y == 0.0) {
236+
if (std::isinf(y)) {
235237
over:
236238
set_error("lbeta", SF_ERROR_OVERFLOW, NULL);
237239
return (sign * std::numeric_limits<double>::infinity());
238240
}
239241

240-
if (std::abs(std::abs(a) - std::abs(y)) > std::abs(std::abs(b) - std::abs(y))) {
241-
y = b / y;
242+
if (std::abs(std::abs(a*y) - 1.0) > std::abs(std::abs(b*y) - 1.0)) {
243+
y = b * y;
242244
y *= a;
243245
} else {
244-
y = a / y;
246+
y = a * y;
245247
y *= b;
246248
}
247249

scipy/special/xsf/cephes/expn.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
#include "../config.h"
6666
#include "../error.h"
6767
#include "const.h"
68-
#include "gamma.h"
68+
#include "rgamma.h"
6969
#include "polevl.h"
7070

7171
namespace xsf {
@@ -252,7 +252,7 @@ namespace cephes {
252252
k = xk;
253253
t = n;
254254
r = n - 1;
255-
ans = (std::pow(z, r) * psi / Gamma(t)) - ans;
255+
ans = (std::pow(z, r) * psi * rgamma(t)) - ans;
256256
return ans;
257257
}
258258

scipy/special/xsf/cephes/gamma.h

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -149,17 +149,30 @@ namespace cephes {
149149
int sgngam = 1;
150150

151151
if (!std::isfinite(x)) {
152-
return x;
152+
if (x > 0) {
153+
// gamma(+inf) = +inf
154+
return x;
155+
}
156+
// gamma(NaN) and gamma(-inf) both should equal NaN.
157+
return std::numeric_limits<double>::quiet_NaN();
153158
}
159+
160+
if (x == 0) {
161+
/* For pole at zero, value depends on sign of zero.
162+
* +inf when approaching from right, -inf when approaching
163+
* from left. */
164+
return std::copysign(std::numeric_limits<double>::infinity(), x);
165+
}
166+
154167
q = std::abs(x);
155168

156169
if (q > 33.0) {
157170
if (x < 0.0) {
158171
p = std::floor(q);
159172
if (p == q) {
160-
gamnan:
161-
set_error("Gamma", SF_ERROR_OVERFLOW, NULL);
162-
return (std::numeric_limits<double>::infinity());
173+
// x is a negative integer. This is a pole.
174+
set_error("Gamma", SF_ERROR_SINGULAR, NULL);
175+
return (std::numeric_limits<double>::quiet_NaN());
163176
}
164177
i = p;
165178
if ((i & 1) == 0) {
@@ -215,7 +228,9 @@ namespace cephes {
215228

216229
small:
217230
if (x == 0.0) {
218-
goto gamnan;
231+
/* For this to have happened, x must have started as a negative integer. */
232+
set_error("Gamma", SF_ERROR_SINGULAR, NULL);
233+
return (std::numeric_limits<double>::quiet_NaN());
219234
} else
220235
return (z / ((1.0 + 0.5772156649015329 * x) * x));
221236
}
@@ -360,16 +375,23 @@ namespace cephes {
360375
}
361376
if (x > 0) {
362377
return 1.0;
363-
} else {
364-
fx = std::floor(x);
365-
if (x - fx == 0.0) {
366-
return 0.0;
367-
} else if (static_cast<int>(fx) % 2) {
368-
return -1.0;
369-
} else {
370-
return 1.0;
371-
}
372-
}
378+
}
379+
if (x == 0) {
380+
return std::copysign(1.0, x);
381+
}
382+
if (std::isinf(x)) {
383+
// x > 0 case handled, so x must be negative infinity.
384+
return std::numeric_limits<double>::quiet_NaN();
385+
}
386+
fx = std::floor(x);
387+
if (x - fx == 0.0) {
388+
return std::numeric_limits<double>::quiet_NaN();
389+
}
390+
// sign of gamma for x in (-n, -n+1) for positive integer n is (-1)^n.
391+
if (static_cast<int>(fx) % 2) {
392+
return -1.0;
393+
}
394+
return 1.0;
373395
}
374396

375397
} // namespace cephes

scipy/special/xsf/cephes/hyp2f1.h

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575

7676
#include "const.h"
7777
#include "gamma.h"
78+
#include "rgamma.h"
7879
#include "psi.h"
7980

8081
namespace xsf {
@@ -252,9 +253,9 @@ namespace cephes {
252253
/* sum for t = 0 */
253254
y = xsf::cephes::psi(1.0) + xsf::cephes::psi(1.0 + e) - xsf::cephes::psi(a + d1) -
254255
xsf::cephes::psi(b + d1) - ax;
255-
y /= xsf::cephes::Gamma(e + 1.0);
256+
y *= xsf::cephes::rgamma(e + 1.0);
256257

257-
p = (a + d1) * (b + d1) * s / xsf::cephes::Gamma(e + 2.0); /* Poch for t=1 */
258+
p = (a + d1) * (b + d1) * s * xsf::cephes::rgamma(e + 2.0); /* Poch for t=1 */
258259
t = 1.0;
259260
do {
260261
r = xsf::cephes::psi(1.0 + t) + xsf::cephes::psi(1.0 + t + e) -
@@ -292,10 +293,10 @@ namespace cephes {
292293
}
293294
nosum:
294295
p = xsf::cephes::Gamma(c);
295-
y1 *= xsf::cephes::Gamma(e) * p /
296-
(xsf::cephes::Gamma(a + d1) * xsf::cephes::Gamma(b + d1));
296+
y1 *= xsf::cephes::Gamma(e) * p *
297+
(xsf::cephes::rgamma(a + d1) * xsf::cephes::rgamma(b + d1));
297298

298-
y *= p / (xsf::cephes::Gamma(a + d2) * xsf::cephes::Gamma(b + d2));
299+
y *= p * (xsf::cephes::rgamma(a + d2) * xsf::cephes::rgamma(b + d2));
299300
if ((aid & 1) != 0)
300301
y = -y;
301302

@@ -493,8 +494,8 @@ namespace cephes {
493494
p *= std::pow(-x, -a);
494495
q *= std::pow(-x, -b);
495496
t1 = Gamma(c);
496-
s = t1 * Gamma(b - a) / (Gamma(b) * Gamma(c - a));
497-
y = t1 * Gamma(a - b) / (Gamma(a) * Gamma(c - b));
497+
s = t1 * Gamma(b - a) * (rgamma(b) * rgamma(c - a));
498+
y = t1 * Gamma(a - b) * (rgamma(a) * rgamma(c - b));
498499
return s * p + y * q;
499500
} else if (x < -1.0) {
500501
if (std::abs(a) < std::abs(b)) {
@@ -532,7 +533,7 @@ namespace cephes {
532533
}
533534
if (d <= 0.0)
534535
goto hypdiv;
535-
y = Gamma(c) * Gamma(d) / (Gamma(p) * Gamma(r));
536+
y = Gamma(c) * Gamma(d) * (rgamma(p) * rgamma(r));
536537
goto hypdon;
537538
}
538539
if (d <= -1.0)

scipy/special/xsf/cephes/hyperg.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767

6868
#include "const.h"
6969
#include "gamma.h"
70+
#include "rgamma.h"
7071

7172
namespace xsf {
7273
namespace cephes {
@@ -203,14 +204,14 @@ namespace cephes {
203204

204205
h1 = hyp2f0(a, a - b + 1, -1.0 / x, 1, &err1);
205206

206-
temp = std::exp(u) / xsf::cephes::Gamma(b - a);
207+
temp = std::exp(u) * xsf::cephes::rgamma(b - a);
207208
h1 *= temp;
208209
err1 *= temp;
209210

210211
h2 = hyp2f0(b - a, 1.0 - a, 1.0 / x, 2, &err2);
211212

212213
if (a < 0)
213-
temp = std::exp(t) / xsf::cephes::Gamma(a);
214+
temp = std::exp(t) * xsf::cephes::rgamma(a);
214215
else
215216
temp = std::exp(t - xsf::cephes::lgam(a));
216217

scipy/special/xsf/cephes/incbet.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,21 @@ namespace cephes {
7474

7575
namespace detail {
7676

77+
/* Compute (u * v) * w, disabling optimizations for gcc on 32 bit systems.
78+
* Used below in incbet_pseries to prevent aggressive optimizations from
79+
* degrading accuracy.
80+
*/
81+
#if defined(__GNUC__) && defined(__i386__)
82+
#pragma GCC push_options
83+
#pragma GCC optimize("00")
84+
#endif
85+
XSF_HOST_DEVICE inline double triple_product(double u, double v, double w) {
86+
return (u * v) * w;
87+
}
88+
#if defined(__GNUC__) && defined(__i386__)
89+
#pragma GCC pop_options
90+
#endif
91+
7792
constexpr double incbet_big = 4.503599627370496e15;
7893
constexpr double incbet_biginv = 2.22044604925031308085e-16;
7994

@@ -104,7 +119,7 @@ namespace cephes {
104119
u = a * std::log(x);
105120
if ((a + b) < MAXGAM && std::abs(u) < MAXLOG) {
106121
t = 1.0 / beta(a, b);
107-
s = s * t * std::pow(x, a);
122+
s = triple_product(s, t, std::pow(x, a)); // (s * t) * std::pow(x, a)
108123
} else {
109124
t = -lbeta(a, b) + u + std::log(s);
110125
if (t < MINLOG) {

0 commit comments

Comments
 (0)