Skip to content

Commit 1e1dfc9

Browse files
authored
Merge pull request #3241 from stan-dev/fix-gamma-lccdf
fix-gamma-lccdf
2 parents 45bd042 + 6e16802 commit 1e1dfc9

5 files changed

Lines changed: 1101 additions & 32 deletions

File tree

stan/math/prim/prob/gamma_lccdf.hpp

Lines changed: 32 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -58,53 +58,55 @@ return_type_t<T_y, T_shape, T_inv_scale> gamma_lccdf(const T_y& y,
5858
// The gradients are technically ill-defined, but treated as zero
5959
for (size_t i = 0; i < stan::math::size(y); i++) {
6060
if (y_vec.val(i) == 0) {
61+
// LCCDF(0) = log(P(Y > 0)) = log(1) = 0
6162
return ops_partials.build(0.0);
6263
}
6364
}
6465

65-
VectorBuilder<is_autodiff_v<T_shape>, T_partials_return, T_shape> gamma_vec(
66-
math::size(alpha));
67-
VectorBuilder<is_autodiff_v<T_shape>, T_partials_return, T_shape> digamma_vec(
68-
math::size(alpha));
69-
70-
if constexpr (is_autodiff_v<T_shape>) {
71-
for (size_t i = 0; i < stan::math::size(alpha); i++) {
72-
const T_partials_return alpha_dbl = alpha_vec.val(i);
73-
gamma_vec[i] = tgamma(alpha_dbl);
74-
digamma_vec[i] = digamma(alpha_dbl);
75-
}
76-
}
77-
7866
for (size_t n = 0; n < N; n++) {
7967
// Explicit results for extreme values
8068
// The gradients are technically ill-defined, but treated as zero
8169
if (y_vec.val(n) == INFTY) {
70+
// LCCDF(∞) = log(P(Y > ∞)) = log(0) = -∞
8271
return ops_partials.build(negative_infinity());
8372
}
8473

8574
const T_partials_return y_dbl = y_vec.val(n);
8675
const T_partials_return alpha_dbl = alpha_vec.val(n);
8776
const T_partials_return beta_dbl = beta_vec.val(n);
88-
89-
const T_partials_return Pn = gamma_q(alpha_dbl, beta_dbl * y_dbl);
90-
91-
P += log(Pn);
92-
93-
if constexpr (is_autodiff_v<T_y>) {
94-
partials<0>(ops_partials)[n] -= beta_dbl * exp(-beta_dbl * y_dbl)
95-
* pow(beta_dbl * y_dbl, alpha_dbl - 1)
96-
/ tgamma(alpha_dbl) / Pn;
77+
const T_partials_return beta_y_dbl = beta_dbl * y_dbl;
78+
79+
// Qn = 1 - Pn
80+
const T_partials_return Qn = gamma_q(alpha_dbl, beta_y_dbl);
81+
const T_partials_return log_Qn = log(Qn);
82+
83+
P += log_Qn;
84+
85+
if constexpr (is_any_autodiff_v<T_y, T_inv_scale>) {
86+
const T_partials_return log_y_dbl = log(y_dbl);
87+
const T_partials_return log_beta_dbl = log(beta_dbl);
88+
const T_partials_return log_pdf
89+
= alpha_dbl * log_beta_dbl - lgamma(alpha_dbl)
90+
+ (alpha_dbl - 1.0) * log_y_dbl - beta_y_dbl;
91+
const T_partials_return common_term = exp(log_pdf - log_Qn);
92+
93+
if constexpr (is_autodiff_v<T_y>) {
94+
// d/dy log(1-F(y)) = -f(y)/(1-F(y))
95+
partials<0>(ops_partials)[n] -= common_term;
96+
}
97+
if constexpr (is_autodiff_v<T_inv_scale>) {
98+
// d/dbeta log(1-F(y)) = -y*f(y)/(beta*(1-F(y)))
99+
partials<2>(ops_partials)[n] -= y_dbl / beta_dbl * common_term;
100+
}
97101
}
102+
98103
if constexpr (is_autodiff_v<T_shape>) {
104+
const T_partials_return digamma_val = digamma(alpha_dbl);
105+
const T_partials_return gamma_val = tgamma(alpha_dbl);
106+
// d/dalpha log(1-F(y)) = grad_upper_inc_gamma / (1-F(y))
99107
partials<1>(ops_partials)[n]
100-
+= grad_reg_inc_gamma(alpha_dbl, beta_dbl * y_dbl, gamma_vec[n],
101-
digamma_vec[n])
102-
/ Pn;
103-
}
104-
if constexpr (is_autodiff_v<T_inv_scale>) {
105-
partials<2>(ops_partials)[n] -= y_dbl * exp(-beta_dbl * y_dbl)
106-
* pow(beta_dbl * y_dbl, alpha_dbl - 1)
107-
/ tgamma(alpha_dbl) / Pn;
108+
+= grad_reg_inc_gamma(alpha_dbl, beta_y_dbl, gamma_val, digamma_val)
109+
/ Qn;
108110
}
109111
}
110112
return ops_partials.build(P);

stan/math/prim/prob/gamma_lcdf.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ return_type_t<T_y, T_shape, T_inv_scale> gamma_lcdf(const T_y& y,
5555
size_t N = max_size(y, alpha, beta);
5656

5757
// Explicit return for extreme values
58-
// The gradients are technically ill-defined, but treated as zero
58+
// The gradients are technically ill-defined
5959
for (size_t i = 0; i < stan::math::size(y); i++) {
6060
if (y_vec.val(i) == 0) {
6161
return ops_partials.build(negative_infinity());
@@ -70,7 +70,6 @@ return_type_t<T_y, T_shape, T_inv_scale> gamma_lcdf(const T_y& y,
7070
}
7171

7272
const T_partials_return y_dbl = y_vec.val(n);
73-
const T_partials_return log_y_dbl = log(y_dbl);
7473
const T_partials_return alpha_dbl = alpha_vec.val(n);
7574
const T_partials_return beta_dbl = beta_vec.val(n);
7675
const T_partials_return log_beta_dbl = log(beta_dbl);
@@ -82,6 +81,7 @@ return_type_t<T_y, T_shape, T_inv_scale> gamma_lcdf(const T_y& y,
8281
P += log_Pn;
8382

8483
if constexpr (is_any_autodiff_v<T_y, T_inv_scale>) {
84+
const T_partials_return log_y_dbl = log(y_dbl);
8585
const T_partials_return d_num
8686
= (-beta_y_dbl) + (alpha_dbl - 1) * (log_beta_dbl + log_y_dbl);
8787
const T_partials_return d_den = lgamma(alpha_dbl) + log_Pn;
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
#include <stan/math/prim.hpp>
2+
#include <gtest/gtest.h>
3+
#include <limits>
4+
5+
TEST(ProbGamma, lccdf_works) {
6+
using stan::math::gamma_lccdf;
7+
8+
double y = 0.8;
9+
double alpha = 1.1;
10+
double beta = 2.3;
11+
12+
EXPECT_NO_THROW(gamma_lccdf(y, alpha, beta));
13+
}
14+
15+
TEST(ProbGamma, lccdf_zero_y) {
16+
using stan::math::gamma_lccdf;
17+
18+
// When y = 0, LCCDF(0) = log(P(Y > 0)) = log(1) = 0
19+
// For continuous distribution, P(Y > 0) = 1
20+
double alpha = 1.5;
21+
double beta = 2.0;
22+
23+
double result = gamma_lccdf(0.0, alpha, beta);
24+
EXPECT_EQ(result, 0.0);
25+
}
26+
27+
TEST(ProbGamma, lccdf_large_y) {
28+
using stan::math::gamma_lccdf;
29+
30+
// When y is very large, CDF approaches 1, so LCCDF = log(1-1) = log(0) = -inf
31+
double alpha = 1.5;
32+
double beta = 2.0;
33+
double y = 1e10;
34+
35+
double result = gamma_lccdf(y, alpha, beta);
36+
37+
// Should be a very large negative number (approaching -infinity)
38+
EXPECT_LT(result, -1000.0);
39+
}
40+
41+
TEST(ProbGamma, lccdf_infinity_y) {
42+
using stan::math::gamma_lccdf;
43+
using stan::math::negative_infinity;
44+
45+
// When y = infinity, LCCDF = log(P(Y > ∞)) = log(0) = -∞
46+
double alpha = 1.5;
47+
double beta = 2.0;
48+
double y = std::numeric_limits<double>::infinity();
49+
50+
double result = gamma_lccdf(y, alpha, beta);
51+
EXPECT_EQ(result, negative_infinity());
52+
}
53+
54+
TEST(ProbGamma, lccdf_small_alpha_small_y) {
55+
using stan::math::gamma_lccdf;
56+
57+
// Small alpha, small y - numerically challenging
58+
double y = 0.001;
59+
double alpha = 0.1;
60+
double beta = 1.0;
61+
62+
double result = gamma_lccdf(y, alpha, beta);
63+
64+
// Should be finite and negative
65+
EXPECT_TRUE(std::isfinite(result));
66+
EXPECT_LT(result, 0.0);
67+
}
68+
69+
TEST(ProbGamma, lccdf_large_alpha_large_y) {
70+
using stan::math::gamma_lccdf;
71+
72+
// Large alpha, large y
73+
double y = 100.0;
74+
double alpha = 50.0;
75+
double beta = 0.5;
76+
77+
double result = gamma_lccdf(y, alpha, beta);
78+
79+
// Should be finite
80+
EXPECT_TRUE(std::isfinite(result));
81+
}
82+
83+
TEST(ProbGamma, lccdf_alpha_one) {
84+
using stan::math::gamma_lccdf;
85+
using std::exp;
86+
using std::log;
87+
88+
// When alpha = 1, gamma becomes exponential
89+
// For exponential with rate beta: LCCDF(y) = log(1 - (1-exp(-beta*y))) =
90+
// log(exp(-beta*y)) = -beta*y
91+
double y = 2.0;
92+
double alpha = 1.0;
93+
double beta = 3.0;
94+
95+
double result = gamma_lccdf(y, alpha, beta);
96+
double expected = -beta * y; // = -6.0
97+
98+
EXPECT_NEAR(result, expected, 1e-10);
99+
}
100+
101+
TEST(ProbGamma, lccdf_various_values) {
102+
using stan::math::gamma_lccdf;
103+
104+
// Test a variety of parameter combinations
105+
std::vector<std::tuple<double, double, double>> test_cases = {
106+
{0.5, 0.5, 1.0}, // Small y, small alpha
107+
{1.0, 1.0, 1.0}, // All ones
108+
{2.0, 3.0, 0.5}, // Moderate values
109+
{10.0, 2.0, 0.1}, // Large y, small beta
110+
{0.1, 10.0, 2.0}, // Small y, large alpha
111+
{5.0, 5.0, 1.0}, // Equal alpha and y
112+
{0.01, 0.5, 10.0}, // Small y, large beta
113+
{100.0, 100.0, 1.0} // Large matched values
114+
};
115+
116+
for (const auto& test_case : test_cases) {
117+
double y = std::get<0>(test_case);
118+
double alpha = std::get<1>(test_case);
119+
double beta = std::get<2>(test_case);
120+
121+
double result = gamma_lccdf(y, alpha, beta);
122+
123+
// All results should be finite and <= 0
124+
EXPECT_TRUE(std::isfinite(result))
125+
<< "Failed for y=" << y << ", alpha=" << alpha << ", beta=" << beta;
126+
EXPECT_LE(result, 0.0) << "Failed for y=" << y << ", alpha=" << alpha
127+
<< ", beta=" << beta;
128+
}
129+
}
130+
131+
TEST(ProbGamma, lccdf_extreme_small_values) {
132+
using stan::math::gamma_lccdf;
133+
134+
// Very small but non-zero values
135+
double y = 1e-10;
136+
double alpha = 1e-5;
137+
double beta = 1.0;
138+
139+
double result = gamma_lccdf(y, alpha, beta);
140+
141+
EXPECT_TRUE(std::isfinite(result));
142+
}
143+
144+
TEST(ProbGamma, lccdf_extreme_large_alpha) {
145+
using stan::math::gamma_lccdf;
146+
147+
// Very large alpha (approaches normal distribution)
148+
double y = 1000.0;
149+
double alpha = 1000.0;
150+
double beta = 1.0;
151+
152+
double result = gamma_lccdf(y, alpha, beta);
153+
154+
EXPECT_TRUE(std::isfinite(result));
155+
}
156+
157+
TEST(ProbGamma, lccdf_monotonic_in_y) {
158+
using stan::math::gamma_lccdf;
159+
160+
// LCCDF should be monotonically decreasing in y
161+
double alpha = 2.0;
162+
double beta = 1.5;
163+
164+
double y1 = 1.0;
165+
double y2 = 2.0;
166+
double y3 = 3.0;
167+
168+
double lccdf1 = gamma_lccdf(y1, alpha, beta);
169+
double lccdf2 = gamma_lccdf(y2, alpha, beta);
170+
double lccdf3 = gamma_lccdf(y3, alpha, beta);
171+
172+
EXPECT_GT(lccdf1, lccdf2);
173+
EXPECT_GT(lccdf2, lccdf3);
174+
}
175+
176+
TEST(ProbGamma, lccdf_consistency_with_cdf) {
177+
using stan::math::gamma_cdf;
178+
using stan::math::gamma_lccdf;
179+
using std::log;
180+
181+
// Test that lccdf(y) ≈ log(1 - cdf(y))
182+
double y = 1.5;
183+
double alpha = 2.5;
184+
double beta = 1.8;
185+
186+
double lccdf_val = gamma_lccdf(y, alpha, beta);
187+
double cdf_val = gamma_cdf(y, alpha, beta);
188+
double expected = log(1.0 - cdf_val);
189+
190+
EXPECT_NEAR(lccdf_val, expected, 1e-10);
191+
}
192+
193+
TEST(ProbGamma, lccdf_numerically_challenging) {
194+
using stan::math::gamma_lccdf;
195+
196+
// Test cases that might cause numerical issues
197+
std::vector<std::tuple<double, double, double>> challenging_cases = {
198+
{1e-8, 1e-6, 1.0}, // Very small y and alpha
199+
{1e-6, 100.0, 1e-3}, // Very small y, large alpha, small beta
200+
{1000.0, 0.1, 1e-4}, // Large y, small alpha, very small beta
201+
{50.0, 50.0, 1.0}, // Matched moderate values
202+
{0.001, 0.001, 100.0}, // Small y and alpha, large beta
203+
{1e6, 10.0, 1e-6}, // Very large y, moderate alpha, very small beta
204+
};
205+
206+
for (const auto& test_case : challenging_cases) {
207+
double y = std::get<0>(test_case);
208+
double alpha = std::get<1>(test_case);
209+
double beta = std::get<2>(test_case);
210+
211+
double result = gamma_lccdf(y, alpha, beta);
212+
213+
// Should not be NaN
214+
EXPECT_FALSE(std::isnan(result))
215+
<< "NaN for y=" << y << ", alpha=" << alpha << ", beta=" << beta;
216+
217+
// Should be <= 0 (log of probability)
218+
EXPECT_LE(result, 0.0) << "Positive value for y=" << y
219+
<< ", alpha=" << alpha << ", beta=" << beta;
220+
}
221+
}
222+
223+
TEST(ProbGamma, lccdf_shape_zero_throws) {
224+
using stan::math::gamma_lccdf;
225+
226+
// alpha (shape) must be positive
227+
EXPECT_THROW(gamma_lccdf(1.0, 0.0, 1.0), std::domain_error);
228+
EXPECT_THROW(gamma_lccdf(1.0, -1.0, 1.0), std::domain_error);
229+
}
230+
231+
TEST(ProbGamma, lccdf_rate_zero_throws) {
232+
using stan::math::gamma_lccdf;
233+
234+
// beta (rate) must be positive
235+
EXPECT_THROW(gamma_lccdf(1.0, 1.0, 0.0), std::domain_error);
236+
EXPECT_THROW(gamma_lccdf(1.0, 1.0, -1.0), std::domain_error);
237+
}
238+
239+
TEST(ProbGamma, lccdf_negative_y_throws) {
240+
using stan::math::gamma_lccdf;
241+
242+
// y must be non-negative
243+
EXPECT_THROW(gamma_lccdf(-1.0, 1.0, 1.0), std::domain_error);
244+
EXPECT_THROW(gamma_lccdf(-0.001, 1.0, 1.0), std::domain_error);
245+
}

0 commit comments

Comments
 (0)