Skip to content

Commit 6fa1f71

Browse files
Add loglogistic, logitexp, log1mlogistic and logit1mexp (#82)
* Add `loglogistic`, `logitexp`, `log1mlogistic` and `logit1mexp` This takes advantage of `LogExpFunctions`'s accurate implementations of `log1pexp` and `log1mexp`, combined with negation in the log-odds domain to provide more accurate and less expensive implementations of the function compositions. * Register inverses; why not? * Update documentation * Fix failing test: be slightly less stringent * Re-style documentation * Use alternate design for `loglogistic` * Simplify dispatch cases * Add `ChangesOfVariables` support * Fix duplication in docstring * Be as careful as possible in the evaluation; we get ulp error of `logexpm1` * Apply the same conversion technique as `loglogistic` i.e. properly handle `Integer` and `Rational` * Eliminate 3 lines of code * Hide the proper handling of integers and rationals. This enables the desired interface of `f(x::Real)` * Add tests of inverses * Add ChainRules extension and respective tests * Add ChangesOfVariables tests * Fix typo * Update documentation * Add tests to ensure correctness for subtypes of `Real` in `Base` * Simplify definitions * Remove ChainRules definitions * Also remove ChainRules tests * Match expected form of `ChangesOfVariables` tests * Fix oversight in function type dispatches * Add `ChainRulesCore` support back * Revert "Add `ChainRulesCore` support back" This reverts commit 1b0f914. * Add extensive return type tests * Fix minor oversight from running tests locally * Add tests for `NaN`
1 parent c81ab8f commit 6fa1f71

File tree

8 files changed

+277
-2
lines changed

8 files changed

+277
-2
lines changed

docs/src/index.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Various special functions based on `log` and `exp` moved from [StatsFuns.jl](htt
44

55
The original authors of these functions are the StatsFuns.jl contributors.
66

7-
LogExpFunctions supports [`InverseFunctions.inverse`](https://github.com/JuliaMath/InverseFunctions.jl) and [`ChangesOfVariables.test_with_logabsdet_jacobian`](https://github.com/JuliaMath/ChangesOfVariables.jl) for `log1mexp`, `log1pexp`, `log2mexp`, `logexpm1`, `logistic`, `logit`, and `logcosh` (no inverse).
7+
LogExpFunctions supports [`InverseFunctions.inverse`](https://github.com/JuliaMath/InverseFunctions.jl) and [`ChangesOfVariables.test_with_logabsdet_jacobian`](https://github.com/JuliaMath/ChangesOfVariables.jl) for `log1mexp`, `log1pexp`, `log2mexp`, `logexpm1`, `logistic`, `logit`, `loglogistic`, `logitexp`, `log1mlogistic`, `logit1mexp`, and `logcosh` (no inverse).
88

99
```@docs
1010
xlogx
@@ -31,4 +31,8 @@ softmax!
3131
softmax
3232
cloglog
3333
cexpexp
34+
loglogistic
35+
logitexp
36+
log1mlogistic
37+
logit1mexp
3438
```

ext/LogExpFunctionsChangesOfVariablesExt.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,24 @@ function ChangesOfVariables.with_logabsdet_jacobian(::typeof(logcosh), x::Real)
4242
return y, log1mexp(a) - z
4343
end
4444

45+
function ChangesOfVariables.with_logabsdet_jacobian(::typeof(loglogistic), x::Real)
46+
y = loglogistic(x)
47+
return y, y - x
48+
end
49+
50+
function ChangesOfVariables.with_logabsdet_jacobian(::typeof(log1mlogistic), x::Real)
51+
y = log1mlogistic(x)
52+
return y, x + y
53+
end
54+
55+
function ChangesOfVariables.with_logabsdet_jacobian(::typeof(logitexp), x::Real)
56+
y = logitexp(x)
57+
return y, y - x
58+
end
59+
60+
function ChangesOfVariables.with_logabsdet_jacobian(::typeof(logit1mexp), x::Real)
61+
y = logit1mexp(x)
62+
return y, -y - x
63+
end
64+
4565
end # module

ext/LogExpFunctionsInverseFunctionsExt.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@ InverseFunctions.inverse(::typeof(logistic)) = logit
1616
InverseFunctions.inverse(::typeof(cloglog)) = cexpexp
1717
InverseFunctions.inverse(::typeof(cexpexp)) = cloglog
1818

19+
InverseFunctions.inverse(::typeof(loglogistic)) = logitexp
20+
InverseFunctions.inverse(::typeof(logitexp)) = loglogistic
21+
22+
InverseFunctions.inverse(::typeof(log1mlogistic)) = logit1mexp
23+
InverseFunctions.inverse(::typeof(logit1mexp)) = log1mlogistic
24+
1925
end # module

src/LogExpFunctions.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import LinearAlgebra
88

99
export xlogx, xlogy, xlog1py, xexpx, xexpy, logistic, logit, log1psq, log1pexp, log1mexp, log2mexp, logexpm1,
1010
softplus, invsoftplus, log1pmx, logmxp1, logaddexp, logsubexp, logsumexp, logsumexp!, softmax,
11-
softmax!, logcosh, logabssinh, cloglog, cexpexp
11+
softmax!, logcosh, logabssinh, cloglog, cexpexp,
12+
loglogistic, logitexp, log1mlogistic, logit1mexp
1213

1314
# expm1(::Float16) is not defined in older Julia versions,
1415
# hence for better Float16 support we use an internal function instead

src/basicfuns.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,3 +441,68 @@ $(SIGNATURES)
441441
Compute the complementary double exponential, `1 - exp(-exp(x))`.
442442
"""
443443
cexpexp(x) = -_expm1(-exp(x))
444+
445+
#=
446+
this uses the identity:
447+
448+
log(logistic(x)) = -log(1 + exp(-x))
449+
=#
450+
"""
451+
$(SIGNATURES)
452+
453+
Return `log(logistic(x))`, computed more carefully and with fewer calls
454+
than the naive composition of functions.
455+
456+
Its inverse is the [`logitexp`](@ref) function.
457+
"""
458+
loglogistic(x::Real) = -log1pexp(-float(x))
459+
460+
#=
461+
this uses the identity:
462+
463+
logit(exp(x)) = log(exp(x) / (1 + exp(x))) = -log(exp(-x) - 1)
464+
=#
465+
"""
466+
$(SIGNATURES)
467+
468+
Return `logit(exp(x))`, computed more carefully and with fewer calls than
469+
the naive composition of functions.
470+
471+
Its inverse is the [`loglogistic`](@ref) function.
472+
"""
473+
logitexp(x::Real) = -logexpm1(-float(x))
474+
475+
#=
476+
this uses the identity:
477+
478+
log(logistic(-x)) = -log(1 + exp(x))
479+
480+
that is, negation in the log-odds domain.
481+
=#
482+
483+
"""
484+
$(SIGNATURES)
485+
486+
Return `log(1 - logistic(x))`, computed more carefully and with fewer calls than
487+
the naive composition of functions.
488+
489+
Its inverse is the [`logit1mexp`](@ref) function.
490+
"""
491+
log1mlogistic(x::Real) = -log1pexp(x)
492+
493+
#=
494+
495+
this uses the same identity:
496+
497+
-logit(exp(x)) = logit(1 - exp(x)) = log((1 - exp(x)) / exp(x)) = log(exp(-x) - 1)
498+
=#
499+
500+
"""
501+
$(SIGNATURES)
502+
503+
Return `logit(1 - exp(x))`, computed more carefully and with fewer calls than
504+
the naive composition of functions.
505+
506+
Its inverse is the [`log1mlogistic`](@ref) function.
507+
"""
508+
logit1mexp(x::Real) = logexpm1(-float(x))

test/basicfuns.jl

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,3 +468,161 @@ end
468468
@test cexpexp(-Inf) == 0.0
469469
@test cexpexp(0) == (ℯ - 1) /
470470
end
471+
472+
@testset "loglogistic: $T" for T in (Float16, Float32, Float64)
473+
lim1 = T === Float16 ? -14.0 : -50.0
474+
lim2 = T === Float16 ? -10.0 : -37.0
475+
xs = T[Inf, -Inf, 0.0, lim1, lim2]
476+
for x in xs
477+
@test loglogistic(x) == log(logistic(x))
478+
end
479+
480+
ϵ = eps(T)
481+
xs = T[ϵ, 1.0, 18.0, 33.3, 50.0]
482+
for x in xs
483+
lhs = loglogistic(x)
484+
rhs = log(logistic(x))
485+
@test abs(lhs - rhs) < ϵ
486+
end
487+
488+
# misc
489+
@test loglogistic(T(Inf)) == -zero(T)
490+
@test loglogistic(-T(Inf)) == -T(Inf)
491+
@test loglogistic(-T(103.0)) == -T(103.0)
492+
@test abs(loglogistic(T(35.0))) < 3eps(T)
493+
@test abs(loglogistic(T(103.0))) < eps(T)
494+
@test isfinite(loglogistic(-T(745.0)))
495+
@test isfinite(loglogistic(T(50.0)))
496+
@test isfinite(loglogistic(T(745.0)))
497+
@test isnan(loglogistic(T(NaN)))
498+
@test isnan(loglogistic(T(-NaN)))
499+
500+
# type-consistency
501+
xs = T[Inf, -Inf, 0.0, lim1, lim2, ϵ, 1.0, 18.0, 33.3, 50.0]
502+
for x in xs
503+
@test typeof(loglogistic(x)) === T
504+
end
505+
end
506+
507+
508+
@testset "logitexp: $T" for T in (Float16, Float32, Float64)
509+
ϵ = eps(T)
510+
xs = T[ϵ, ϵ, 0.2, 0.4, 0.8, 1.0 - ϵ, 1.0 - ϵ]
511+
neg_xs = -xs
512+
for x in xs
513+
@test abs(logitexp(loglogistic(x)) - x) <= ϵ
514+
end
515+
for x in neg_xs
516+
@test abs(logitexp(loglogistic(x)) - x) <= 2ϵ
517+
end
518+
xs = T[-Inf, 0.0, Inf]
519+
for x in xs
520+
@test logitexp(loglogistic(x)) == x
521+
end
522+
523+
# misc
524+
@test isnan(logitexp(T(NaN)))
525+
@test isnan(logitexp(T(-NaN)))
526+
527+
# type-consistency
528+
xs = loglogistic.([T[ϵ, ϵ, 0.2, 0.4, 0.8, 1.0 - ϵ, 1.0 - ϵ]; neg_xs; T[-Inf, 0.0, Inf]])
529+
for x in xs
530+
@test typeof(logitexp(x)) === T
531+
end
532+
end
533+
534+
@testset "log1mlogistic: $T" for T in (Float16, Float32, Float64)
535+
@test log1mlogistic(T(Inf)) == -T(Inf)
536+
@test log1mlogistic(-T(Inf)) == -zero(T)
537+
@test log1mlogistic(-T(103.0)) < eps(T)
538+
@test abs(log1mlogistic(T(35.0))) == T(35.0)
539+
@test abs(log1mlogistic(T(103.0))) == T(103.0)
540+
@test isfinite(log1mlogistic(-T(745.0)))
541+
@test isfinite(log1mlogistic(T(50.0)))
542+
@test isfinite(log1mlogistic(T(745.0)))
543+
@test isnan(log1mlogistic(T(NaN)))
544+
@test isnan(log1mlogistic(T(-NaN)))
545+
546+
# type-consistency
547+
ϵ = eps(T)
548+
lim1 = T === Float16 ? -14.0 : -50.0
549+
lim2 = T === Float16 ? -10.0 : -37.0
550+
xs = T[Inf, -Inf, 0.0, lim1, lim2, ϵ, 1.0, 18.0, 33.3, 50.0]
551+
for x in xs
552+
@test typeof(log1mlogistic(x)) === T
553+
end
554+
end
555+
556+
557+
@testset "logit1mexp: $T" for T in (Float16, Float32, Float64)
558+
ϵ = eps(T)
559+
xs = T[ϵ, ϵ, 0.2, 0.4, 0.8, 1.0 - ϵ, 1.0 - ϵ]
560+
neg_xs = -xs
561+
for x in xs
562+
@test abs(logit1mexp(log1mlogistic(x)) - x) <= 2ϵ
563+
end
564+
for x in neg_xs
565+
@test abs(logit1mexp(log1mlogistic(x)) - x) <= ϵ
566+
end
567+
xs = T[-Inf, 0.0, Inf]
568+
for x in xs
569+
@test logit1mexp(log1mlogistic(x)) == x
570+
end
571+
572+
# misc
573+
@test isnan(logit1mexp(T(NaN)))
574+
@test isnan(logit1mexp(T(-NaN)))
575+
576+
# type-consistency
577+
xs = log1mlogistic.([T[ϵ, ϵ, 0.2, 0.4, 0.8, 1.0 - ϵ, 1.0 - ϵ]; neg_xs; T[-Inf, 0.0, Inf]])
578+
for x in xs
579+
@test typeof(logit1mexp(x)) === T
580+
end
581+
end
582+
583+
@testset "loglogistic, log1mlogistic correctness wrt Unsigned, Rational: $T" for T in
584+
(UInt8, UInt16, UInt32, UInt64, UInt128)
585+
@test loglogistic(T(5)) == loglogistic(5.0)
586+
@test log1mlogistic(T(5)) == log1mlogistic(5.0)
587+
588+
@test loglogistic(T(0x01)//T(0x02)) == loglogistic(0.5)
589+
@test log1mlogistic(T(0x01)//T(0x02)) == log1mlogistic(0.5)
590+
591+
@test loglogistic(T(0x01)//T(0x00)) == loglogistic(Inf)
592+
@test log1mlogistic(T(0x01)//T(0x00)) == log1mlogistic(Inf)
593+
594+
end
595+
596+
@testset "loglogistic, log1mlogistic, logitexp, logit1mexp correctness wrt Integer edge case: $T" for T in
597+
(Int8, Int16, Int32, Int64, Int128)
598+
# If not handled, these will be zero
599+
@test loglogistic(typemin(T)) == loglogistic(float(typemin(T)))
600+
@test log1mlogistic(typemin(T)) == log1mlogistic(float(typemin(T)))
601+
602+
# If not handled, these would throw since negation at typemin is a round-trip
603+
@test logitexp(typemin(T)) == logitexp(float(typemin(T)))
604+
@test logit1mexp(typemin(T)) == logit1mexp(float(typemin(T)))
605+
end
606+
607+
@testset "loglogistic, log1mlogistic, logitexp, logit1mexp return types" begin
608+
for T in
609+
(UInt8, UInt16, UInt32, UInt64, UInt128, Int8, Int16, Int32, Int64, Int128)
610+
@test typeof(loglogistic(T(5))) === Float64
611+
@test typeof(log1mlogistic(T(5))) === Float64
612+
@test typeof(loglogistic(T(0x01)//T(0x02))) === Float64
613+
@test typeof(log1mlogistic(T(0x01)//T(0x02))) === Float64
614+
@test typeof(logitexp(T(0))) === Float64
615+
@test typeof(logit1mexp(T(0))) === Float64
616+
end
617+
@test typeof(loglogistic(BigInt(5))) === BigFloat
618+
@test typeof(log1mlogistic(BigInt(5))) === BigFloat
619+
@test typeof(loglogistic(BigInt(0x01)//BigInt(0x02))) === BigFloat
620+
@test typeof(log1mlogistic(BigInt(0x01)//BigInt(0x02))) === BigFloat
621+
@test typeof(logitexp(BigInt(0))) === BigFloat
622+
@test typeof(logit1mexp(BigInt(0))) === BigFloat
623+
624+
@test typeof(loglogistic(BigFloat(5))) === BigFloat
625+
@test typeof(log1mlogistic(BigFloat(5))) === BigFloat
626+
@test typeof(logitexp(BigFloat(0))) === BigFloat
627+
@test typeof(logit1mexp(BigFloat(0))) === BigFloat
628+
end

test/inverse.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,10 @@
1111

1212
InverseFunctions.test_inverse(cloglog, rand())
1313
InverseFunctions.test_inverse(cexpexp, rand())
14+
15+
InverseFunctions.test_inverse(loglogistic, randexp())
16+
InverseFunctions.test_inverse(logitexp, -randexp())
17+
18+
InverseFunctions.test_inverse(log1mlogistic, randexp())
19+
InverseFunctions.test_inverse(logit1mexp, -randexp())
1420
end

test/with_logabsdet_jacobian.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,19 @@
1919

2020
ChangesOfVariables.test_with_logabsdet_jacobian(logcosh, x, derivative)
2121
ChangesOfVariables.test_with_logabsdet_jacobian(logcosh, -x, derivative)
22+
23+
dloglogistic(x) = logistic(-x)
24+
dlog1mlogistic(x) = -logistic(x)
25+
dlogitexp(x) = inv(1 - exp(x))
26+
dlogit1mexp(x) = -inv(1 - exp(x))
27+
derivative(f::typeof(loglogistic), x) = dloglogistic(x)
28+
derivative(f::typeof(log1mlogistic), x) = dlog1mlogistic(x)
29+
derivative(f::typeof(logitexp), x) = dlogitexp(x)
30+
derivative(f::typeof(logit1mexp), x) = dlogit1mexp(x)
31+
32+
ChangesOfVariables.test_with_logabsdet_jacobian(loglogistic, x, derivative)
33+
ChangesOfVariables.test_with_logabsdet_jacobian(logitexp, -x, derivative)
34+
35+
ChangesOfVariables.test_with_logabsdet_jacobian(log1mlogistic, x, derivative)
36+
ChangesOfVariables.test_with_logabsdet_jacobian(logit1mexp, -x, derivative)
2237
end

0 commit comments

Comments
 (0)