Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5b96b87
Add logmeanexp/logvarexp/logstdexp implementations
Copilot Jun 3, 2026
aa991ca
Fix logstats iterator implementation and tests
Copilot Jun 3, 2026
3930a78
Add PR77-focused logstatsexp tests
Copilot Jun 3, 2026
0431c84
Fix PR review issues in logstatsexp
Copilot Jun 3, 2026
6c42836
Fix unresolved review comments: remove dead _count_elements, update d…
Copilot Jun 3, 2026
7f14412
Use explicit multiplication in logstatsexp
Copilot Jun 3, 2026
28fc64e
fix iterator real-input validation for log variance
Copilot Jun 3, 2026
7a886c1
refactor iterator counting and simplify normalization logic
Copilot Jun 4, 2026
e760614
Simplify logstatsexp; add logmeanexp_and_logvarexp / logmeanexp_and_l…
cossio Jun 4, 2026
3842a7e
Use the numerically stable centered variance; fix min-Julia CI
cossio Jun 4, 2026
6789ee6
Use scalar `/ 2` instead of broadcast `./ 2` for std
cossio Jun 4, 2026
8c421b5
Potential fix for pull request finding
cossio Jun 4, 2026
73f2e55
Potential fix for pull request finding
cossio Jun 4, 2026
9fa4627
Fuse dims variance into logsumexp; repair empty-array testset
cossio Jun 4, 2026
dadf0d9
Fix dims-reduction correctness bugs found in review; drop lazy wrapper
cossio Jun 4, 2026
695b2ef
Simplify: dedup the one-pass reduction loops
cossio Jun 4, 2026
23f5dc4
Make logmeanexp single-pass; fix single-use HasLength iterators
cossio Jun 4, 2026
8fafe93
Apply suggestion from @devmotion
cossio Jun 8, 2026
c9439aa
Apply suggestion from @devmotion
cossio Jun 8, 2026
1be77c1
Apply suggestion from @devmotion
cossio Jun 8, 2026
691e34e
Clean up logstatsexp per review feedback
cossio Jun 8, 2026
09a8da1
Add in-place logmeanexp!/logvarexp!; dims reductions allocate only th…
cossio Jun 8, 2026
5601c23
Use keyword-argument punning in logstatsexp tests
cossio Jun 8, 2026
9669d64
Remove combined logmeanexp_and_* functions
cossio Jun 8, 2026
8de5fea
simplifications
cossio Jun 8, 2026
1fff6ba
comment
cossio Jun 8, 2026
9983e1f
simplify more
cossio Jun 9, 2026
44ddd37
rm AbstractRange redundant
cossio Jun 9, 2026
ef4756d
_finish_logvar
cossio Jun 9, 2026
172d5b8
chainrules
cossio Jun 9, 2026
f1f82da
no iterator support in logvarexp
cossio Jun 9, 2026
0a900f0
logstdexp! tests
cossio Jun 9, 2026
850df33
nits
cossio Jun 9, 2026
b245d7d
simplify pullback
cossio Jun 9, 2026
0ce6af4
rm _logsumexp_count
cossio Jun 10, 2026
e6cc62a
Simplify oftype usage and add return-type tests
cossio Jun 21, 2026
82d59b1
comments
cossio Jun 21, 2026
69f455b
Shorten test comments
cossio Jun 21, 2026
310aecb
Stop tracking .claude worktrees
cossio Jun 21, 2026
899b08f
Remove .claude from .gitignore
cossio Jun 21, 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
6 changes: 6 additions & 0 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,10 @@ loglogistic
logitexp
log1mlogistic
logit1mexp
logmeanexp
logmeanexp!
logvarexp
logvarexp!
logstdexp
logstdexp!
```
51 changes: 51 additions & 0 deletions ext/LogExpFunctionsChainRulesCoreExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,57 @@ function ChainRulesCore.rrule(::typeof(logsumexp), x::AbstractArray{<:Real}; dim
return Ω, logsumexp_pullback
end

function ChainRulesCore.frule((_, Δx), ::typeof(logmeanexp), x::AbstractArray{<:Real}; dims=:)
Ω = logmeanexp(x; dims)
n = length(x) ÷ length(Ω)
ΔΩ = sum(exp.(x .- Ω) .* Δx; dims) ./ n
return Ω, ΔΩ
end
function ChainRulesCore.rrule(::typeof(logmeanexp), x::AbstractArray{<:Real}; dims=:)
Ω = logmeanexp(x; dims)
n = length(x) ÷ length(Ω)
return Ω, _∂x_pullback(exp.(x .- Ω) ./ n, x)
end

function ChainRulesCore.frule((_, Δx), ::typeof(logvarexp), x::AbstractArray{<:Real}; dims=:, corrected::Bool=true)
logmean = logmeanexp(x; dims)
Ω = logvarexp(x; dims, corrected, logmean)
ΔΩ = sum(_∂x_logvarexp(x, logmean, dims) .* Δx; dims)
return Ω, ΔΩ
end
function ChainRulesCore.rrule(::typeof(logvarexp), x::AbstractArray{<:Real}; dims=:, corrected::Bool=true)
logmean = logmeanexp(x; dims)
Ω = logvarexp(x; dims, corrected, logmean)
return Ω, _∂x_pullback(_∂x_logvarexp(x, logmean, dims), x)
end
function ChainRulesCore.frule((_, Δx), ::typeof(logstdexp), x::AbstractArray{<:Real}; dims=:, corrected::Bool=true)
logmean = logmeanexp(x; dims)
Ω = logstdexp(x; dims, corrected, logmean)
ΔΩ = sum(_∂x_logvarexp(x, logmean, dims) ./ 2 .* Δx; dims)
return Ω, ΔΩ
end
function ChainRulesCore.rrule(::typeof(logstdexp), x::AbstractArray{<:Real}; dims=:, corrected::Bool=true)
logmean = logmeanexp(x; dims)
Ω = logstdexp(x; dims, corrected, logmean)
return Ω, _∂x_pullback(_∂x_logvarexp(x, logmean, dims) / 2, x)
end
function _∂x_logvarexp(x::AbstractArray{<:Real}, logmean, dims)
d = x .- logmean
e = expm1.(d)
return (2 .* exp.(d) .* e) ./ sum(abs2, e; dims)
end
function _∂x_pullback(∂x, x)
project_x = ChainRulesCore.ProjectTo(x)
function pullback(Ω̄)
x̄ = ChainRulesCore.InplaceableThunk(
Δ -> Δ .+= Ω̄ .* ∂x,
ChainRulesCore.@thunk(project_x(Ω̄ .* ∂x)),
)
return ChainRulesCore.NoTangent(), x̄
end
return pullback
end

# no rules for mutating functions currently:
# https://juliadiff.org/ChainRulesCore.jl/stable/writing_good_rules.html#Which-functions-need-rules?
function ChainRulesCore.frule((_, Δx), ::typeof(softmax), x::AbstractArray{<:Real}; dims=:)
Expand Down
2 changes: 2 additions & 0 deletions src/LogExpFunctions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ import LinearAlgebra
export xlogx, xlogy, xlog1py, xexpx, xexpy, logistic, logit, log1psq, log1pexp, log1mexp, log2mexp, logexpm1,
softplus, invsoftplus, log1pmx, logmxp1, logaddexp, logsubexp, logsumexp, logsumexp!, softmax,
softmax!, logcosh, logabssinh, logabstanh, cloglog, cexpexp,
logmeanexp, logmeanexp!, logvarexp, logvarexp!, logstdexp, logstdexp!,
loglogistic, logitexp, log1mlogistic, logit1mexp

include("basicfuns.jl")
include("logsumexp.jl")
include("logstatsexp.jl")
Comment thread
cossio marked this conversation as resolved.
Comment thread
cossio marked this conversation as resolved.

if !isdefined(Base, :get_extension)
include("../ext/LogExpFunctionsChainRulesCoreExt.jl")
Expand Down
106 changes: 106 additions & 0 deletions src/logstatsexp.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Numerically stable `log`-of-statistics-of-`exp` reductions:
# logmeanexp, logvarexp, logstdexp.

"""
$(SIGNATURES)

Compute `log(mean(exp, X))` in a numerically stable way.

`X` should be an iterator of numbers. For an array, `dims` selects the dimensions to reduce
over, returning `log.(mean(exp.(X); dims))`.

See also [`logmeanexp!`](@ref).
"""
function logmeanexp(X)
next = iterate(X)
isnothing(next) && throw(ArgumentError("reducing over an empty collection is not allowed"))
x, state = next
acc = x
count = 1
while true
next = iterate(X, state)
isnothing(next) && break
x, state = next
acc = _logsumexp_onepass_op(acc, x)
count += 1
end
lse = _logsumexp_onepass_result(acc)
return oftype(lse, lse - log(count))
end
logmeanexp(X::AbstractArray{<:Number}; dims=:) = _logmeanexp(X, dims)
function _logmeanexp(X::AbstractArray{<:Number}, ::Colon)
out = logsumexp(X)
return oftype(out, out - log(length(X)))
end
function _logmeanexp(X::AbstractArray{<:Number}, dims)
out = similar(X, float(eltype(X)), Base.reduced_indices(axes(X), dims))
return logmeanexp!(out, X)
end

"""
$(SIGNATURES)

Compute [`logmeanexp`](@ref) of `X` over the singleton dimensions of `out`, and write the
result to `out`.
"""
function logmeanexp!(out::AbstractArray, X::AbstractArray{<:Number})
logsumexp!(out, X)
return out .-= log(length(X) / length(out))
end


"""
$(SIGNATURES)

Compute `log(var(exp.(X); corrected))` in a numerically stable way.

`X` should be an array of real numbers; `dims` selects the dimensions to reduce over,
returning `log.(var(exp.(X); dims, corrected))`. A precomputed `logmeanexp(X; dims)` can be
passed as `logmean` to avoid recomputing it.

See also [`logvarexp!`](@ref).
"""
logvarexp(X::AbstractArray{<:Real}; dims=:, corrected::Bool=true, logmean=logmeanexp(X; dims)) = _logvarexp(X, dims, corrected, logmean)
function _logvarexp(X::AbstractArray{<:Real}, ::Colon, corrected::Bool, logmean)
out = logsumexp(2 .* logsubexp.(X, logmean))
return oftype(out, out - log(max(0, length(X) - corrected)))
end
function _logvarexp(X::AbstractArray{<:Real}, dims, corrected::Bool, logmean)
out = logsumexp(2 .* logsubexp.(X, logmean); dims)
return out .-= log(max(0, length(X) / length(out) - corrected))
end


"""
$(SIGNATURES)

Compute [`logvarexp`](@ref) of `X` over the singleton dimensions of `out`, and write the
result to `out`.
"""
function logvarexp!(out::AbstractArray{<:Real}, X::AbstractArray{<:Real}; corrected::Bool=true)
logmeanexp!(out, X)
logsumexp!(out, 2 .* logsubexp.(X, out))
return out .-= log(max(0, length(X) / length(out) - corrected))
end


"""
$(SIGNATURES)

Compute `log(std(exp.(X); dims, corrected))` in a numerically stable way.

Keyword arguments are as in [`logvarexp`](@ref).
"""
logstdexp(X::AbstractArray{<:Real}; dims=:, corrected::Bool=true, logmean=logmeanexp(X; dims)) = logvarexp(X; dims, corrected, logmean) / 2


"""
$(SIGNATURES)

Compute [`logstdexp`](@ref) of `X` over the singleton dimensions of `out`, and write the
result to `out`.
"""
function logstdexp!(out::AbstractArray{<:Real}, X::AbstractArray{<:Real}; corrected::Bool=true)
logvarexp!(out, X; corrected)
return out ./= 2
end
1 change: 1 addition & 0 deletions test/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688"
OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[compat]
Expand Down
12 changes: 12 additions & 0 deletions test/chainrules.jl
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@
test_rrule(logsumexp, x; fkwargs=(dims=dims,))
end

for x in (randn(10), randn(10, 8)), dims in (:, 1, 1:2, 2)
dims isa Colon || all(d <= ndims(x) for d in dims) || continue
test_frule(logmeanexp, x; fkwargs=(dims=dims,))
test_rrule(logmeanexp, x; fkwargs=(dims=dims,))
for corrected in (true, false)
test_frule(logvarexp, x; fkwargs=(dims=dims, corrected=corrected))
test_rrule(logvarexp, x; fkwargs=(dims=dims, corrected=corrected))
test_frule(logstdexp, x; fkwargs=(dims=dims, corrected=corrected))
test_rrule(logstdexp, x; fkwargs=(dims=dims, corrected=corrected))
end
end

for x in (randn(10), randn(10, 8))
test_frule(softmax, x)
test_rrule(softmax, x)
Expand Down
Loading