Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7ae8e42
Compute the incomplete beta shape gradients by continued fraction
avehtari Sep 20, 2026
138e257
Route inc_beta_dda and inc_beta_ddb through grad_reg_inc_beta
avehtari Sep 20, 2026
eb89df5
Compute both beta_cdf shape partials in one root call
avehtari Sep 20, 2026
671d5de
Test the incomplete beta shape gradients against fixed references
avehtari Sep 20, 2026
bd8f27a
Merge remote-tracking branch 'origin/develop' into stable-inc-beta
SteveBronder Oct 20, 2026
d2eeacd
Reverse-mode grad_reg_inc_beta with a Hessian-vector product reverse …
SteveBronder Oct 21, 2026
5042b15
Merge commit 'e50e79f1889ea8c05e2267b03af3e7185b08d1f2' into HEAD
yashikno Oct 21, 2026
2c96499
[Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1
stan-buildbot Oct 21, 2026
a34e6f7
Return infinity from inc_beta_ddz where the density is infinite
avehtari Oct 22, 2026
3fd3fe2
Start the inc_beta upper-bound mix test from an empty autodiff stack
avehtari Oct 22, 2026
5f36279
Evaluate the incomplete beta complement by its symmetry relation
avehtari Sep 28, 2026
277a9c7
Use inc_beta_ddz for the density factor of the lccdf partials
avehtari Sep 28, 2026
1c67662
Test the lccdf functions where the complement is below eps
avehtari Sep 28, 2026
0409160
Return negative infinity from the beta lccdf functions at y = 1
avehtari Sep 29, 2026
8eaaad9
Form 1 - r in the student_t cdf functions without subtraction
avehtari Oct 8, 2026
36e2070
Evaluate the neg_binomial_lccdf beta density at the argument below 1/2
avehtari Oct 8, 2026
75d8bfe
Evaluate the incomplete beta complement from the argument below 1/2
avehtari Oct 8, 2026
709a1a4
Merge stable-inc-beta-cdfs into stable-beta-lccdf
SteveBronder Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions stan/math/prim/fun/inc_beta.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,69 @@ inline double inc_beta(double a, double b, double x) {
return boost::math::ibeta(a, b, x, boost_policy_t<>());
}

namespace internal {

/**
* Return the complement of the regularized incomplete beta function,
* 1 - I_x(a, b) = I_{1-x}(b, a), from both x and 1 - x.
*
* Boost forms 1 - x from its argument, so an argument close to 1 loses
* relative precision in 1 - x. This overload calls Boost with whichever
* of x and 1 - x is at most 1/2: ibetac(a, b, x) or ibeta(b, a, 1 - x).
* Both keep their relative precision when the complement is below eps.
*
* Below the mean a / (a + b), while I_x(a, b) is at most 1/2, the
* complement is formed as 1 - I_x(a, b), which then has full precision.
* This also avoids Boost's arcsine case a = b = 1/2, where ibetac
* evaluates asin(sqrt(1 - x)) and loses digits for small x.
*
* @param a first shape, a > 0
* @param b second shape, b > 0
* @param x argument, 0 <= x <= 1
* @param one_m_x 1 - x, computed by the caller without cancellation
* @return 1 - I_x(a, b)
*/
inline double inc_beta_complement(double a, double b, double x,
double one_m_x) {
check_not_nan("inc_beta_complement", "a", a);
check_not_nan("inc_beta_complement", "b", b);
check_not_nan("inc_beta_complement", "x", x);
if (x > 0.5) {
return boost::math::ibeta(b, a, one_m_x, boost_policy_t<>());
}
if (x < a / (a + b)) {
const double inc = boost::math::ibeta(a, b, x, boost_policy_t<>());
if (inc <= 0.5) {
return 1.0 - inc;
}
}
return boost::math::ibetac(a, b, x, boost_policy_t<>());
}

/**
* Return the complement of the regularized incomplete beta function for
* autodiff arguments, by the symmetry relation through inc_beta.
*
* @tparam T1 type of a
* @tparam T2 type of b
* @tparam T3 type of x
* @tparam T4 type of one_m_x
* @param a first shape, a > 0
* @param b second shape, b > 0
* @param x argument, 0 <= x <= 1; not used
* @param one_m_x 1 - x
* @return 1 - I_x(a, b)
*/
template <typename T1, typename T2, typename T3, typename T4,
require_any_not_arithmetic_t<T1, T2, T3, T4>* = nullptr>
inline return_type_t<T1, T2, T4> inc_beta_complement(const T1& a, const T2& b,
const T3& /* x */,
const T4& one_m_x) {
return inc_beta(b, a, one_m_x);
}

} // namespace internal

/**
* Enables the vectorized application of the inc_beta function, when
* any arguments are containers.
Expand Down
66 changes: 50 additions & 16 deletions stan/math/prim/prob/beta_lccdf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <stan/math/prim/fun/digamma.hpp>
#include <stan/math/prim/fun/grad_reg_inc_beta.hpp>
#include <stan/math/prim/fun/inc_beta.hpp>
#include <stan/math/prim/fun/inc_beta_ddz.hpp>
#include <stan/math/prim/fun/inv.hpp>
#include <stan/math/prim/fun/log.hpp>
#include <stan/math/prim/fun/max_size.hpp>
Expand Down Expand Up @@ -73,38 +74,71 @@ inline return_type_t<T_y, T_scale_succ, T_scale_fail> beta_lccdf(
constexpr bool calc_beta = is_autodiff_v<T_scale_fail>;
for (size_t n = 0; n < N; n++) {
const auto y_dbl = y_vec.val(n);

// Explicit results for extreme values
// The gradients are technically ill-defined, but treated as zero
if (y_dbl >= 1.0) {
return ops_partials.build(negative_infinity());
}

const auto alpha_dbl = alpha_vec.val(n);
const auto beta_dbl = beta_vec.val(n);
const auto betafunc_dbl = beta(alpha_dbl, beta_dbl);
const auto Pn = 1.0 - inc_beta(alpha_dbl, beta_dbl, y_dbl);
// The complement 1 - I_y(a, b), evaluated from the smaller of y and
// 1 - y. Forming it as a difference loses every digit once I_y(a, b)
// rounds to 1, which happens for every complement below eps.
const auto Pn = internal::inc_beta_complement(alpha_dbl, beta_dbl, y_dbl,
1.0 - y_dbl);
const auto inv_Pn
= is_constant_all<T_y, T_scale_succ, T_scale_fail>::value ? 0 : inv(Pn);

ccdf_log += log(Pn);

if constexpr (is_autodiff_v<T_y>) {
partials<0>(ops_partials)[n] -= pow(1 - y_dbl, beta_dbl - 1)
* pow(y_dbl, alpha_dbl - 1) * inv_Pn
/ betafunc_dbl;
// inc_beta_ddz is the same density, evaluated without the product of
// two powers over a beta function, each of which can underflow on
// its own.
partials<0>(ops_partials)[n]
-= inc_beta_ddz(alpha_dbl, beta_dbl, y_dbl) * inv_Pn;
}

// The shape derivatives from the same argument as the value. For
// y <= 1/2 they are those of I_y(a, b), which are those of -Pn; on the
// reflected arguments they are those of Pn itself, with beta the first
// shape and alpha the second.
[[maybe_unused]] const bool reflect = y_dbl > 0.5;
if constexpr (calc_alpha && calc_beta) {
const auto [g1, g2] = grad_reg_inc_beta<GRAD_RIB::BOTH>(
alpha_dbl, beta_dbl, y_dbl, digamma(alpha_dbl), digamma(beta_dbl),
digamma(alpha_dbl + beta_dbl), 0.0);
partials<1>(ops_partials)[n] -= g1 * inv_Pn;
partials<2>(ops_partials)[n] -= g2 * inv_Pn;
if (reflect) {
const auto [g1, g2] = grad_reg_inc_beta<GRAD_RIB::BOTH>(
beta_dbl, alpha_dbl, 1.0 - y_dbl, digamma(beta_dbl),
digamma(alpha_dbl), digamma(alpha_dbl + beta_dbl), 0.0);
partials<1>(ops_partials)[n] += g2 * inv_Pn;
partials<2>(ops_partials)[n] += g1 * inv_Pn;
} else {
const auto [g1, g2] = grad_reg_inc_beta<GRAD_RIB::BOTH>(
alpha_dbl, beta_dbl, y_dbl, digamma(alpha_dbl), digamma(beta_dbl),
digamma(alpha_dbl + beta_dbl), 0.0);
partials<1>(ops_partials)[n] -= g1 * inv_Pn;
partials<2>(ops_partials)[n] -= g2 * inv_Pn;
}
} else if constexpr (calc_alpha) {
const auto digamma_sum = digamma(alpha_dbl + beta_dbl);
partials<1>(ops_partials)[n]
-= grad_reg_inc_beta<GRAD_RIB::G1>(alpha_dbl, beta_dbl, y_dbl,
digamma(alpha_dbl), 0.0,
digamma(alpha_dbl + beta_dbl), 0.0)
+= (reflect ? grad_reg_inc_beta<GRAD_RIB::G2>(
beta_dbl, alpha_dbl, 1.0 - y_dbl, 0.0,
digamma(alpha_dbl), digamma_sum, 0.0)
: -grad_reg_inc_beta<GRAD_RIB::G1>(
alpha_dbl, beta_dbl, y_dbl, digamma(alpha_dbl),
0.0, digamma_sum, 0.0))
* inv_Pn;
} else if constexpr (calc_beta) {
const auto digamma_sum = digamma(alpha_dbl + beta_dbl);
partials<2>(ops_partials)[n]
-= grad_reg_inc_beta<GRAD_RIB::G2>(alpha_dbl, beta_dbl, y_dbl, 0.0,
digamma(beta_dbl),
digamma(alpha_dbl + beta_dbl), 0.0)
+= (reflect ? grad_reg_inc_beta<GRAD_RIB::G1>(
beta_dbl, alpha_dbl, 1.0 - y_dbl,
digamma(beta_dbl), 0.0, digamma_sum, 0.0)
: -grad_reg_inc_beta<GRAD_RIB::G2>(
alpha_dbl, beta_dbl, y_dbl, 0.0, digamma(beta_dbl),
digamma_sum, 0.0))
* inv_Pn;
}
}
Expand Down
51 changes: 40 additions & 11 deletions stan/math/prim/prob/beta_proportion_lccdf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <stan/math/prim/fun/beta.hpp>
#include <stan/math/prim/fun/grad_reg_inc_beta.hpp>
#include <stan/math/prim/fun/inc_beta.hpp>
#include <stan/math/prim/fun/inc_beta_ddz.hpp>
#include <stan/math/prim/fun/inv.hpp>
#include <stan/math/prim/fun/log.hpp>
#include <stan/math/prim/fun/max_size.hpp>
Expand Down Expand Up @@ -75,36 +76,64 @@ inline return_type_t<T_y, T_loc, T_prec> beta_proportion_lccdf(

for (size_t n = 0; n < N; n++) {
const T_partials_return y_dbl = y_vec.val(n);

// Explicit results for extreme values
// The gradients are technically ill-defined, but treated as zero
if (y_dbl >= 1.0) {
return ops_partials.build(negative_infinity());
}

const T_partials_return mu_dbl = mu_vec.val(n);
const T_partials_return kappa_dbl = kappa_vec.val(n);
const T_partials_return mukappa_dbl = mu_dbl * kappa_dbl;
const T_partials_return kappa_mukappa_dbl = kappa_dbl - mukappa_dbl;
const T_partials_return betafunc_dbl = beta(mukappa_dbl, kappa_mukappa_dbl);
const T_partials_return Pn
= 1 - inc_beta(mukappa_dbl, kappa_mukappa_dbl, y_dbl);
// The complement 1 - I_y(a, b), evaluated from the smaller of y and
// 1 - y. Forming it as a difference loses every digit once I_y(a, b)
// rounds to 1, which happens for every complement below eps.
const T_partials_return Pn = internal::inc_beta_complement(
mukappa_dbl, kappa_mukappa_dbl, y_dbl, 1.0 - y_dbl);

ccdf_log += log(Pn);

const T_partials_return inv_Pn
= is_constant_all<T_y, T_loc, T_prec>::value ? 0 : inv(Pn);

if constexpr (is_autodiff_v<T_y>) {
partials<0>(ops_partials)[n] -= pow(1 - y_dbl, kappa_mukappa_dbl - 1)
* pow(y_dbl, mukappa_dbl - 1) * inv_Pn
/ betafunc_dbl;
// inc_beta_ddz is the same density, evaluated without the product of
// two powers over a beta function, each of which can underflow on
// its own.
partials<0>(ops_partials)[n]
-= inc_beta_ddz(mukappa_dbl, kappa_mukappa_dbl, y_dbl) * inv_Pn;
}

// the mu and kappa partials each need both shape partials
if constexpr (is_any_autodiff_v<T_loc, T_prec>) {
const auto [g1, g2] = grad_reg_inc_beta<GRAD_RIB::BOTH>(
mukappa_dbl, kappa_mukappa_dbl, y_dbl, digamma(mukappa_dbl),
digamma(kappa_mukappa_dbl), digamma(kappa_dbl), 0.0);
// The derivatives with respect to a = mu kappa and b = kappa - mu kappa,
// from the same argument as the value
T_partials_return dPn_da = 0;
T_partials_return dPn_db = 0;
if (y_dbl <= 0.5) {
// the derivatives of I_y(a, b), which are those of -Pn
const auto [g1, g2] = grad_reg_inc_beta<GRAD_RIB::BOTH>(
mukappa_dbl, kappa_mukappa_dbl, y_dbl, digamma(mukappa_dbl),
digamma(kappa_mukappa_dbl), digamma(kappa_dbl), 0.0);
dPn_da = -g1;
dPn_db = -g2;
} else {
// on the reflected arguments, the derivatives of Pn itself
const auto [g1, g2] = grad_reg_inc_beta<GRAD_RIB::BOTH>(
kappa_mukappa_dbl, mukappa_dbl, 1 - y_dbl,
digamma(kappa_mukappa_dbl), digamma(mukappa_dbl),
digamma(kappa_dbl), 0.0);
dPn_da = g2;
dPn_db = g1;
}
if constexpr (is_autodiff_v<T_loc>) {
partials<1>(ops_partials)[n] -= kappa_dbl * (g1 - g2) * inv_Pn;
partials<1>(ops_partials)[n] += kappa_dbl * (dPn_da - dPn_db) * inv_Pn;
}
if constexpr (is_autodiff_v<T_prec>) {
partials<2>(ops_partials)[n]
-= (g1 * mu_dbl + g2 * (1 - mu_dbl)) * inv_Pn;
+= (dPn_da * mu_dbl + dPn_db * (1 - mu_dbl)) * inv_Pn;
}
}
}
Expand Down
17 changes: 11 additions & 6 deletions stan/math/prim/prob/binomial_lccdf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <stan/math/prim/fun/beta.hpp>
#include <stan/math/prim/fun/constants.hpp>
#include <stan/math/prim/fun/inc_beta.hpp>
#include <stan/math/prim/fun/inc_beta_ddz.hpp>
#include <stan/math/prim/fun/log.hpp>
#include <stan/math/prim/fun/max_size.hpp>
#include <stan/math/prim/fun/scalar_seq_view.hpp>
Expand Down Expand Up @@ -89,16 +90,20 @@ inline return_type_t<T_prob> binomial_lccdf(const T_n& n, const T_N& N,
}

const T_partials_return theta_dbl = theta_vec.val(i);
const T_partials_return Pi
= 1.0 - inc_beta(N_dbl - n_dbl, n_dbl + 1, 1 - theta_dbl);
// The complement by the symmetry relation I_z(a, b) = I_{1-z}(b, a),
// which also removes the 1 - theta argument. Forming it as
// 1 - I_{1-theta}(N - n, n + 1) loses every digit once that rounds to
// 1, which happens for every complement below eps.
const T_partials_return Pi = inc_beta(n_dbl + 1, N_dbl - n_dbl, theta_dbl);

P += log(Pi);

if constexpr (is_autodiff_v<T_prob>) {
const T_partials_return denom = beta(N_dbl - n_dbl, n_dbl + 1) * Pi;
partials<0>(ops_partials)[i] += pow(theta_dbl, n_dbl)
* pow(1 - theta_dbl, N_dbl - n_dbl - 1)
/ denom;
// inc_beta_ddz is the same density, evaluated without the product of
// two powers over a beta function, each of which can underflow on
// its own.
partials<0>(ops_partials)[i]
+= inc_beta_ddz(n_dbl + 1, N_dbl - n_dbl, theta_dbl) / Pi;
}
}

Expand Down
47 changes: 36 additions & 11 deletions stan/math/prim/prob/neg_binomial_lccdf.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <stan/math/prim/fun/digamma.hpp>
#include <stan/math/prim/fun/grad_reg_inc_beta.hpp>
#include <stan/math/prim/fun/inc_beta.hpp>
#include <stan/math/prim/fun/inc_beta_ddz.hpp>
#include <stan/math/prim/fun/inv.hpp>
#include <stan/math/prim/fun/log.hpp>
#include <stan/math/prim/fun/max_size.hpp>
Expand Down Expand Up @@ -76,23 +77,47 @@ inline return_type_t<T_shape, T_inv_scale> neg_binomial_lccdf(
const auto inv_beta_p1 = inv(beta_dbl + 1);
const auto p_dbl = beta_dbl * inv_beta_p1;
const auto d_dbl = square(inv_beta_p1);
const auto Pi = 1.0 - inc_beta(alpha_dbl, n_dbl + 1.0, p_dbl);
const auto beta_func = beta(n_dbl + 1, alpha_dbl);
// The complement 1 - I_p(alpha, n + 1), evaluated from the smaller of
// p and 1 - p = 1 / (beta + 1), both formed without cancellation.
// Forming it as a difference loses every digit once I_p(alpha, n + 1)
// rounds to 1, which happens for every complement below eps.
const auto Pi = internal::inc_beta_complement(alpha_dbl, n_dbl + 1.0, p_dbl,
inv_beta_p1);

P += log(Pi);

// alpha is the first shape of inc_beta, so only its partial is needed
// Only the alpha partial is needed, from the same argument as the value
if constexpr (is_autodiff_v<T_shape>) {
partials<0>(ops_partials)[i]
-= grad_reg_inc_beta<GRAD_RIB::G1>(
alpha_dbl, n_dbl + 1, p_dbl, digamma(alpha_dbl), 0.0,
digamma(n_dbl + alpha_dbl + 1), 0.0)
/ Pi;
const auto digamma_alpha = digamma(alpha_dbl);
const auto digamma_sum = digamma(n_dbl + alpha_dbl + 1);
if (p_dbl <= 0.5) {
// alpha is the first shape of I_p(alpha, n + 1), whose derivative
// is that of -Pi
partials<0>(ops_partials)[i]
-= grad_reg_inc_beta<GRAD_RIB::G1>(alpha_dbl, n_dbl + 1, p_dbl,
digamma_alpha, 0.0, digamma_sum,
0.0)
/ Pi;
} else {
// on the reflected arguments alpha is the second shape, and the
// derivative is that of Pi itself
partials<0>(ops_partials)[i]
+= grad_reg_inc_beta<GRAD_RIB::G2>(n_dbl + 1, alpha_dbl,
inv_beta_p1, 0.0, digamma_alpha,
digamma_sum, 0.0)
/ Pi;
}
}
if constexpr (is_autodiff_v<T_inv_scale>) {
partials<1>(ops_partials)[i] -= d_dbl * pow(1 - p_dbl, n_dbl)
* pow(p_dbl, alpha_dbl - 1)
/ (beta_func * Pi);
// inc_beta_ddz is the same density, evaluated without the product of
// two powers over a beta function, each of which can underflow on
// its own. The density at p equals the reflected density at 1 - p;
// it is evaluated at whichever of the two is at most 1/2, because
// inc_beta_ddz forms the complement of its argument internally.
const T_partials_return density
= p_dbl <= 0.5 ? inc_beta_ddz(alpha_dbl, n_dbl + 1.0, p_dbl)
: inc_beta_ddz(n_dbl + 1.0, alpha_dbl, inv_beta_p1);
partials<1>(ops_partials)[i] -= d_dbl * density / Pi;
}
}

Expand Down
Loading
Loading