Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
99 changes: 92 additions & 7 deletions src/distrs/beta.jl
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,97 @@ function betainvccdf(α::Real, β::Real, p::Real)
return last(beta_inc_inv(β, α, p))
end

# Rmath implementations
function betainvlogcdf(α::Real, β::Real, lq::Real)
T = float(Base.promote_typeof(α, β, lq))
return convert(T, Rmath.qbeta(lq, α, β, true, true))
# Newton iteration in log-space for inverting log-CDF.
# Solves betalogcdf(α, β, x) = lp for x.
# Uses the identity: d/dx log(F(x)) = f(x)/F(x) = exp(logpdf - logcdf)
# Newton step: x_{n+1} = x_n - (logcdf(x_n) - lp) / exp(logpdf(x_n) - logcdf(x_n))
# = x_n - (logcdf(x_n) - lp) * exp(logcdf(x_n) - logpdf(x_n))
function _betainvlogcdf(α::Float64, β::Float64, lp::Float64)
if lp > -1
# Moderate lp: use -expm1(lp) = 1-exp(lp) for accuracy near lp=0
return Float64(betainvccdf(α, β, -expm1(lp)))
end

# Use Newton iteration in log-space.
# Initial estimate from small-x asymptotic: I_x(α,β) ≈ x^α / (α·B(α,β))
# ⟹ log(I_x) ≈ α·log(x) - log(α) - logbeta(α,β)
# ⟹ x₀ ≈ exp((lp + log(α) + logbeta(α,β)) / α)
logx = (lp + log(α) + logbeta(α, β)) / α
if logx < log(nextfloat(0.0))
return 0.0 # answer underflows Float64
end
x = exp(logx)
x = min(x, 1.0 - eps(1.0))

for _ in 1:200
lcdf = Float64(betalogcdf(α, β, x))
residual = lcdf - lp
if abs(residual) <= 2 * eps(max(abs(lp), 1.0))
break
end
lpdf = Float64(betalogpdf(α, β, x))
# Newton step: Δx = residual * F(x)/f(x) = residual * exp(logcdf - logpdf)
dx = residual * exp(lcdf - lpdf)
# Guard: don't let x go negative or jump too far
x_new = x - clamp(dx, -x / 2, x / 2)
x_new = clamp(x_new, nextfloat(0.0), 1.0 - eps(1.0))
if x_new == x
break
end
x = x_new
end

return x
end

function betainvlogcdf(α::Real, β::Real, lp::Real)
T = float(Base.promote_typeof(α, β, lp))
_lp = Float64(lp)

if isnan(_lp) || _lp > 0
return convert(T, NaN)
elseif _lp == 0
return one(T)
elseif isinf(_lp) # -Inf
return zero(T)
end

# Handle degenerate cases
_α = Float64(α); _β = Float64(β)
Comment on lines +149 to +160

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These Float64 calls will error for Duals and (arguably even worse) lead to silently incorrect results for higher-precision numbers. Ideally, we should just avoid the Float64 conversions in this function and instead ensure that the functions called here are written generically enough to support all common use cases. Then the responsibility for potentially handling other or higher-precision Reals would be off-loaded to those.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should probably convert the PRs to drafts. The current ones are mostly how Claude Code prepared them and I need to go through the code in detail.

I agree that a function like this should probably avoid conversion to Float64, but most of the functions containing the actual algorithms will necessarily be restricted to Float64 because the number of terms in the various approximations has been determined based on the precision.

if iszero(_α) && _β > 0
return zero(T)
elseif iszero(_β) && _α > 0
return _lp > log1p(-1.0) ? one(T) : zero(T)
end

return convert(T, _betainvlogcdf(_α, _β, _lp))
end
function betainvlogccdf(α::Real, β::Real, lq::Real)
T = float(Base.promote_typeof(α, β, lq))
return convert(T, Rmath.qbeta(lq, α, β, false, true))

function betainvlogccdf(α::Real, β::Real, lp::Real)
T = float(Base.promote_typeof(α, β, lp))
_lp = Float64(lp)

if isnan(_lp) || _lp > 0
return convert(T, NaN)
elseif _lp == 0
return zero(T)
elseif isinf(_lp) # -Inf
return one(T)
end

# Handle degenerate cases
_α = Float64(α); _β = Float64(β)
if iszero(_α) && _β > 0
return _lp > log1p(-1.0) ? zero(T) : one(T)
elseif iszero(_β) && _α > 0
return one(T)
end

# For moderate lp (near 0): use -expm1(lp) = 1-exp(lp) for accuracy
if _lp > -1
return convert(T, Float64(betainvcdf(_α, _β, -expm1(_lp))))
end

# For very negative lp: betainvlogccdf(α, β, lp) = 1 - betainvlogcdf(β, α, lp)
return convert(T, 1.0 - _betainvlogcdf(_β, _α, _lp))
end
123 changes: 116 additions & 7 deletions src/distrs/gamma.jl
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,121 @@ function gammainvccdf(k::Real, θ::Real, p::Real)
return _θ * gamma_inc_inv(_k, 1 - _p, _p)
end

# Rmath implementations
function gammainvlogcdf(k::Real, θ::Real, lq::Real)
T = float(Base.promote_typeof(k, θ, lq))
return convert(T, Rmath.qgamma(lq, k, θ, true, true))
# Newton iteration in log-space for inverting log-CDF.
# Solves gammalogcdf(k, θ, x) = lp for x.
function _gammainvlogcdf(k::Float64, θ::Float64, lp::Float64)
if lp > -1
# Use -expm1(lp) = 1-exp(lp) for accuracy near lp=0
return Float64(gammainvccdf(k, θ, -expm1(lp)))
end

# Use Newton iteration in log-space.
# Initial estimate from small-x asymptotic:
# P(k, x/θ) ≈ (x/θ)^k / (k·Γ(k)) for small x/θ
# ⟹ x₀/θ ≈ exp((lp + log(k) + loggamma(k)) / k)
logz = (lp + log(k) + loggamma(k)) / k
if logz + log(θ) < log(nextfloat(0.0))
return 0.0 # answer underflows Float64
end
z = exp(logz)
x = z * θ
x = max(x, nextfloat(0.0))

for _ in 1:200
lcdf = Float64(gammalogcdf(k, θ, x))
residual = lcdf - lp
if abs(residual) <= 2 * eps(max(abs(lp), 1.0))
break
end
lpdf = Float64(gammalogpdf(k, θ, x))
# Newton step: Δx = residual * F(x)/f(x) = residual * exp(logcdf - logpdf)
dx = residual * exp(lcdf - lpdf)
# Guard: don't let x go negative or jump too far
x_new = x - clamp(dx, -x / 2, x / 2)
x_new = max(x_new, nextfloat(0.0))
if x_new == x
break
end
x = x_new
end

return x
end

# Newton iteration in log-space for inverting log-CCDF.
# Solves gammalogccdf(k, θ, x) = lp for x.
function _gammainvlogccdf(k::Float64, θ::Float64, lp::Float64)
if lp > -1
# Use -expm1(lp) = 1-exp(lp) for accuracy near lp=0
return Float64(gammainvcdf(k, θ, -expm1(lp)))
end

# Extreme right tail: initial estimate
# Q(k, x/θ) ≈ (x/θ)^(k-1)·exp(-x/θ)/Γ(k) for large x/θ
# ⟹ log(Q) ≈ (k-1)·log(x/θ) - x/θ - loggamma(k)
# For large x/θ, the -x/θ term dominates: x₀/θ ≈ -lp - loggamma(k)
z = max(-lp - loggamma(k), 1.0)
x = z * θ

for _ in 1:200
lccdf = Float64(gammalogccdf(k, θ, x))
residual = lccdf - lp
if abs(residual) <= 2 * eps(max(abs(lp), 1.0))
break
end
lpdf = Float64(gammalogpdf(k, θ, x))
# d/dx log(ccdf(x)) = -f(x)/ccdf(x) = -exp(logpdf - logccdf)
# Newton step: Δx = (logccdf - lp) / (-exp(logpdf - logccdf))
# = -(logccdf - lp) * exp(logccdf - logpdf)
dx = -residual * exp(lccdf - lpdf)
# Guard: don't overshoot
x_new = x - clamp(dx, -x, x / 2)
x_new = max(x_new, nextfloat(0.0))
if x_new == x
break
end
x = x_new
end

return x
end
function gammainvlogccdf(k::Real, θ::Real, lq::Real)
T = float(Base.promote_typeof(k, θ, lq))
return convert(T, Rmath.qgamma(lq, k, θ, false, true))

function gammainvlogcdf(k::Real, θ::Real, lp::Real)
T = float(Base.promote_typeof(k, θ, lp))
_lp = Float64(lp)

if isnan(_lp) || _lp > 0
return convert(T, NaN)
elseif _lp == 0
return convert(T, Inf)
elseif isinf(_lp) # -Inf
return zero(T)
end

_k = Float64(k); _θ = Float64(θ)
if iszero(_k)
return _lp >= 0 ? zero(T) : convert(T, NaN)
end

return convert(T, _gammainvlogcdf(_k, _θ, _lp))
end

function gammainvlogccdf(k::Real, θ::Real, lp::Real)
T = float(Base.promote_typeof(k, θ, lp))
_lp = Float64(lp)

if isnan(_lp) || _lp > 0
return convert(T, NaN)
elseif _lp == 0
return zero(T)
elseif isinf(_lp) # -Inf
return convert(T, Inf)
end

_k = Float64(k); _θ = Float64(θ)
if iszero(_k)
return _lp >= 0 ? zero(T) : convert(T, NaN)
end

return convert(T, _gammainvlogccdf(_k, _θ, _lp))
end
Loading