Add TruncatedGamma distributions - #2257
Conversation
Benchmark reportthis PR + run time: 1 faster
compile time: unchanged across 32 benchmarksSignificant changes (1) ─────── run time ─────── ───── compile time ─────
benchmark baseline this PR Δ baseline this PR Δ
──────────────────────────────────────────────────────────────────────────────
+ categorical_log_prob 2.3 ms 2.2 ms -7.0% 71.2 ms 68.6 ms -3.8%Red is slower, green is faster; a row is coloured by the worse of its two columns. A delta in parentheses cleared the threshold on a measurement below the resolution floor, so it is shown without being called a change. † marks a benchmark that could not be compared — see below. Full results
|
| baseline | this PR | |
|---|---|---|
| ref | master |
truncated-gamma |
| commit | eda5d6dc |
3f92d929 |
| numpyro | 0.21.0 | 0.21.0 |
| jax | 0.11.1 | 0.11.1 |
| backend | cpu | cpu |
| python | 3.14.7 | 3.14.7 |
Runner: Linux-6.17.0-1022-azure-x86_64-with-glibc2.39, 4 CPUs.
Produced by this benchmark run.
|
Overall LGTM :) I did an assisted review with Claude and here are the findings :) 1.
|
Add LeftTruncatedGamma, RightTruncatedGamma and TwoSidedTruncatedGamma, dispatched by TruncatedGamma(concentration, rate, low=, high=), following the structure outlined in pyro-ppl#969. Supersedes the inactive pyro-ppl#1187. The two-sided normalizer switches to the upper-tail form when the interval lies above the median: the lower-tail difference F(high) - F(low) is exactly zero in single precision for Gamma(2, 1) on [30, 40]. Adds gammainccinv to distributions/util.py, wrapping tfp.math.igammacinv.
- `_partial_moment` re-decides the tail switch at `concentration + order`. Gamma(alpha+k) has a larger median, so an interval above the base median can sit deep in the lower tail at the shifted order, where the upper-tail form is the one that cancels. Reusing the order-zero branch collapsed the second moment to zero and produced a negative variance. - `icdf` bisects the cdf instead of calling an inverse incomplete gamma, which saturates below a tail probability of ~3e-8 and returned draws outside the support once the retained mass got small. A Newton step from the bracketed root supplies the implicit derivative, so sampling stays differentiable. This removes the need for the `gammainccinv` wrapper, so `util.py` is untouched. - `mean` and `variance` now return nan for an underflowed normalizer, matching `cdf`; documented why that differs from `log_prob` returning -inf. - Documented the float32 cancellation in `E[X^2] - E[X]^2` for narrow intervals far from the origin. - Fixed two line-wrapped reST cross-references, gave the base-distribution assert a message pointing at TruncatedGamma, dropped a test assertion that measured scipy's accuracy rather than ours, and restored the file's left/right/two-sided ordering in batch_util.
Narrow the new classes' returns from ArrayLike to Array, take Optional keys in sample with the accompanying assert, cast constraint bounds to NumLike, and declare _support at class level — matching what pyro-ppl#2206 applied to the existing truncated classes. The generic test_output_is_array added by that PR also caught a broadcasting bug in the bisection helpers: promote_shapes can leave low and high at size-1 dims while the cdf carries the full batch shape, so the fori_loop carry changed shape mid-loop for batched parameters. The helpers now take the target shape explicitly, broadcast from the quantile shape and the distribution's batch shape.
50933a8 to
3f92d92
Compare
Closes #969. Supersedes #1187, inactive since 2022 — same approach, credit to @quattro.
Summary
LeftTruncatedGamma,RightTruncatedGammaandTwoSidedTruncatedGamma, dispatchedby
TruncatedGamma(concentration, rate, *, low=None, high=None), following the structure@fehiepsi outlined in Truncated Gamma #969
gammainccinvtodistributions/util.py, wrappingtfp.math.igammacinvlike theexisting
gammaincinv; needed foricdfon the upper-tail branchTwoSidedTruncatedGammanormalizes withQ(a, λ·low) − Q(a, λ·high)when the intervallies above the median, and switches
cdf,icdf,meanandvarianceto match. Thelower-tail form
F(high) − F(low)is exactly zero in single precision forGamma(2, 1)on
[30, 40], which would makelog_prob+infE[X^k] = (a)_k / λ^k · [P(a+k, λ·high) − P(a+k, λ·low)] / Zjax.grad(lambda a: gammainc(a, jnp.inf))isnanand would break the gradient w.r.t.concentrationfor left truncationTwo limits, documented in the docstrings:
log_probreturns-infrather than+infonce the normalizer underflows, and
icdfreturnsnanpast the point wheretfp.math.igammainvsaturates, whichGamma.icdfalso hits on master. A bisectionfallback would lift the second, here or as a follow-up.
Tests
test/test_distributions.pycoveringlog_probagainst scipy,normalization and moments against quadrature,
cdf/icdfround trip, KS tests onsamples, batch shapes, both limits, the dispatcher, gradients and NUTS recovery
CONTINUOUSparametrization and inbatch_util.vmap_overmake lintpasses, as dotest_distributions.py,test_gof.py,test_distributions_util.py,test_constraints.py,test_transforms.pyandtest_pickle.py. (test_mcmc.py::test_beta_bernoulli_x64[SA]fails for me on mastertoo, unrelated to this change.)
Dependencies
None new;
tensorflow_probabilityis already optional and used bygammaincinv.