Repository navigation
Conversation
This was referenced Oct 8, 2026
SteveBronder
added this pull request to stack #3462
October 14, 2026 19:43
avehtari
marked this pull request as draft
October 15, 2026 14:14
Collaborator
|
It’s hard to know the right place to do this, but thank you, @avehtari . I was on vacation and came back to see that almost every issue I’ve submitted to Stan is suddenly fixed with one or another of your PRs. I thought I’d try some of these easier ones to learn the system better before diving into my modified-Bessel wish…but now I guess modified Bessel is all that’s left! |
Member
Author
|
@jaburgoyne thanks for creating those issues, they did help to prioritize the work. I could do that modified Bessel, too, but happy to leave it also for you so you can learn more about Stan |
avehtari
force-pushed
the
stable-beta-lccdf
branch
from
October 22, 2026 09:03
688eda1 to
75d8bfe
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2031. Fixes #2923. Fixes #3234. Fixes #3377. Fixes other functions. Partially addresses #2790.
Currently this PR is based on stable-inc-beta branch PR #3409, so that PR should be merged first.
This improves
beta_lccdf,beta_proportion_lccdf,binomial_lccdf,neg_binomial_lccdf,neg_binomial_2_lccdf,student_t_cdf,student_t_lcdf, andstudent_t_lccdf.This PR was Claude assisted
EDIT 2026-10-08: Added three more commits to fix more issues in functions touched by this PR
EDIT 2026-10-22: Rebased onto the current head of #3409, which now includes the reverse-mode and one-sided
grad_reg_inc_betafrom @SteveBronder. The seven commits keep their content; the shape gradients now request only the derivatives that each function needs. See the new Speed section.Summary
Seven tail functions form the complement of the regularized incomplete beta function as
1 - inc_beta(a, b, x)in linear space:beta_lccdf,beta_proportion_lccdf,neg_binomial_lccdf,binomial_lccdf, and theq >= 2branch ofstudent_t_cdf,student_t_lcdfandstudent_t_lccdf. Onceinc_betarounds to 1, the complement is exactly 0, its log is-inf, and the gradients areinforNaN. This happens for every tail probability beloweps, and thelccdffunctions are used for such tails. Theq < 2branch of the three Student t functions has a second subtraction of the same kind,1 - r.This PR evaluates each complement without the subtraction. A new internal function,
internal::inc_beta_complement, evaluates it from whichever ofxand1 - xis at most 1/2.binomial_lccdfneeds no helper and uses the symmetry relation directly. The shape gradients come fromgrad_reg_inc_betaon the same argument as the value. The density factors, whose powers underflow separately and gave0 / 0, are replaced byinc_beta_ddz; inneg_binomial_lccdfthe density is also evaluated at the argument at most 1/2. In the Student tq < 2branch,1 - ris formed without subtraction.Which part of the PR fixes which issue:
beta_lccdf(0.865169 | 1, 19)is-infbeta_lccdfstudent_t_cdfp-values of exactly 0q >= 2branchstudent_t_cdfis 0 or 1 for large|t|1 - rin the Student tq < 2branchneg_binomial_lccdfis-inffor largebeta1 / (beta + 1), and the density factor of thebetagradientDBL_MIN; below thatlog_inc_betais neededDepends on #3409: the shape gradients go through the repaired
grad_reg_inc_beta. The value changes do not depend on it. This PR is based on thestable-inc-betabranch of #3409, so its diff shows only its own seven commits. I will retarget it todevelopwhen #3409 merges.Fixes #3377. On
develop, the reporter's casebeta_lccdf(0.865169 | 1, 19)returns-infwith infinite gradients. On this branch it returns-38.0709295957162, which equals the closed form19 * log1m(y)to all printed digits. The three gradients match a 50-digit reference:d/dy = -140.917148134 = -19 / (1 - y),d/dalpha = 3.4106443951,d/dbeta = -2.00373313662. The cause is the defect described below:(1 - y)^19 = 3e-17is beloweps, so1 - inc_beta(...)is exactly 0.Fixes #3234.
student_t_cdfreturned exactly 0 for p-values below about 1e-16 wheneverq = df / t^2 >= 2, the branch that formed1 - inc_beta(...). At the six rows of the report, this branch matches a 40-digit reference to all printed digits. For example,2 * student_t_cdf(-9.466995 | 196.9309, 0, 1)is9.173253e-18, wheredevelopgives0.Fixes #2923. In the
q < 2branch,r = 1 / (1 + q)rounds to 1 onceqis beloweps, so1 - ris 0 and the tail probability is 0. The reported casestudent_t_cdf(-1e8 | 0.1, 0, 1)is 0 ondevelopfor a true 0.0661503218, and its mirror at1e8is 1 for a true 0.9338496782. Before that point,1 - rloseseps / qrelative: 1e-13 at|t| = 10withnu = 0.1. Everynufails once|t|exceeds about6.7e7 sqrt(nu); the Cauchystudent_t_lcdf(-1e10 | 1, 0, 1)is-inffor a true -24.17. Over the reported table (nu = 0.1,|t|from 1 to 1e29, both signs), this branch is within 4e-16 of a 60-digit reference in the value and within 5e-15 in they,nuandsigmagradients.Fixes #2031. The reported case
neg_binomial_lccdf(0 | 1, 1e18)is-infondevelopfor a true -41.4465316739, which islog1m_exp(-alpha * log1p(1 / beta)). On this branch the value and both gradients are correct to 1e-15. Forn >= 1, thebetagradient also needs the density factor at1 / (beta + 1). Atp = beta / (beta + 1)that factor is exactly 0 onceprounds to 1 (frombetanear 1e16), where the true gradient is about-(n + 1) / beta; atbeta = 1e8it loses 8 digits.beta_lccdf(0.9 | 50, 50)-inf, gradients±infbeta_lccdf(0.758 | 352, 590)-inf, gradients±infbeta_proportion_lccdf(0.758 | 0.4, 900)-inf, gradients±infneg_binomial_lccdf(50 | 2, 99)-inf, gradientsNaNneg_binomial_lccdf(1 | 2, 1e18)-inf, gradientsinfbinomial_lccdf(500 | 1000, 0.2)-inf, gradientNaNstudent_t_lccdf(10 | 1000, 0, 1)-inf, gradients∓infstudent_t_lcdf(-10 | 1000, 0, 1)-inf, gradients∓infstudent_t_cdf(-10 | 1000, 0, 1)NaNstudent_t_cdf(-1e8 | 0.1, 0, 1)NaNstudent_t_lcdf(-1e10 | 1, 0, 1)-inf, gradientsinfThe defect on
developThis section describes
develop. The reference is mpmath at 50 or 60 digits, with each value computed by two independent routes. For the beta family, the reference computes the complement on the small side of the symmetry relation. For Student t, the tail is0.5 I_{nu / (nu + t^2)}(nu / 2, 1 / 2), differentiated numerically. I confirmed the numbers by compiling and running the real headers.1. The complement cancels.
Pn = 1.0 - inc_beta(alpha, beta, y)is 0 wheneverinc_betareturns exactly 1. That happens whenever the true complement is beloweps = 2.2e-16, that is, whenever the truelccdfis belowlog(eps) = -36.7.beta_lccdf(0.9 | 50, 50), with both shapes 50 andyat 0.9, returns-inffor a true-54.1. A Student t with 1000 degrees of freedom at 10 standard deviations returns-inffor a true-50.8. None of the parameter sets in the table above is unusual; the only condition is a tail probability below1e-16.2. The gradients follow.
inv_Pnisinf, so the shape andypartials areinfwhere the density factor is finite, andNaNwhere it is not.student_t_cdf(-10 | 1000, 0, 1)is the worst case: its value8.3e-23is representable and correct, but both gradients areNaN.3. The density factor underflows on its own. In
binomial_lccdf,pow(theta, n) * pow(1 - theta, N - n - 1) / beta(N - n, n + 1)is a ratio of quantities that each underflow long before the ratio does; the other files have the equivalent expression. After the repair of the complement,binomial_lccdf(500 | 1000, 0.2)has a correct value but still aNaNgradient, becausepow(0.2, 500)is 0 andbeta(500, 501)is 0. Inneg_binomial_lccdf, the factorpow(1 - p, n)is formed fromp = beta / (beta + 1), which rounds to 1 for largebeta.4.
1 - rin the Student tq < 2branch. The branch forms1 - rby subtraction, withr = 1 / (1 + q)andq = nu / t^2. See #2923 above.Where
inc_betaunderflows to 0, the complement is 1 and the old code is right. For example,neg_binomial_lccdf(3 | 900, 0.02)returns 0 with zero gradients on both trees, and that is the correct answer.What this PR does
The complement from the argument at most 1/2. Boost's
ibetaforms1 - xfrom its argument, so an argument close to 1 loses relative precision in1 - x. The symmetry relation1 - I_x(a, b) = I_{1-x}(b, a)removes the cancellation of a tail beloweps, but it passes1 - x, which is close to 1 wherexis small. The newinternal::inc_beta_complement(a, b, x, one_m_x)inprim/fun/inc_beta.hpptakes bothxand1 - x, and calls Boost with the one that is at most 1/2:x > 1/2:inc_beta(b, a, 1 - x), by the symmetry relation;xbelow the meana / (a + b), whileinc_beta(a, b, x)is at most 1/2:1 - inc_beta(a, b, x), which then has full precision;ibetac(a, b, x).The middle case is necessary because, for the arcsine case
a = b = 1/2(the Cauchy,nu = 1), Boost'sibetacevaluatesasin(sqrt(1 - x)), which loses digits for smallx. For an autodiffT_partials_return, which occurs only in second-order autodiff, the function uses the symmetry relation throughinc_beta.The shape gradients come from
grad_reg_inc_betaon the same argument. Forx <= 1/2, the call is on(a, b, x), as ondevelop, and its outputs are the negatives of the derivatives of the complement. Forx > 1/2, the call is on(b, a, 1 - x), and its two outputs are the derivatives of the complement itself with respect tobanda. Above the mean,grad_reg_inc_betaapplies the symmetry relation internally, so on an argument at most 1/2 it keeps its relative precision in the deep tail too.beta_lccdf:x = y,1 - x = 1 - y.beta_proportion_lccdf: the same witha = mu kappaandb = kappa - mu kappa; the chain rule tomuandkappadoes not change.neg_binomial_lccdf:x = p = beta / (beta + 1)and1 - x = 1 / (beta + 1), which the function already has asinv_beta_p1; both are formed without cancellation.binomial_lccdf: the complement isinc_beta(n + 1, N - n, theta). Boost forms1 - thetaexactly fromtheta, so the function needs no helper. The derivative is closed form.neg_binomial_2_lccdfdoes not change: it returnsneg_binomial_lccdf(n, phi, phi / mu), so it gets every change ofneg_binomial_lccdf.student_t_cdf,student_t_lcdf,student_t_lccdf: theq >= 2branch takesz = 1 - I_r(1/2, nu/2)from the helper, withr <= 1/3; its gradient code is the same as ondevelop. Theq < 2branch forms1 - ras1 / (1 + t^2 / nu), which has no cancellation and is 1 att = 0. The two branches compute the same quantity, and a test checks continuity acrossq = 2.The density factor.
beta_lccdf,beta_proportion_lccdf,neg_binomial_lccdfandbinomial_lccdfuseinc_beta_ddzfor the density, not the product of powers over a beta function. It is the same quantity; fordoubleit is Boost'sibeta_derivative, which scales internally.beta_cdfalready used it, so this also removes a duplicated expression.ibeta_derivativealso forms1 - xfrom its argument, and it returns 0 atx == 1when its second shape is above 1. Forp > 1/2,neg_binomial_lccdftherefore evaluates the density atpas the equal reflected density at1 - p. The three Student t files keep theird_ibeta, which does not underflow in their parameter range.The bound
y = 1.beta_lccdfandbeta_proportion_lccdfreturnnegative_infinity()aty >= 1before the loop.neg_binomial_lccdfandbinomial_lccdfalready have this explicit bound handling (see Testing).After the change, the
-infthreshold moves fromepstoDBL_MIN, that is, from a tail probability ofe^-36.7toe^-709.Accuracy after the change
Every value and every gradient in the table above matches the reference to all printed digits. The remaining relative errors are 1e-15 to 1e-10, which is the accuracy of Boost's
inc_betaand of the gradient root; this PR adds no error of its own.Where
developwas correct, the change keeps its accuracy. I compared 117 points point by point, in the bulk (|t|down to 1e-7,ydown to 1e-10,betadown to 1e-9) and in the tails. At every point wheredevelopis finite, no value or gradient is more than 4 times less accurate than ondevelopabove a relative error of 1e-14. For example, thenugradient ofstudent_t_cdf(-1e-7 | 1, 0, 1)is within 1e-15, and thealphagradient ofbeta_lccdf(1e-10 | 0.5, 3)is within 6e-16.Continuity across the Student t branch seam: at
y = 1.9999, 2.0, 2.0001withnu = 8, the value and thenugradient are linear inyto 1e-8 on both sides ofq = 2.Speed
AMD EPYC 9965, g++ 11.5,
-O3, one core, two rounds within 1 %.N = 1000observations with shared shapes, value plus all gradients, ns per observation. Thefvar<var>rows add the tangent and its reverse sweep, as inhessian(). Most of the gain comes from the gradient roots of #3409, which this PR is based on; the last column isolates the effect of the reverse-mode and one-sided roots from @SteveBronder on this PR's functions.developbeta_lccdfbeta_lccdf,alphaonlybeta_lccdfbeta_proportion_lccdfneg_binomial_lccdfneg_binomial_2_lccdfstudent_t_lccdfnu3student_t_lccdfnu30student_t_cdfnu30beta_lccdf,fvar<var>neg_binomial_lccdf,fvar<var>student_t_lccdf,fvar<var>nu3binomial_lccdfdoes not call the gradient root and is not in the table: on this griddevelopreturns-infat many points, so its time is no baseline.Testing
Two new files have fixed references from the high-precision computation. Finite differences cannot see these defects: at the failing points the finite difference of
-infisNaN, andexpect_adwould report agreement.rev/prob/beta_ccdf_log_test.cpp, 8 cases:beta_lccdfat three points beloweps, one control, and four small-ypoints (two in the bulk, two with the complement beloweps);beta_proportion_lccdfat one deep-tail point and two small-ypoints;neg_binomial_lccdfat two deep-tail points, six large-betapoints (Numerical issues in neg_binomial_lccdf if beta is very large #2031: four withbetafrom 1e8 to 1e18, one control on each side ofbeta = 1) and four points withbeta < 1;binomial_lccdfwith its gradient.rev/prob/student_t_ccdf_log_test.cpp, 6 cases:student_t_lccdfat five points in both branches; fourstudent_t_lccdfpoints at larget(Student T CDF precision for large values #2923); the reported Student T CDF precision for large values #2923 case, its mirror and the Cauchylcdfat-1e10, with the value and all three gradients; three points neart = 0with the value and all three gradients (nu = 1,nu = 30); the branch-seam continuity check;student_t_lcdfandstudent_t_cdfat the point where the gradients of the cdf wereNaN.Regression swap: with the
developversions of the changed headers installed under these two files, 11 of the 14 cases fail. Three pass: thebinomial_lccdfcase (its header was not swapped in this run), the seam-continuity check, and the near-t = 0Student t case, wheredevelopis accurate. With the headers of this branch, all 14 pass, before and after the swap.Full suites, all passing on the rebased head: the root
inc_betaunit tests inprim,rev,fwdandmix; the unit prob tests ofbeta,beta_proportion,binomial,student_tandneg_binomial*(which includesneg_binomial_2); and thetest/probgeneratedcdf,lcdfandlccdftests ofbeta,beta_proportion,student_t,neg_binomial,neg_binomial_2andbinomial(185 programs). Tested with g++ 11.5 on the #3409 head3fd3fe2f6awith this PR's commits applied.The generated tests found one defect that the fixed-reference tests did not. For
double,inc_beta_ddzis Boost'sibeta_derivative, which throws on overflow where the oldpowproduct returnedinf. Aty = 1with a second shape below 1 the density is infinite, and the generatedUpperBoundcase with an autodiffyreaches that point. This is the reason for the explicit bound aty = 1. The value at the bound does not change,-inf; theypartial there is 0 instead ofinforNaN.Known limits, not addressed here
DBL_MINthe complement is not representable.neg_binomial_lccdf(200 | 2, 99)andbinomial_lccdf(500 | 1000, 0.01)are still-inf, for true values -920.34 and -1622.73. A repair needslog_inc_beta, the incomplete beta function in log space, which the tree does not have. The same function would repair the mirror defect inbeta_lcdfand the related functions, whereinc_betaunderflows to 0 foryfar below the mean. That is a separate PR: a new function with its own overloads and tests.t^2overflows above|t| = 1.3e154; that range would need a log form.lccdfvalue that is the log of a probability that rounds to 1 is returned as 0, as ondevelop:beta_proportion_lccdf(1e-8 | 0.3, 10)is 0 for a true -8.4e-23. The gradients there are correct.beta_cdf,neg_binomial_cdfandneg_binomial_2_cdfare not affected: they return the probability itself, and there 0 is the correct rounding.Side Effects
No
Release notes
More stable and faster
beta_lccdf,beta_proportion_lccdf,binomial_lccdf,neg_binomial_lccdf,neg_binomial_2_lccdf,student_t_cdf,student_t_lcdf, andstudent_t_lccdf.Checklist
Copyright holder: Aalto University
The copyright holder is typically you or your assignee, such as a university or company. By submitting this pull request, the copyright holder is agreeing to the license the submitted work under the following licenses:
- Code: BSD 3-clause (https://opensource.org/licenses/BSD-3-Clause)
- Documentation: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
the basic tests are passing
./runTests.py test/unit)make test-headers)make test-math-dependencies)make doxygen)make cpplint)the code is written in idiomatic C++ and changes are documented in the doxygen
the new changes are tested