From 395ec491f48b97f8b42476f1d5619419c0e1b477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathieu=20Besan=C3=A7on?= Date: Thu, 5 Aug 2021 22:54:56 +0200 Subject: [PATCH 01/58] Update CITATION.bib (#1377) --- CITATION.bib | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/CITATION.bib b/CITATION.bib index 81141f167..dd5839b69 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -1,18 +1,16 @@ % reference paper -@article{2019arXiv190708611B, - author = {{Besan{\c{c}}on}, Mathieu and {Anthoff}, David and {Arslan}, Alex and - {Byrne}, Simon and {Lin}, Dahua and {Papamarkou}, Theodore and - {Pearson}, John}, - title = {Distributions.jl: Definition and Modeling of Probability Distributions in the JuliaStats Ecosystem}, - journal = {arXiv e-prints}, - keywords = {Statistics - Computation, Computer Science - Mathematical Software}, - year = 2019, - month = "Jul", - eid = {arXiv:1907.08611}, - pages = {arXiv:1907.08611}, - archivePrefix = {arXiv}, - eprint = {1907.08611}, - primaryClass = {stat.CO}, +@article{JSSv098i16, + author = {Mathieu Besançon and Theodore Papamarkou and David Anthoff and Alex Arslan and Simon Byrne and Dahua Lin and John Pearson}, + title = {Distributions.jl: Definition and Modeling of Probability Distributions in the JuliaStats Ecosystem}, + journal = {Journal of Statistical Software}, + volume = {98}, + number = {16}, + year = {2021}, + keywords = {Julia; distributions; modeling; interface; mixture; KDE; sampling; probabilistic programming; inference}, + issn = {1548-7660}, + pages = {1--30}, + doi = {10.18637/jss.v098.i16}, + url = {https://www.jstatsoft.org/v098/i16} } % reference for the software itself From 2856bc3e106d751a02e3fe9cccb046916a52955e Mon Sep 17 00:00:00 2001 From: st-- Date: Thu, 12 Aug 2021 01:17:57 +0300 Subject: [PATCH 02/58] Fix AbstractMvNormal docstring (#1376) --- src/multivariate/mvnormal.jl | 20 ++++++++++---------- src/multivariate/mvnormalcanon.jl | 18 +++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/multivariate/mvnormal.jl b/src/multivariate/mvnormal.jl index 387b63956..6e6c8cad0 100644 --- a/src/multivariate/mvnormal.jl +++ b/src/multivariate/mvnormal.jl @@ -42,7 +42,7 @@ type `MvNormal`, defined as below, which allows users to specify the special str the mean and covariance. ```julia -struct MvNormal{Cov<:AbstractPDMat,Mean<:AbstractVector} <: AbstractMvNormal +struct MvNormal{T<:Real,Cov<:AbstractPDMat,Mean<:AbstractVector} <: AbstractMvNormal μ::Mean Σ::Cov end @@ -51,19 +51,19 @@ end Here, the mean vector can be an instance of any `AbstractVector`. The covariance can be of any subtype of `AbstractPDMat`. Particularly, one can use `PDMat` for full covariance, `PDiagMat` for diagonal covariance, and `ScalMat` for the isotropic covariance -- those -in the form of ``\\sigma \\mathbf{I}``. (See the Julia package -[PDMats](https://github.com/lindahua/PDMats.jl) for details). +in the form of ``\\sigma^2 \\mathbf{I}``. (See the Julia package +[PDMats](https://github.com/JuliaStats/PDMats.jl/) for details). -We also define a set of alias for the types using different combinations of mean vectors and covariance: +We also define a set of aliases for the types using different combinations of mean vectors and covariance: ```julia -const IsoNormal = MvNormal{ScalMat, Vector{Float64}} -const DiagNormal = MvNormal{PDiagMat, Vector{Float64}} -const FullNormal = MvNormal{PDMat, Vector{Float64}} +const IsoNormal = MvNormal{Float64, ScalMat{Float64}, Vector{Float64}} +const DiagNormal = MvNormal{Float64, PDiagMat{Float64,Vector{Float64}}, Vector{Float64}} +const FullNormal = MvNormal{Float64, PDMat{Float64,Matrix{Float64}}, Vector{Float64}} -const ZeroMeanIsoNormal{Axes} = MvNormal{ScalMat, Zeros{Float64,1,Axes}} -const ZeroMeanDiagNormal{Axes} = MvNormal{PDiagMat, Zeros{Float64,1,Axes}} -const ZeroMeanFullNormal{Axes} = MvNormal{PDMat, Zeros{Float64,1,Axes}} +const ZeroMeanIsoNormal{Axes} = MvNormal{Float64, ScalMat{Float64}, Zeros{Float64,1,Axes}} +const ZeroMeanDiagNormal{Axes} = MvNormal{Float64, PDiagMat{Float64,Vector{Float64}}, Zeros{Float64,1,Axes}} +const ZeroMeanFullNormal{Axes} = MvNormal{Float64, PDMat{Float64,Matrix{Float64}}, Zeros{Float64,1,Axes}} ``` Multivariate normal distributions support affine transformations: diff --git a/src/multivariate/mvnormalcanon.jl b/src/multivariate/mvnormalcanon.jl index aa6d74738..91c9596d3 100644 --- a/src/multivariate/mvnormalcanon.jl +++ b/src/multivariate/mvnormalcanon.jl @@ -5,7 +5,7 @@ """ MvNormalCanon -Multivariate normal distribution is an [exponential family distribution](http://en.wikipedia.org/wiki/Exponential_family), +The multivariate normal distribution is an [exponential family distribution](http://en.wikipedia.org/wiki/Exponential_family), with two *canonical parameters*: the *potential vector* ``\\mathbf{h}`` and the *precision matrix* ``\\mathbf{J}``. The relation between these parameters and the conventional representation (*i.e.* the one using mean ``\\boldsymbol{\\mu}`` and covariance ``\\boldsymbol{\\Sigma}``) is: @@ -19,7 +19,7 @@ which is also a subtype of `AbstractMvNormal` to represent a multivariate normal canonical parameters. Particularly, `MvNormalCanon` is defined as: ```julia -struct MvNormalCanon{P<:AbstractPDMat,V<:AbstractVector} <: AbstractMvNormal +struct MvNormalCanon{T<:Real,P<:AbstractPDMat,V<:AbstractVector} <: AbstractMvNormal μ::V # the mean vector h::V # potential vector, i.e. inv(Σ) * μ J::P # precision matrix, i.e. inv(Σ) @@ -29,13 +29,13 @@ end We also define aliases for common specializations of this parametric type: ```julia -const FullNormalCanon = MvNormalCanon{PDMat, Vector{Float64}} -const DiagNormalCanon = MvNormalCanon{PDiagMat, Vector{Float64}} -const IsoNormalCanon = MvNormalCanon{ScalMat, Vector{Float64}} +const FullNormalCanon = MvNormalCanon{Float64, PDMat{Float64,Matrix{Float64}}, Vector{Float64}} +const DiagNormalCanon = MvNormalCanon{Float64, PDiagMat{Float64,Vector{Float64}}, Vector{Float64}} +const IsoNormalCanon = MvNormalCanon{Float64, ScalMat{Float64}, Vector{Float64}} -const ZeroMeanFullNormalCanon{Axes} = MvNormalCanon{PDMat, Zeros{Float64,1}} -const ZeroMeanDiagNormalCanon{Axes} = MvNormalCanon{PDiagMat, Zeros{Float64,1}} -const ZeroMeanIsoNormalCanon{Axes} = MvNormalCanon{ScalMat, Zeros{Float64,1,Axes}} +const ZeroMeanFullNormalCanon{Axes} = MvNormalCanon{Float64, PDMat{Float64,Matrix{Float64}}, Zeros{Float64,1,Axes}} +const ZeroMeanDiagNormalCanon{Axes} = MvNormalCanon{Float64, PDiagMat{Float64,Vector{Float64}}, Zeros{Float64,1,Axes}} +const ZeroMeanIsoNormalCanon{Axes} = MvNormalCanon{Float64, ScalMat{Float64}, Zeros{Float64,1,Axes}} ``` **Note:** `MvNormalCanon` share the same set of methods as `MvNormal`. @@ -46,7 +46,7 @@ struct MvNormalCanon{T<:Real,P<:AbstractPDMat,V<:AbstractVector} <: AbstractMvNo J::P # precision matrix, i.e. inv(Σ) end -const FullNormalCanon = MvNormalCanon{Float64, PDMat{Float64,Matrix{Float64}},Vector{Float64}} +const FullNormalCanon = MvNormalCanon{Float64,PDMat{Float64,Matrix{Float64}},Vector{Float64}} const DiagNormalCanon = MvNormalCanon{Float64,PDiagMat{Float64,Vector{Float64}},Vector{Float64}} const IsoNormalCanon = MvNormalCanon{Float64,ScalMat{Float64},Vector{Float64}} From e49b8448f7db33a3ddea2536628d89ffed59b59f Mon Sep 17 00:00:00 2001 From: David Widmann Date: Thu, 26 Aug 2021 17:27:09 +0200 Subject: [PATCH 03/58] Deprecate all outer constructors of `Truncated` (#1384) * Deprecate all outer constructors of `Truncated` * Bump version --- Project.toml | 2 +- src/truncate.jl | 13 +------------ src/univariate/continuous/kolmogorov.jl | 2 +- test/testutils.jl | 2 +- test/truncate.jl | 6 +++--- 5 files changed, 7 insertions(+), 18 deletions(-) diff --git a/Project.toml b/Project.toml index ecd582202..83b7b870f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.11" +version = "0.25.12" [deps] FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" diff --git a/src/truncate.jl b/src/truncate.jl index 66f6bf763..a017196ab 100644 --- a/src/truncate.jl +++ b/src/truncate.jl @@ -57,18 +57,7 @@ struct Truncated{D<:UnivariateDistribution, S<:ValueSupport, T <: Real} <: Univa end end -### Constructors -function Truncated(d::UnivariateDistribution, l::T, u::T) where {T <: Real} - l < u || error("lower bound should be less than upper bound.") - T2 = promote_type(T, eltype(d)) - lcdf = isinf(l) ? zero(T2) : T2(cdf(d, l)) - ucdf = isinf(u) ? one(T2) : T2(cdf(d, u)) - tp = ucdf - lcdf - Truncated(d, promote(l, u, lcdf, ucdf, tp, log(tp))...) -end - -Truncated(d::UnivariateDistribution, l::Integer, u::Integer) = Truncated(d, float(l), float(u)) - +### Constructors of `Truncated` are deprecated - users should call `truncated` @deprecate Truncated(d::UnivariateDistribution, l::Real, u::Real) truncated(d, l, u) params(d::Truncated) = tuple(params(d.untruncated)..., d.lower, d.upper) diff --git a/src/univariate/continuous/kolmogorov.jl b/src/univariate/continuous/kolmogorov.jl index 4729fbe06..f7bb373ab 100644 --- a/src/univariate/continuous/kolmogorov.jl +++ b/src/univariate/continuous/kolmogorov.jl @@ -153,7 +153,7 @@ function rand(rng::AbstractRNG, d::Kolmogorov) end # equivalent to -# rand(Truncated(Gamma(1.5,1),tp,Inf)) +# rand(truncated(Gamma(1.5,1),tp,Inf)) function rand_trunc_gamma(rng::AbstractRNG) tp = 2.193245422464302 #pi^2/(8*t^2) while true diff --git a/test/testutils.jl b/test/testutils.jl index fea1fc55f..7785c32fd 100644 --- a/test/testutils.jl +++ b/test/testutils.jl @@ -579,7 +579,7 @@ function test_params(d::Truncated) d_unt = d.untruncated D = typeof(d_unt) pars = params(d_unt) - d_new = Truncated(D(pars...), d.lower, d.upper) + d_new = truncated(D(pars...), d.lower, d.upper) @test d_new == d @test d == deepcopy(d) end diff --git a/test/truncate.jl b/test/truncate.jl index 363af62e5..85ae457bd 100644 --- a/test/truncate.jl +++ b/test/truncate.jl @@ -19,7 +19,7 @@ function verify_and_test_drive(jsonfile, selected, n_tsamples::Int,lower::Int,up dname = string(dsym) - dsymt = Symbol("Truncated($(dct["dtype"]),$lower,$upper") + dsymt = Symbol("truncated($(dct["dtype"]),$lower,$upper") dnamet = string(dsym) # test whether it is included in the selected list @@ -36,8 +36,8 @@ function verify_and_test_drive(jsonfile, selected, n_tsamples::Int,lower::Int,up continue end - println(" testing Truncated($(ex),$lower,$upper)") - d = Truncated(eval(Meta.parse(ex)),lower,upper) + println(" testing truncated($(ex),$lower,$upper)") + d = truncated(eval(Meta.parse(ex)),lower,upper) if dtype != Uniform && dtype != TruncatedNormal # Uniform is truncated to Uniform @assert isa(dtype, Type) && dtype <: UnivariateDistribution @test isa(d, dtypet) From 27f54135500bd5d2a58fc90d8a55f2ca9f68ffe6 Mon Sep 17 00:00:00 2001 From: Jiayi Wei Date: Thu, 26 Aug 2021 10:56:49 -0500 Subject: [PATCH 04/58] Fix the numerical instability in `Truncated`. (#1374) * Fix the numerical instability in `Truncated`. * Incorporate suggested changes. Co-authored-by: David Widmann --- src/truncate.jl | 12 +++++++----- test/truncnormal.jl | 10 ++++++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/truncate.jl b/src/truncate.jl index a017196ab..206172e79 100644 --- a/src/truncate.jl +++ b/src/truncate.jl @@ -22,11 +22,13 @@ end function truncated(d::UnivariateDistribution, l::T, u::T) where {T <: Real} l < u || error("lower bound should be less than upper bound.") - T2 = promote_type(T, eltype(d)) - lcdf = isinf(l) ? zero(T2) : T2(cdf(d, l)) - ucdf = isinf(u) ? one(T2) : T2(cdf(d, u)) - tp = ucdf - lcdf - Truncated(d, promote(l, u, lcdf, ucdf, tp, log(tp))...) + logcdf_l = logcdf(d, l) + logcdf_u = logcdf(d, u) + lcdf = exp(logcdf_l) + ucdf = exp(logcdf_u) + log_tp = logsubexp(logcdf_l, logcdf_u) + tp = exp(log_tp) + Truncated(d, promote(l, u, lcdf, ucdf, tp, log_tp)...) end truncated(d::UnivariateDistribution, l::Integer, u::Integer) = truncated(d, float(l), float(u)) diff --git a/test/truncnormal.jl b/test/truncnormal.jl index 8f5c1be0e..a6f93b0b5 100644 --- a/test/truncnormal.jl +++ b/test/truncnormal.jl @@ -41,3 +41,13 @@ end @test abs(var(X) - var(trunc)) < 0.01 end end + +@testset "Truncated normal should be numerically stable at low probability regions" begin + original = Normal(-5.0, 0.2) + trunc = truncated(original, 0.0, 5.0) + for x in LinRange(0.0, 5.0, 100) + @test isfinite(logpdf(original, x)) + @test isfinite(logpdf(trunc, x)) + @test isfinite(pdf(trunc, x)) + end +end From a20ed4662f01620659668e512e1c4a74cdaf17a8 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Fri, 27 Aug 2021 08:52:14 +0200 Subject: [PATCH 05/58] Fix deprecations and test error with Julia nightly (#1385) * Fix deprecations and test error with Julia nightly * Fix more deprecations --- src/multivariate/mvlognormal.jl | 1 + src/multivariate/product.jl | 4 ++-- test/binomial.jl | 7 +++---- test/convolution.jl | 32 +++++++++++++++++++------------- test/dirichletmultinomial.jl | 1 - test/lognormal.jl | 4 +++- test/matrixvariates.jl | 4 +++- test/mixture.jl | 6 +++--- test/mvlognormal.jl | 4 ++-- test/mvnormal.jl | 2 +- test/mvtdist.jl | 2 ++ test/normal.jl | 4 +++- test/poissonbinomial.jl | 5 +++-- test/samplers.jl | 2 +- test/types.jl | 17 ++++++++++------- 15 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/multivariate/mvlognormal.jl b/src/multivariate/mvlognormal.jl index 24bfdd8ce..25cddeae8 100644 --- a/src/multivariate/mvlognormal.jl +++ b/src/multivariate/mvlognormal.jl @@ -168,6 +168,7 @@ end # Constructors mirror the ones for MvNormmal MvLogNormal(μ::AbstractVector{<:Real}, Σ::AbstractMatrix{<:Real}) = MvLogNormal(MvNormal(μ, Σ)) MvLogNormal(Σ::AbstractMatrix{<:Real}) = MvLogNormal(MvNormal(Σ)) +MvLogNormal(μ::AbstractVector{<:Real}, Σ::UniformScaling{<:Real}) = MvLogNormal(MvNormal(μ, Σ)) # Deprecated constructors MvLogNormal(μ::AbstractVector,σ::Vector) = MvLogNormal(MvNormal(μ,σ)) diff --git a/src/multivariate/product.jl b/src/multivariate/product.jl index 55bb0103f..454ffada3 100644 --- a/src/multivariate/product.jl +++ b/src/multivariate/product.jl @@ -61,6 +61,6 @@ covariance matrix. """ function product_distribution(dists::AbstractVector{<:Normal}) µ = mean.(dists) - σ = std.(dists) - return MvNormal(µ, σ) + σ2 = var.(dists) + return MvNormal(µ, Diagonal(σ2)) end diff --git a/test/binomial.jl b/test/binomial.jl index 0991f370e..10dedb8ce 100644 --- a/test/binomial.jl +++ b/test/binomial.jl @@ -1,13 +1,12 @@ using Distributions using Test, Random +Random.seed!(1234) +@testset "binomial" begin # Test the consistency between the recursive and nonrecursive computation of the pdf # of the Binomial distribution -Random.seed!(1234) for (p, n) in [(0.6, 10), (0.8, 6), (0.5, 40), (0.04, 20), (1., 100), (0., 10), (0.999999, 1000), (1e-7, 1000)] - local p - d = Binomial(n, p) a = pdf.(d, 0:n) @@ -21,7 +20,6 @@ for (p, n) in [(0.6, 10), (0.8, 6), (0.5, 40), (0.04, 20), (1., 100), (0., 10), for t in rng @test pdf(d, t) ≈ b[t - first(rng) + 1] end - end # Test calculation of expectation value for Binomial distribution @@ -36,3 +34,4 @@ end @test isplatykurtic(Bernoulli(0.5)) @test ismesokurtic(Normal(0.0, 1.0)) @test isleptokurtic(Laplace(0.0, 1.0)) +end diff --git a/test/convolution.jl b/test/convolution.jl index 213e22444..bcb5292d8 100644 --- a/test/convolution.jl +++ b/test/convolution.jl @@ -1,3 +1,9 @@ +using Distributions +using FillArrays + +using LinearAlgebra +using Test + @testset "discrete univariate" begin @testset "Bernoulli" begin @@ -139,17 +145,17 @@ end @testset "iso-/diag-normal" begin - in1 = MvNormal([1.2, 0.3], 2) - in2 = MvNormal([-2.0, 6.9], 0.5) + in1 = MvNormal([1.2, 0.3], 2 * I) + in2 = MvNormal([-2.0, 6.9], 0.5 * I) - zmin1 = MvNormal(2, 1.9) - zmin2 = MvNormal(2, 5.2) + zmin1 = MvNormal(Zeros(2), 1.9 * I) + zmin2 = MvNormal(Diagonal(Fill(5.2, 2))) - dn1 = MvNormal([0.0, 4.7], [0.1, 1.8]) - dn2 = MvNormal([-3.4, 1.2], [3.2, 0.2]) + dn1 = MvNormal([0.0, 4.7], Diagonal([0.1, 1.8])) + dn2 = MvNormal([-3.4, 1.2], Diagonal([3.2, 0.2])) - zmdn1 = MvNormal([1.2, 0.3]) - zmdn2 = MvNormal([-0.8, 1.0]) + zmdn1 = MvNormal(Diagonal([1.2, 0.3])) + zmdn2 = MvNormal(Diagonal([-0.8, 1.0])) dist_list = (in1, in2, zmin1, zmin2, dn1, dn2, zmdn1, zmdn2) @@ -161,7 +167,7 @@ end end # erroring - in3 = MvNormal([1, 2, 3], 0.2) + in3 = MvNormal([1, 2, 3], 0.2 * I) @test_throws ArgumentError convolve(in1, in3) end @@ -202,10 +208,10 @@ end @testset "mixed" begin - in1 = MvNormal([1.2, 0.3], 2) - zmin1 = MvNormal(2, 1.9) - dn1 = MvNormal([0.0, 4.7], [0.1, 1.8]) - zmdn1 = MvNormal([1.2, 0.3]) + in1 = MvNormal([1.2, 0.3], 2 * I) + zmin1 = MvNormal(Zeros(2), 1.9 * I) + dn1 = MvNormal([0.0, 4.7], Diagonal([0.1, 1.8])) + zmdn1 = MvNormal(Diagonal([1.2, 0.3])) m1 = Symmetric(rand(2, 2)) m1sq = m1^2 full = MvNormal(ones(2), m1sq.data) diff --git a/test/dirichletmultinomial.jl b/test/dirichletmultinomial.jl index 31bd78a42..68a5dff15 100644 --- a/test/dirichletmultinomial.jl +++ b/test/dirichletmultinomial.jl @@ -51,7 +51,6 @@ d = DirichletMultinomial(10, 5) @test !insupport(d, 3.0 * ones(5)) for x in (2 * ones(5), [1, 2, 3, 4, 0], [3.0, 0.0, 3.0, 0.0, 4.0], [0, 0, 0, 0, 10]) - local x @test pdf(d, x) ≈ factorial(d.n) * gamma(d.α0) / gamma(d.n + d.α0) * prod(gamma.(d.α + x) ./ factorial.(x) ./ gamma.(d.α)) @test logpdf(d, x) ≈ diff --git a/test/lognormal.jl b/test/lognormal.jl index 8ee4956e9..b5013cb7f 100644 --- a/test/lognormal.jl +++ b/test/lognormal.jl @@ -16,7 +16,9 @@ isnan_type(::Type{T}, v) where {T} = isnan(v) && v isa T @test logdiffcdf(LogNormal(), Float64(exp(5)), Float64(exp(3))) ≈ -6.607938594596893 rtol=1e-12 let d = LogNormal(Float64(0), Float64(1)), x = Float64(exp(-60)), y = Float64(exp(-60.001)) float_res = logdiffcdf(d, x, y) - big_float_res = log(cdf(d, BigFloat(x, 100)) - cdf(d, BigFloat(y, 100))) + big_x = VERSION < v"1.1" ? BigFloat(x, 100) : BigFloat(x; precision=100) + big_y = VERSION < v"1.1" ? BigFloat(y, 100) : BigFloat(y; precision=100) + big_float_res = log(cdf(d, big_x) - cdf(d, big_y)) @test float_res ≈ big_float_res end diff --git a/test/matrixvariates.jl b/test/matrixvariates.jl index 1498a315b..55082cb1f 100644 --- a/test/matrixvariates.jl +++ b/test/matrixvariates.jl @@ -7,6 +7,7 @@ using Test import JSON import Distributions: _univariate, _multivariate, _rand_params +@testset "matrixvariates" begin #= 1. baseline tests 2. compare 1 x 1 matrix-variate with univariate @@ -200,7 +201,7 @@ function pvalue_kolmogorovsmirnoff(x::AbstractVector, d::UnivariateDistribution) end function test_draws_against_univariate_cdf(D::MatrixDistribution, d::UnivariateDistribution) - α = 0.05 + α = 0.025 M = 100000 matvardraws = [rand(D)[1] for m in 1:M] @test pvalue_kolmogorovsmirnoff(matvardraws, d) >= α @@ -537,3 +538,4 @@ for distribution in matrixvariates test_matrixvariate(dist, n, p, M) end end +end diff --git a/test/mixture.jl b/test/mixture.jl index 3873df72a..b23a4488b 100644 --- a/test/mixture.jl +++ b/test/mixture.jl @@ -222,9 +222,9 @@ end @testset "Testing MultivariatevariateMixture" begin g_m = MixtureModel( - IsoNormal[ MvNormal([0.0, 0.0], 1.0), - MvNormal([0.2, 1.0], 1.0), - MvNormal([-0.5, -3.0], 1.6) ], + IsoNormal[ MvNormal([0.0, 0.0], I), + MvNormal([0.2, 1.0], I), + MvNormal([-0.5, -3.0], 1.6 * I) ], [0.2, 0.5, 0.3]) @test isa(g_m, MixtureModel{Multivariate, Continuous, IsoNormal}) @test length(components(g_m)) == 3 diff --git a/test/mvlognormal.jl b/test/mvlognormal.jl index dc4cf5816..a57caac69 100644 --- a/test/mvlognormal.jl +++ b/test/mvlognormal.jl @@ -96,8 +96,8 @@ end ####### Validate results for a single-dimension MvLogNormal by comparing with univariate LogNormal @testset "Comparing results from MvLogNormal with univariate LogNormal" begin - l1 = LogNormal(0.1,0.4) - l2 = MvLogNormal(0.1*ones(1),0.4) + l1 = LogNormal(0.1, 0.4) + l2 = MvLogNormal([0.1], 0.16 * I) @test [mean(l1)] ≈ mean(l2) @test [median(l1)] ≈ median(l2) @test [mode(l1)] ≈ mode(l2) diff --git a/test/mvnormal.jl b/test/mvnormal.jl index e0d34cd11..bb24c9e21 100644 --- a/test/mvnormal.jl +++ b/test/mvnormal.jl @@ -362,7 +362,7 @@ end end @testset "MvNormal: Sampling with integer-valued parameters (#1004)" begin - d = MvNormal([0, 0], [1, 1]) + d = MvNormal([0, 0], Diagonal([1, 1])) @test rand(d) isa Vector{Float64} @test rand(d, 10) isa Matrix{Float64} @test rand(d, (3, 2)) isa Matrix{Vector{Float64}} diff --git a/test/mvtdist.jl b/test/mvtdist.jl index 4e1b20324..5f1cc71d2 100644 --- a/test/mvtdist.jl +++ b/test/mvtdist.jl @@ -4,6 +4,7 @@ using Test import Distributions: GenericMvTDist import PDMats: PDMat +@testset "mvtdist" begin # Set location vector mu and scale matrix Sigma as in # Hofert M. On Sampling from the Multivariate t Distribution. The R Journal mu = [1., 2] @@ -84,3 +85,4 @@ end x = rand(X_implicit) @test logpdf(X_implicit, x) ≈ logpdf(X_expicit, x) end +end diff --git a/test/normal.jl b/test/normal.jl index 4101abc64..f3a79c63a 100644 --- a/test/normal.jl +++ b/test/normal.jl @@ -14,7 +14,9 @@ isnan_type(::Type{T}, v) where {T} = isnan(v) && v isa T @test logdiffcdf(Normal(), Float64(5), Float64(3)) ≈ -6.607938594596893 rtol=1e-12 let d = Normal(Float64(0), Float64(1)), x = Float64(-60), y = Float64(-60.001) float_res = logdiffcdf(d, x, y) - big_float_res = log(cdf(d, BigFloat(x, 100)) - cdf(d, BigFloat(y, 100))) + big_x = VERSION < v"1.1" ? BigFloat(x, 100) : BigFloat(x; precision=100) + big_y = VERSION < v"1.1" ? BigFloat(y, 100) : BigFloat(y; precision=100) + big_float_res = log(cdf(d, big_x) - cdf(d, big_y)) @test float_res ≈ big_float_res end @test_throws ArgumentError logdiffcdf(Normal(), 1.0, 2.0) diff --git a/test/poissonbinomial.jl b/test/poissonbinomial.jl index dec55cf46..98f4e403d 100644 --- a/test/poissonbinomial.jl +++ b/test/poissonbinomial.jl @@ -1,6 +1,8 @@ using Distributions +using ForwardDiff using Test +@testset "poissonbinomial" begin function naive_esf(x::AbstractVector{T}) where T <: Real n = length(x) S = zeros(T, n+1) @@ -36,8 +38,6 @@ naive_sol = naive_pb(p) # Test the special base where PoissonBinomial distribution reduces # to Binomial distribution for (p, n) in [(0.8, 6), (0.5, 10), (0.04, 20)] - local p - d = PoissonBinomial(fill(p, n)) dref = Binomial(n, p) println(" testing PoissonBinomial p=$p, n=$n") @@ -149,3 +149,4 @@ end f = x -> logpdf(PoissonBinomial(x), 0) at = [0.5, 0.5] @test isapprox(ForwardDiff.gradient(f, at), fdm(f, at), atol=1e-6) +end diff --git a/test/samplers.jl b/test/samplers.jl index 0555b1d63..68e042ec1 100644 --- a/test/samplers.jl +++ b/test/samplers.jl @@ -108,7 +108,7 @@ import Distributions: Gamma(0.1, 1.0), Gamma(2.0, 1.0), MatrixNormal(3, 4), - MvNormal(3, 1.0), + MvNormal(zeros(3), I), Normal(1.5, 2.0), Poisson(0.5), ) diff --git a/test/types.jl b/test/types.jl index 4beb0148b..4eefd5d96 100644 --- a/test/types.jl +++ b/test/types.jl @@ -26,14 +26,17 @@ using ForwardDiff: Dual @testset "Test Sample Type" begin for T in (Float64,Float32,Dual{Nothing,Float64,0}) @testset "Type $T" begin - for d in (MvNormal,MvLogNormal,MvNormalCanon,Dirichlet) - dist = d(map(T,ones(2))) - @test eltype(typeof(dist)) == T - @test eltype(rand(dist)) == eltype(dist) + dists = ( + MvNormal(Diagonal(ones(T, 2))), + MvLogNormal(Diagonal(ones(T, 2))), + MvNormalCanon(Diagonal(ones(T, 2))), + Dirichlet(ones(T, 2)), + Distributions.mvtdist(one(T), Matrix{T}(I, 2, 2)), + ) + for dist in dists + @test eltype(typeof(dist)) === T + @test eltype(rand(dist)) === eltype(dist) end - dist = Distributions.mvtdist(map(T,1.0),map(T,[1.0 0.0; 0.0 1.0])) - @test eltype(typeof(dist)) == T - @test eltype(rand(dist)) == eltype(dist) end end end From 0fe87a29a78c4aa15d36f4848fe27d73b070900d Mon Sep 17 00:00:00 2001 From: David Widmann Date: Fri, 27 Aug 2021 09:32:23 +0200 Subject: [PATCH 06/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 83b7b870f..766bdb971 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.12" +version = "0.25.13" [deps] FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" From 59df675409a7e2490e4a45edd32c0267df435c55 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Fri, 27 Aug 2021 16:27:36 +0200 Subject: [PATCH 07/58] Fix #1328 (#1386) * Fix #1328 * Add more tests * Bump version * Update src/truncate.jl * Do not check support (does not work reliably and needs more work) --- Project.toml | 2 +- src/truncate.jl | 63 ++++++++++++++++++++++++------------------------ test/truncate.jl | 24 +++++++++++++++--- 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/Project.toml b/Project.toml index 766bdb971..4ff02a515 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.13" +version = "0.25.14" [deps] FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" diff --git a/src/truncate.jl b/src/truncate.jl index 206172e79..e040cd968 100644 --- a/src/truncate.jl +++ b/src/truncate.jl @@ -1,56 +1,57 @@ """ - truncated(d, l, u): + truncated(d::UnivariateDistribution, l::Real, u::Real) -Truncate a distribution between `l` and `u`. -Builds the most appropriate distribution for the type of `d`, -the fallback is constructing a `Truncated` wrapper. +Truncate a univariate distribution `d` to the interval `[l, u]`. -To implement a specialized truncated form for a distribution `D`, -the method `truncate(d::D, l::T, u::T) where {T <: Real}` -should be implemented. +The lower bound `l` can be finite or `-Inf` and the upper bound `u` can be finite or +`Inf`. The function throws an error if `l > u`. -# Arguments -- `d::UnivariateDistribution`: The original distribution. -- `l::Real`: The lower bound of the truncation, which can be a finite value or `-Inf`. -- `u::Real`: The upper bound of the truncation, which can be a finite value of `Inf`. +The function falls back to constructing a [`Truncated`](@ref) wrapper. -Throws an error if `l >= u`. +# Implementation + +To implement a specialized truncated form for distributions of type `D`, the method +`truncate(d::D, l::T, u::T) where {T <: Real}` should be implemented. """ function truncated(d::UnivariateDistribution, l::Real, u::Real) return truncated(d, promote(l, u)...) end function truncated(d::UnivariateDistribution, l::T, u::T) where {T <: Real} - l < u || error("lower bound should be less than upper bound.") - logcdf_l = logcdf(d, l) - logcdf_u = logcdf(d, u) - lcdf = exp(logcdf_l) - ucdf = exp(logcdf_u) - log_tp = logsubexp(logcdf_l, logcdf_u) - tp = exp(log_tp) - Truncated(d, promote(l, u, lcdf, ucdf, tp, log_tp)...) + l <= u || error("the lower bound must be less or equal than the upper bound") + + # (log)lcdf = (log) P(X < l) where X ~ d + loglcdf = if value_support(typeof(d)) === Discrete + logsubexp(logcdf(d, l), logpdf(d, l)) + else + logcdf(d, l) + end + lcdf = exp(loglcdf) + + # (log)ucdf = (log) P(X ≤ u) where X ~ d + logucdf = logcdf(d, u) + ucdf = exp(logucdf) + + # (log)tp = (log) P(l ≤ X ≤ u) where X ∼ d + logtp = logsubexp(loglcdf, logucdf) + tp = exp(logtp) + + Truncated(d, promote(l, u, lcdf, ucdf, tp, logtp)...) end truncated(d::UnivariateDistribution, l::Integer, u::Integer) = truncated(d, float(l), float(u)) """ - Truncated(d, l, u): - -Create a generic wrapper for a truncated distribution. -Prefer calling the function `truncated(d, l, u)`, which can choose the appropriate -representation of the truncated distribution. + Truncated -# Arguments -- `d::UnivariateDistribution`: The original distribution. -- `l::Real`: The lower bound of the truncation, which can be a finite value or `-Inf`. -- `u::Real`: The upper bound of the truncation, which can be a finite value of `Inf`. +Generic wrapper for a truncated distribution. """ struct Truncated{D<:UnivariateDistribution, S<:ValueSupport, T <: Real} <: UnivariateDistribution{S} untruncated::D # the original distribution (untruncated) lower::T # lower bound upper::T # upper bound - lcdf::T # cdf of lower bound - ucdf::T # cdf of upper bound + lcdf::T # cdf of lower bound (exclusive): P(X < lower) + ucdf::T # cdf of upper bound (inclusive): P(X ≤ upper) tp::T # the probability of the truncated part, i.e. ucdf - lcdf logtp::T # log(tp), i.e. log(ucdf - lcdf) diff --git a/test/truncate.jl b/test/truncate.jl index 85ae457bd..12bfefc22 100644 --- a/test/truncate.jl +++ b/test/truncate.jl @@ -4,6 +4,7 @@ module TestTruncate using Distributions using ForwardDiff: Dual, ForwardDiff +using StatsFuns import JSON using Test using ..Main: fdm @@ -71,11 +72,11 @@ function verify_and_test(d::UnivariateDistribution, dct::Dict, n_tsamples::Int) for pt in pts x = _parse_x(d, pt["x"]) lp = d.lower <= x <= d.upper ? Float64(pt["logpdf"]) - d.logtp : -Inf - cf = x <= d.lower ? 0.0 : x >= d.upper ? 1.0 : (Float64(pt["cdf"]) - d.lcdf)/d.tp + cf = x < d.lower ? 0.0 : x >= d.upper ? 1.0 : (Float64(pt["cdf"]) - d.lcdf)/d.tp if !isa(d, Distributions.Truncated{Distributions.StudentizedRange{Float64},Distributions.Continuous}) - @test isapprox(logpdf(d, x), lp, atol=sqrt(eps())) + @test logpdf(d, x) ≈ lp atol=sqrt(eps()) end - @test isapprox(cdf(d, x) , cf, atol=sqrt(eps())) + @test cdf(d, x) ≈ cf atol=sqrt(eps()) # NOTE: some distributions use pdf() in StatsFuns.jl which have no generic support yet if !(typeof(d) in [Distributions.Truncated{Distributions.NoncentralChisq{Float64},Distributions.Continuous, Float64}, Distributions.Truncated{Distributions.NoncentralF{Float64},Distributions.Continuous, Float64}, @@ -141,4 +142,21 @@ f = x -> logpdf(truncated(Normal(x[1], x[2]), x[3], x[4]), mean(x)) at = [0.0, 1.0, 0.0, 1.0] @test isapprox(ForwardDiff.gradient(f, at), fdm(f, at), atol=1e-6) + @testset "errors" begin + @test_throws ErrorException truncated(Normal(), 1, 0) + @test_throws ArgumentError truncated(Uniform(), 1, 2) + @test_throws ErrorException truncated(Exponential(), 3, 1) + end + + @testset "#1328" begin + dist = Poisson(2.0) + dist_zeroinflated = MixtureModel([Dirac(0.0), dist], [0.4, 0.6]) + dist_zerotruncated = truncated(dist, 1, Inf) + dist_zeromodified = MixtureModel([Dirac(0.0), dist_zerotruncated], [0.4, 0.6]) + + @test logsumexp(logpdf(dist, x) for x in 0:1000) ≈ 0 atol=1e-15 + @test logsumexp(logpdf(dist_zeroinflated, x) for x in 0:1000) ≈ 0 atol=1e-15 + @test logsumexp(logpdf(dist_zerotruncated, x) for x in 0:1000) ≈ 0 atol=1e-15 + @test logsumexp(logpdf(dist_zeromodified, x) for x in 0:1000) ≈ 0 atol=1e-15 + end end From 0cda70a821a0b8c6a2b6ba3c20f79c03acb8647a Mon Sep 17 00:00:00 2001 From: David Widmann Date: Mon, 30 Aug 2021 22:08:48 +0200 Subject: [PATCH 08/58] Add ChainRules definitions for pdf of `PoissonBinomial` (#1390) * Add ChainRules definitions for pdf of `PoissonBinomial` * Add `frule`s --- Project.toml | 5 +- src/Distributions.jl | 2 + src/univariate/discrete/poissonbinomial.jl | 67 ++++++++++++++++++++++ test/poissonbinomial.jl | 17 ++++-- 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/Project.toml b/Project.toml index 4ff02a515..c646945cb 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["JuliaStats"] version = "0.25.14" [deps] +ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" PDMats = "90014a1f-27ba-587c-ab20-58faa44d9150" @@ -17,6 +18,7 @@ StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" StatsFuns = "4c63d2b9-4356-54db-8cca-17b64c39e42c" [compat] +ChainRulesCore = "1" FillArrays = "0.9, 0.10, 0.11, 0.12" PDMats = "0.10, 0.11" QuadGK = "2" @@ -27,6 +29,7 @@ julia = "1" [extras] Calculus = "49dc2e85-a5d0-5ad3-a950-438e2897f1b9" +ChainRulesTestUtils = "cdddcdb0-9152-4a09-a978-84456f9df70a" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" @@ -36,4 +39,4 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["StableRNGs", "Calculus", "Distributed", "FiniteDifferences", "ForwardDiff", "JSON", "StaticArrays", "Test"] +test = ["StableRNGs", "Calculus", "ChainRulesTestUtils", "Distributed", "FiniteDifferences", "ForwardDiff", "JSON", "StaticArrays", "Test"] diff --git a/src/Distributions.jl b/src/Distributions.jl index 72005e7f8..d33a02655 100644 --- a/src/Distributions.jl +++ b/src/Distributions.jl @@ -25,6 +25,8 @@ import PDMats: dim, PDMat, invquad using SpecialFunctions +import ChainRulesCore + export # re-export Statistics mean, median, quantile, std, var, cov, cor, diff --git a/src/univariate/discrete/poissonbinomial.jl b/src/univariate/discrete/poissonbinomial.jl index a8ed3c0fe..5cd0ddaeb 100644 --- a/src/univariate/discrete/poissonbinomial.jl +++ b/src/univariate/discrete/poissonbinomial.jl @@ -201,3 +201,70 @@ end #### Sampling sampler(d::PoissonBinomial) = PoissBinAliasSampler(d) + +## ChainRules definitions + +# Compute matrix of partial derivatives [∂P(X=j-1)/∂pᵢ]_{i=1,…,n; j=1,…,n+1} +# +# This implementation uses the same dynamic programming "trick" as for the computation of +# the primals. +# +# Reference (for the primal): +# +# Marlin A. Thomas & Audrey E. Taub (1982) +# Calculating binomial probabilities when the trial probabilities are unequal, +# Journal of Statistical Computation and Simulation, 14:2, 125-131, DOI: 10.1080/00949658208810534 +function poissonbinomial_pdf_partialderivatives(p::AbstractVector{<:Real}) + n = length(p) + A = zeros(eltype(p), n, n + 1) + @inbounds for j in 1:n + A[j, end] = 1 + end + @inbounds for (i, pi) in enumerate(p) + qi = 1 - pi + for k in (n - i + 1):n + kp1 = k + 1 + for j in 1:(i - 1) + A[j, k] = pi * A[j, k] + qi * A[j, kp1] + end + for j in (i+1):n + A[j, k] = pi * A[j, k] + qi * A[j, kp1] + end + end + for j in 1:(i-1) + A[j, end] *= pi + end + for j in (i+1):n + A[j, end] *= pi + end + end + @inbounds for j in 1:n, i in 1:n + A[i, j] -= A[i, j+1] + end + return A +end + +for f in (:poissonbinomial_pdf, :poissonbinomial_pdf_fft) + pullback = Symbol(f, :_pullback) + @eval begin + function ChainRulesCore.frule( + (_, Δp)::Tuple{<:Any,<:AbstractVector{<:Real}}, ::typeof($f), p::AbstractVector{<:Real} + ) + y = $f(p) + A = poissonbinomial_pdf_partialderivatives(p) + return y, A' * Δp + end + function ChainRulesCore.rrule(::typeof($f), p::AbstractVector{<:Real}) + y = $f(p) + A = poissonbinomial_pdf_partialderivatives(p) + function $pullback(Δy) + p̄ = ChainRulesCore.InplaceableThunk( + Δ -> LinearAlgebra.mul!(Δ, A, Δy, true, true), + ChainRulesCore.@thunk(A * Δy), + ) + return ChainRulesCore.NoTangent(), p̄ + end + return y, $pullback + end + end +end diff --git a/test/poissonbinomial.jl b/test/poissonbinomial.jl index 98f4e403d..5f783794d 100644 --- a/test/poissonbinomial.jl +++ b/test/poissonbinomial.jl @@ -1,4 +1,5 @@ using Distributions +using ChainRulesTestUtils using ForwardDiff using Test @@ -145,8 +146,16 @@ end @test x ≈ fftw_fft end -# Test autodiff using ForwardDiff -f = x -> logpdf(PoissonBinomial(x), 0) -at = [0.5, 0.5] -@test isapprox(ForwardDiff.gradient(f, at), fdm(f, at), atol=1e-6) +@testset "automatic differentiation" begin + # Test autodiff using ForwardDiff + f = x -> logpdf(PoissonBinomial(x), 0) + at = [0.5, 0.5] + @test isapprox(ForwardDiff.gradient(f, at), fdm(f, at), atol=1e-6) + + # Test ChainRules definition + for f in (Distributions.poissonbinomial_pdf, Distributions.poissonbinomial_pdf_fft) + test_frule(f, rand(50)) + test_rrule(f, rand(50)) + end +end end From 39f989973bf5b67a2fe7fb099a17b751083a30c3 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Mon, 30 Aug 2021 22:13:49 +0200 Subject: [PATCH 09/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index c646945cb..3f4ac3f1a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.14" +version = "0.25.15" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From 3e2a69779ce3e0ffe68455cf04f0c5817aca905b Mon Sep 17 00:00:00 2001 From: David Widmann Date: Thu, 2 Sep 2021 08:33:50 +0200 Subject: [PATCH 10/58] Fix type stability issues of `DiscreteNonParametric` (#1394) --- .../discrete/discretenonparametric.jl | 61 ++++--------------- test/discretenonparametric.jl | 7 +++ 2 files changed, 20 insertions(+), 48 deletions(-) diff --git a/src/univariate/discrete/discretenonparametric.jl b/src/univariate/discrete/discretenonparametric.jl index 590fa54af..e54a0789f 100644 --- a/src/univariate/discrete/discretenonparametric.jl +++ b/src/univariate/discrete/discretenonparametric.jl @@ -178,71 +178,36 @@ insupport(d::DiscreteNonParametric, x::Real) = mean(d::DiscreteNonParametric) = dot(probs(d), support(d)) -function var(d::DiscreteNonParametric{T}) where T - m = mean(d) +function var(d::DiscreteNonParametric) x = support(d) p = probs(d) - k = length(x) - σ² = zero(T) - for i in 1:k - @inbounds σ² += abs2(x[i] - m) * p[i] - end - σ² + return var(x, Weights(p, one(eltype(p))); corrected=false) end -function skewness(d::DiscreteNonParametric{T}) where T - m = mean(d) +function skewness(d::DiscreteNonParametric) x = support(d) p = probs(d) - k = length(x) - μ₃ = zero(T) - σ² = zero(T) - @inbounds for i in 1:k - d = x[i] - m - d²w = abs2(d) * p[i] - μ₃ += d * d²w - σ² += d²w - end - μ₃ / (σ² * sqrt(σ²)) + return skewness(x, Weights(p, one(eltype(p)))) end -function kurtosis(d::DiscreteNonParametric{T}) where T - m = mean(d) +function kurtosis(d::DiscreteNonParametric) x = support(d) p = probs(d) - k = length(x) - μ₄ = zero(T) - σ² = zero(T) - @inbounds for i in 1:k - d² = abs2(x[i] - m) - d²w = d² * p[i] - μ₄ += d² * d²w - σ² += d²w - end - μ₄ / abs2(σ²) - 3 + return kurtosis(x, Weights(p, one(eltype(p)))) end entropy(d::DiscreteNonParametric) = entropy(probs(d)) entropy(d::DiscreteNonParametric, b::Real) = entropy(probs(d), b) -mode(d::DiscreteNonParametric) = support(d)[argmax(probs(d))] -function modes(d::DiscreteNonParametric{T,P}) where {T,P} +function mode(d::DiscreteNonParametric) x = support(d) p = probs(d) - k = length(x) - mds = T[] - max_p = zero(P) - @inbounds for i in 1:k - pi = p[i] - xi = x[i] - if pi > max_p - max_p = pi - mds = [xi] - elseif pi == max_p - push!(mds, xi) - end - end - mds + return mode(x, Weights(p, one(eltype(p)))) +end +function modes(d::DiscreteNonParametric) + x = support(d) + p = probs(d) + return modes(x, Weights(p, one(eltype(p)))) end function mgf(d::DiscreteNonParametric, t::Real) diff --git a/test/discretenonparametric.jl b/test/discretenonparametric.jl index 504fa84fa..52acbfa25 100644 --- a/test/discretenonparametric.jl +++ b/test/discretenonparametric.jl @@ -163,4 +163,11 @@ d = DiscreteNonParametric([1, 2], [0, 1]) d = DiscreteNonParametric([2, 1], [1, 0]) @test iszero(count(isone(rand(d)) for _ in 1:100)) + + @testset "type stability" begin + d = DiscreteNonParametric([0, 1], [0.5, 0.5]) + @inferred(var(d)) + @inferred(kurtosis(d)) + @inferred(skewness(d)) + end end From ec8f7d82c59348443f799e0fb0394441fab31073 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Thu, 2 Sep 2021 11:07:26 +0200 Subject: [PATCH 11/58] Implement `quantile` for univariate mixture models (#1389) * implement quantile for Mixture{Univariate,Continuous} * handle case MixtureModel([Normal(), Normal()]) correctly * Update src/mixtures/mixturemodel.jl Co-authored-by: David Widmann * try to fix type instability * use Base.rtoldefault * Simplify implementation * Bump version * Fix problems with `Float32` * Define `median` for mixture models * Add tests Co-authored-by: Nikos Ignatiadis --- src/mixtures/mixturemodel.jl | 14 ++++++++++++ src/quantilealgs.jl | 12 +++++++---- src/univariate/continuous/normal.jl | 4 ++-- src/univariates.jl | 2 +- test/mixture.jl | 33 ++++++++++++++++++++++++++++- test/normal.jl | 4 ++++ 6 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/mixtures/mixturemodel.jl b/src/mixtures/mixturemodel.jl index 230412e29..7ced3ff1f 100644 --- a/src/mixtures/mixturemodel.jl +++ b/src/mixtures/mixturemodel.jl @@ -441,6 +441,20 @@ componentwise_logpdf(d::MultivariateMixture, x::AbstractVector) = componentwise_ componentwise_logpdf(d::MultivariateMixture, x::AbstractMatrix) = componentwise_logpdf!(Matrix{eltype(x)}(undef, size(x,2), ncomponents(d)), d, x) +function quantile(d::UnivariateMixture{Continuous}, p::Real) + ps = probs(d) + min_q, max_q = extrema(quantile(component(d, i), p) for (i, pi) in enumerate(ps) if pi > 0) + quantile_bisect(d, p, min_q, max_q) +end + +# we also implement `median` since `median` is implemented more efficiently than +# `quantile(d, 1//2)` for some distributions +function median(d::UnivariateMixture{Continuous}) + ps = probs(d) + min_q, max_q = extrema(median(component(d, i)) for (i, pi) in enumerate(ps) if pi > 0) + quantile_bisect(d, 1//2, min_q, max_q) +end + ## Sampling struct MixtureSampler{VF,VS,Sampler} <: Sampleable{VF,VS} diff --git a/src/quantilealgs.jl b/src/quantilealgs.jl index 1de57e79d..4cbd70f7a 100644 --- a/src/quantilealgs.jl +++ b/src/quantilealgs.jl @@ -1,8 +1,12 @@ # Various algorithms for computing quantile -function quantile_bisect(d::ContinuousUnivariateDistribution, p::Real, - lx::Real, rx::Real, tol::Real) - +function quantile_bisect( + d::ContinuousUnivariateDistribution, p::Real, lx::Real, rx::Real, + # base tolerance on types to support e.g. `Float32` (avoids an infinite loop) + # ≈ 3.7e-11 for Float64 + # ≈ 2.4e-5 for Float32 + tol::Real=(eps(Base.promote_typeof(float(lx), float(rx))))^(2 / 3), +) # find quantile using bisect algorithm cl = cdf(d, lx) cr = cdf(d, rx) @@ -22,7 +26,7 @@ function quantile_bisect(d::ContinuousUnivariateDistribution, p::Real, end quantile_bisect(d::ContinuousUnivariateDistribution, p::Real) = - quantile_bisect(d, p, minimum(d), maximum(d), 1.0e-12) + quantile_bisect(d, p, minimum(d), maximum(d)) # if starting at mode, Newton is convergent for any unimodal continuous distribution, see: # Göknur Giner, Gordon K. Smyth (2014) diff --git a/src/univariate/continuous/normal.jl b/src/univariate/continuous/normal.jl index 9612db7a6..c4c646666 100644 --- a/src/univariate/continuous/normal.jl +++ b/src/univariate/continuous/normal.jl @@ -198,7 +198,7 @@ end # quantile function quantile(d::Normal, p::Real) # Promote to ensure that we don't compute erfcinv in low precision and then promote - _p, _μ, _σ = promote(float(p), d.μ, d.σ) + _p, _μ, _σ = map(float, promote(p, d.μ, d.σ)) q = xval(d, -erfcinv(2*_p) * sqrt2) if isnan(_p) return oftype(q, _p) @@ -218,7 +218,7 @@ end # cquantile function cquantile(d::Normal, p::Real) # Promote to ensure that we don't compute erfcinv in low precision and then promote - _p, _μ, _σ = promote(float(p), d.μ, d.σ) + _p, _μ, _σ = map(float, promote(p, d.μ, d.σ)) q = xval(d, erfcinv(2*_p) * sqrt2) if isnan(_p) return oftype(q, _p) diff --git a/src/univariates.jl b/src/univariates.jl index f0b992087..23287a1e5 100644 --- a/src/univariates.jl +++ b/src/univariates.jl @@ -216,7 +216,7 @@ std(d::UnivariateDistribution) = sqrt(var(d)) Return the median value of distribution `d`. """ -median(d::UnivariateDistribution) = quantile(d, 0.5) +median(d::UnivariateDistribution) = quantile(d, 1//2) """ modes(d::UnivariateDistribution) diff --git a/test/mixture.jl b/test/mixture.jl index b23a4488b..8abefb876 100644 --- a/test/mixture.jl +++ b/test/mixture.jl @@ -69,8 +69,17 @@ function test_mixture(g::UnivariateMixture, n::Int, ns::Int, @test @inferred(componentwise_pdf(g, X)) ≈ P0 @test @inferred(componentwise_logpdf(g, X)) ≈ LP0 + # quantile + αs = float(partype(g))[0.0; 0.49; 0.5; 0.51; 1.0] + for α in αs + @test cdf(g, @inferred(quantile(g, α))) ≈ α + end + @test @inferred(median(g)) ≈ quantile(g, 1//2) + # sampling - if (T <: AbstractFloat) + # sampling does not work with `Float32` since `AliasTable` does not support `Float32` + # Ref: https://github.com/JuliaStats/StatsBase.jl/issues/158 + if T <: AbstractFloat && eltype(probs(g)) === Float64 if ismissing(rng) Xs = rand(g, ns) else @@ -172,6 +181,17 @@ end "rand(rng, ...)" => MersenneTwister(123)) @testset "Testing UnivariateMixture" begin + g_u = MixtureModel([Normal(), Normal()]) + @test isa(g_u, MixtureModel{Univariate, Continuous, <:Normal}) + @test ncomponents(g_u) == 2 + test_mixture(g_u, 1000, 10^6, rng) + test_params(g_u) + @test minimum(g_u) == -Inf + @test maximum(g_u) == Inf + @test extrema(g_u) == (-Inf, Inf) + @test @inferred(median(g_u)) === 0.0 + @test @inferred(quantile(g_u, 0.5f0)) === 0.0 + g_u = MixtureModel(Normal{Float64}, [(0.0, 1.0), (2.0, 1.0), (-4.0, 1.5)], [0.2, 0.5, 0.3]) @test isa(g_u, MixtureModel{Univariate,Continuous,<:Normal}) @test ncomponents(g_u) == 3 @@ -181,6 +201,17 @@ end @test maximum(g_u) == Inf @test extrema(g_u) == (-Inf, Inf) + g_u = MixtureModel(Normal{Float32}, [(0f0, 1f0), (0f0, 2f0)], [0.4f0, 0.6f0]) + @test isa(g_u, MixtureModel{Univariate,Continuous,<:Normal}) + @test ncomponents(g_u) == 2 + test_mixture(g_u, 1000, 10^6, rng) + test_params(g_u) + @test minimum(g_u) == -Inf + @test maximum(g_u) == Inf + @test extrema(g_u) == (-Inf, Inf) + @test @inferred(median(g_u)) === 0f0 + @test @inferred(quantile(g_u, 0.5f0)) === 0f0 + g_u = MixtureModel([TriangularDist(-1,2,0),TriangularDist(-.5,3,1),TriangularDist(-2,0,-1)]) @test minimum(g_u) ≈ -2.0 @test maximum(g_u) ≈ 3.0 diff --git a/test/normal.jl b/test/normal.jl index f3a79c63a..0a261ccc3 100644 --- a/test/normal.jl +++ b/test/normal.jl @@ -150,6 +150,8 @@ end @test @inferred(quantile(Normal(1.0f0, 0.0f0), 0.5f0)) === 1.0f0 @test isnan_type(Float32, @inferred(quantile(Normal(1.0f0, 0.0f0), NaN32))) @test @inferred(quantile(Normal(1//1, 0//1), 1//2)) === 1.0 + @test @inferred(quantile(Normal(1f0, 0f0), 1//2)) === 1f0 + @test @inferred(quantile(Normal(1f0, 0.0), 1//2)) === 1.0 @test @inferred(cquantile(Normal(1.0, 0.0), 0.0f0)) === Inf @test @inferred(cquantile(Normal(1.0, 0.0f0), 1.0)) === -Inf @@ -160,6 +162,8 @@ end @test @inferred(cquantile(Normal(1.0f0, 0.0f0), 0.5f0)) === 1.0f0 @test isnan_type(Float32, @inferred(cquantile(Normal(1.0f0, 0.0f0), NaN32))) @test @inferred(cquantile(Normal(1//1, 0//1), 1//2)) === 1.0 + @test @inferred(cquantile(Normal(1f0, 0f0), 1//2)) === 1f0 + @test @inferred(cquantile(Normal(1f0, 0.0), 1//2)) === 1.0 end @testset "Normal: Sampling with integer-valued parameters" begin From ed52948fb47e7e0aecaf81e99ebb090c2738cf0e Mon Sep 17 00:00:00 2001 From: David Widmann Date: Thu, 2 Sep 2021 11:10:33 +0200 Subject: [PATCH 12/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 3f4ac3f1a..2cd3a5c2c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.15" +version = "0.25.16" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From 9ceb9a04e4e8c8e0cdd4f45f4c51492151d822fa Mon Sep 17 00:00:00 2001 From: David Widmann Date: Wed, 29 Sep 2021 11:37:58 +0200 Subject: [PATCH 13/58] Fix tests (#1398) --- test/poissonbinomial.jl | 8 ++++---- test/univariate_bounds.jl | 18 +++--------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/test/poissonbinomial.jl b/test/poissonbinomial.jl index 5f783794d..8e53e6edd 100644 --- a/test/poissonbinomial.jl +++ b/test/poissonbinomial.jl @@ -63,8 +63,8 @@ for (p, n) in [(0.8, 6), (0.5, 10), (0.04, 20)] @test @inferred(quantile(d, i)) ≈ quantile(dref, i) end for i=0:n - @test @inferred(pdf(d, i)) ≈ pdf(dref, i) atol=1e-15 - @test @inferred(pdf(d, i//1)) ≈ pdf(dref, i) atol=1e-15 + @test @inferred(pdf(d, i)) ≈ pdf(dref, i) atol=1e-14 + @test @inferred(pdf(d, i//1)) ≈ pdf(dref, i) atol=1e-14 @test @inferred(logpdf(d, i)) ≈ logpdf(dref, i) @test @inferred(logpdf(d, i//1)) ≈ logpdf(dref, i) for f in (cdf, ccdf, logcdf, logccdf) @@ -124,8 +124,8 @@ for (n₁, n₂, n₃, p₁, p₂, p₃) in [(10, 10, 10, 0.1, 0.5, 0.9), end m += pmf1[i+1] * mc end - @test @inferred(pdf(d, k)) ≈ m atol=1e-15 - @test @inferred(pdf(d, k//1)) ≈ m atol=1e-15 + @test @inferred(pdf(d, k)) ≈ m atol=1e-14 + @test @inferred(pdf(d, k//1)) ≈ m atol=1e-14 @test @inferred(logpdf(d, k)) ≈ log(m) @test @inferred(logpdf(d, k//1)) ≈ log(m) end diff --git a/test/univariate_bounds.jl b/test/univariate_bounds.jl index 89008a272..275eb112c 100644 --- a/test/univariate_bounds.jl +++ b/test/univariate_bounds.jl @@ -91,24 +91,12 @@ filter!(x -> isbounded(x()), dists) @test isnan(logccdf(d, NaN)) @test iszero(pdf(d, lb)) - if dist === Binomial - # https://github.com/JuliaStats/StatsFuns.jl/issues/115 - @test_broken iszero(pdf(d, ub)) - @test iszero(pdf(d, ub + 1e-6)) - else - @test iszero(pdf(d, ub)) - end + @test iszero(pdf(d, ub)) lb_lpdf = logpdf(d, lb) - @test isinf(lb_lpdf) & (lb_lpdf < 0) + @test isinf(lb_lpdf) && lb_lpdf < 0 ub_lpdf = logpdf(d, ub) - if dist === Binomial - # https://github.com/JuliaStats/StatsFuns.jl/issues/115 - @test_broken isinf(ub_lpdf) & (ub_lpdf < 0) - @test logpdf(d, ub + 1e-6) == -Inf - else - @test isinf(ub_lpdf) & (ub_lpdf < 0) - end + @test isinf(ub_lpdf) && ub_lpdf < 0 @test logpdf(d, -Inf) == -Inf @test logpdf(d, Inf) == -Inf end From 9b98caac2f5e419f27a5d6ce4eccdfb96556ac8a Mon Sep 17 00:00:00 2001 From: Mandar Chitre Date: Fri, 1 Oct 2021 06:18:30 +0800 Subject: [PATCH 14/58] Add Rician distribution (#1387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Rician distribution * Removed Rician Integer constructor based on review Co-authored-by: David Widmann * Remove Rician inner constructor based on review suggestion Co-authored-by: David Widmann * Removed @inline as suggested during review Co-authored-by: David Widmann * Type improvement based on review Co-authored-by: David Widmann * Removed duplicate implementation for Rician pdf Co-authored-by: David Widmann * Removed Rician keyword constructor * Type stability improvments for Rician * Add back Rician Integer constructor to ensure tests pass * Fixed Rician broken test cases * Updated Rician implementation to use NoncentralChisq * Improve type stability in Rician Co-authored-by: David Widmann * Improve type stability in Rician Co-authored-by: David Widmann * Improve type stability in Rician Co-authored-by: David Widmann * Improve type stability in Rician Co-authored-by: David Widmann * Avoid type conversion in Rician Co-authored-by: David Widmann * Improve type stability in Rician Co-authored-by: David Widmann * Fixes edge cases without type instability in Rician * Use StatsFuns.sqrthalfπ in Rician Co-authored-by: David Widmann * Use StatsFuns.halfπ in Rician Co-authored-by: David Widmann * Remove forcing of Float64 in Rician Co-authored-by: David Widmann * Remove forcing of Float64 in Rician Co-authored-by: David Widmann * Force same random numbers in Rician tests * Increase tolerence on Rician random tests Co-authored-by: David Widmann Co-authored-by: David Widmann --- docs/src/univariate.md | 1 + src/Distributions.jl | 3 +- src/univariate/continuous/rician.jl | 171 ++++++++++++++++++++++++++++ src/univariates.jl | 1 + test/ref/continuous/rician.R | 23 ++++ test/ref/continuous_test.lst | 4 + test/ref/continuous_test.ref.json | 84 ++++++++++++++ test/ref/rdistributions.R | 1 + test/rician.jl | 97 ++++++++++++++++ test/runtests.jl | 1 + test/truncate.jl | 3 +- 11 files changed, 387 insertions(+), 2 deletions(-) create mode 100644 src/univariate/continuous/rician.jl create mode 100644 test/ref/continuous/rician.R create mode 100644 test/rician.jl diff --git a/docs/src/univariate.md b/docs/src/univariate.md index 0418a38fa..b2bdf6290 100644 --- a/docs/src/univariate.md +++ b/docs/src/univariate.md @@ -132,6 +132,7 @@ NormalInverseGaussian Pareto PGeneralizedGaussian Rayleigh +Rician Semicircle StudentizedRange SymTriangularDist diff --git a/src/Distributions.jl b/src/Distributions.jl index d33a02655..dfa0231dd 100644 --- a/src/Distributions.jl +++ b/src/Distributions.jl @@ -144,6 +144,7 @@ export PoissonBinomial, QQPair, Rayleigh, + Rician, Semicircle, Skellam, SkewNormal, @@ -331,7 +332,7 @@ Supported distributions: MvNormalKnownCov, MvTDist, NegativeBinomial, NoncentralBeta, NoncentralChisq, NoncentralF, NoncentralHypergeometric, NoncentralT, Normal, NormalCanon, NormalInverseGaussian, Pareto, PGeneralizedGaussian, Poisson, PoissonBinomial, - QQPair, Rayleigh, Skellam, Soliton, StudentizedRange, SymTriangularDist, TDist, TriangularDist, + QQPair, Rayleigh, Rician, Skellam, Soliton, StudentizedRange, SymTriangularDist, TDist, TriangularDist, Triweight, Truncated, TruncatedNormal, Uniform, UnivariateGMM, VonMises, VonMisesFisher, WalleniusNoncentralHypergeometric, Weibull, Wishart, ZeroMeanIsoNormal, ZeroMeanIsoNormalCanon, diff --git a/src/univariate/continuous/rician.jl b/src/univariate/continuous/rician.jl new file mode 100644 index 000000000..9a5ccdb59 --- /dev/null +++ b/src/univariate/continuous/rician.jl @@ -0,0 +1,171 @@ +""" + Rician(ν, σ) + +The *Rician distribution* with parameters `ν` and `σ` has probability density function: + +```math +f(x; \\nu, \\sigma) = \\frac{x}{\\sigma^2} \\exp\\left( \\frac{-(x^2 + \\nu^2)}{2\\sigma^2} \\right) I_0\\left( \\frac{x\\nu}{\\sigma^2} \\right). +``` + +If shape and scale parameters `K` and `Ω` are given instead, `ν` and `σ` may be computed from them: + +```math +\\sigma = \\sqrt{\\frac{\\Omega}{2(K + 1)}}, \\quad \\nu = \\sigma\\sqrt{2K} +``` + +```julia +Rician() # Rician distribution with parameters ν=0 and σ=1 +Rician(ν, σ) # Rician distribution with parameters ν and σ + +params(d) # Get the parameters, i.e. (ν, σ) +shape(d) # Get the shape parameter K = ν²/2σ² +scale(d) # Get the scale parameter Ω = ν² + 2σ² +``` + +External links: + +* [Rician distribution on Wikipedia](https://en.wikipedia.org/wiki/Rice_distribution) + +""" +struct Rician{T<:Real} <: ContinuousUnivariateDistribution + ν::T + σ::T + Rician{T}(ν, σ) where {T} = new{T}(ν, σ) +end + +function Rician(ν::T, σ::T; check_args=true) where {T<:Real} + check_args && @check_args(Rician, ν ≥ zero(ν) && σ ≥ zero(σ)) + return Rician{T}(ν, σ) +end + +Rician() = Rician(0.0, 1.0) +Rician(ν::Real, σ::Real) = Rician(promote(ν, σ)...) +Rician(ν::Integer, σ::Integer) = Rician(float(ν), float(σ)) + +@distr_support Rician 0.0 Inf + +#### Conversions + +function convert(::Type{Rician{T}}, ν::Real, σ::Real) where T<:Real + Rician(T(ν), T(σ)) +end + +function convert(::Type{Rician{T}}, d::Rician{S}) where {T <: Real, S <: Real} + Rician(T(d.ν), T(d.σ); check_args=false) +end + +#### Parameters + +shape(d::Rician) = d.ν^2 / (2 * d.σ^2) +scale(d::Rician) = d.ν^2 + 2 * d.σ^2 + +params(d::Rician) = (d.ν, d.σ) +partype(d::Rician{T}) where {T<:Real} = T + +#### Statistics + +# helper +_Lhalf(x) = exp(x/2) * ((1-x) * besseli(zero(x), -x/2) - x * besseli(oneunit(x), -x/2)) + +mean(d::Rician) = d.σ * sqrthalfπ * _Lhalf(-d.ν^2/(2 * d.σ^2)) +var(d::Rician) = 2 * d.σ^2 + d.ν^2 - halfπ * d.σ^2 * _Lhalf(-d.ν^2/(2 * d.σ^2))^2 + +function mode(d::Rician) + m = mean(d) + _minimize_gss(x -> -pdf(d, x), zero(m), m) +end + +# helper: 1D minimization using Golden-section search +function _minimize_gss(f, a, b; tol=1e-12) + ϕ = (√5 + 1) / 2 + c = b - (b - a) / ϕ + d = a + (b - a) / ϕ + while abs(b - a) > tol + if f(c) < f(d) + b = d + else + a = c + end + c = b - (b - a) / ϕ + d = a + (b - a) / ϕ + end + (b + a) / 2 +end + +#### PDF/CDF/quantile delegated to NoncentralChisq + +function quantile(d::Rician, x::Real) + ν, σ = params(d) + return sqrt(quantile(NoncentralChisq(2, (ν / σ)^2), x)) * σ +end + +function cquantile(d::Rician, x::Real) + ν, σ = params(d) + return sqrt(cquantile(NoncentralChisq(2, (ν / σ)^2), x)) * σ +end + +function pdf(d::Rician, x::Real) + ν, σ = params(d) + result = 2 * x / σ^2 * pdf(NoncentralChisq(2, (ν / σ)^2), (x / σ)^2) + return x < 0 || isinf(x) ? zero(result) : result +end + +function logpdf(d::Rician, x::Real) + ν, σ = params(d) + result = log(2 * abs(x) / σ^2) + logpdf(NoncentralChisq(2, (ν / σ)^2), (x / σ)^2) + return x < 0 || isinf(x) ? oftype(result, -Inf) : result +end + +function cdf(d::Rician, x::Real) + ν, σ = params(d) + result = cdf(NoncentralChisq(2, (ν / σ)^2), (x / σ)^2) + return x < 0 ? zero(result) : result +end + +function logcdf(d::Rician, x::Real) + ν, σ = params(d) + result = logcdf(NoncentralChisq(2, (ν / σ)^2), (x / σ)^2) + return x < 0 ? oftype(result, -Inf) : result +end + +#### Sampling + +function rand(rng::AbstractRNG, d::Rician) + x = randn(rng) * d.σ + d.ν + y = randn(rng) * d.σ + hypot(x, y) +end + +#### Fitting + +# implementation based on the Koay inversion technique +function fit(::Type{<:Rician}, x::AbstractArray{T}; tol=1e-12, maxiters=500) where T + μ₁ = mean(x) + μ₂ = var(x) + r = μ₁ / √μ₂ + if r < sqrt(π/(4-π)) + ν = zero(float(T)) + σ = scale(fit(Rayleigh, x)) + else + ξ(θ) = 2 + θ^2 - π/8 * exp(-θ^2 / 2) * ((2 + θ^2) * besseli(0, θ^2 / 4) + θ^2 * besseli(1, θ^2 / 4))^2 + g(θ) = sqrt(ξ(θ) * (1+r^2) - 2) + θ = g(1) + for j in 1:maxiters + θ⁻ = θ + θ = g(θ) + abs(θ - θ⁻) < tol && break + end + ξθ = ξ(θ) + σ = convert(float(T), sqrt(μ₂ / ξθ)) + ν = convert(float(T), sqrt(μ₁^2 + (ξθ - 2) * σ^2)) + end + Rician(ν, σ) +end + +# Not implemented: +# skewness(d::Rician) +# kurtosis(d::Rician) +# entropy(d::Rician) +# mgf(d::Rician, t::Real) +# cf(d::Rician, t::Real) + diff --git a/src/univariates.jl b/src/univariates.jl index 23287a1e5..73c6eddff 100644 --- a/src/univariates.jl +++ b/src/univariates.jl @@ -722,6 +722,7 @@ const continuous_distributions = [ "logitnormal", # LogitNormal depends on Normal "pareto", "rayleigh", + "rician", "semicircle", "skewnormal", "studentizedrange", diff --git a/test/ref/continuous/rician.R b/test/ref/continuous/rician.R new file mode 100644 index 000000000..3214f360b --- /dev/null +++ b/test/ref/continuous/rician.R @@ -0,0 +1,23 @@ + +Rician <- R6Class("Rician", + inherit = ContinuousDistribution, + public = list( + names = c("nu", "sigma"), + nu = NA, + sigma = NA, + initialize = function(n=0, s=1) { + self$nu <- n + self$sigma <- s + }, + supp = function() { c(0, Inf) }, + properties = function() { + n <- self$nu + s <- self$sigma + list(scale = n^2 + 2 * s^2, + shape = n^2 / (2 * s^2)) + }, + pdf = function(x, log=FALSE) { VGAM::drice(x, self$sigma, self$nu, log=log) }, + cdf = function(x){ VGAM::price(x, self$sigma, self$nu) }, + quan = function(v){ VGAM::qrice(v, self$sigma, self$nu) } + ) +) diff --git a/test/ref/continuous_test.lst b/test/ref/continuous_test.lst index eaef0e4e0..381a1c466 100644 --- a/test/ref/continuous_test.lst +++ b/test/ref/continuous_test.lst @@ -149,6 +149,10 @@ Rayleigh() Rayleigh(3.0) Rayleigh(8.0) +Rician(1.0, 1.0) +Rician(5.0, 1.0) +Rician(10.0, 1.0) + StudentizedRange(2.0, 2.0) StudentizedRange(5.0, 10.0) StudentizedRange(10.0, 5.0) diff --git a/test/ref/continuous_test.ref.json b/test/ref/continuous_test.ref.json index dd8807a09..bb7ce9a89 100644 --- a/test/ref/continuous_test.ref.json +++ b/test/ref/continuous_test.ref.json @@ -4050,6 +4050,90 @@ { "q": 0.90, "x": 17.1677282103148 } ] }, +{ + "expr": "Rician(1.0, 1.0)", + "dtype": "Rician", + "minimum": 0, + "maximum": "inf", + "properties": { + "scale": 3, + "shape": 0.5 + }, + "points": [ + { "x": 0.58680768479231, "pdf": 0.32597689889069, "logpdf": -1.12092876242478, "cdf": 0.0999999999999981 }, + { "x": 0.8500705146284, "pdf": 0.42713703077601, "logpdf": -0.850650402069471, "cdf": 0.2 }, + { "x": 1.06959445774872, "pdf": 0.478590262388661, "logpdf": -0.736910449747733, "cdf": 0.300000000000001 }, + { "x": 1.27356435184454, "pdf": 0.497262612586662, "logpdf": -0.698636996890676, "cdf": 0.399999999999999 }, + { "x": 1.47547909178815, "pdf": 0.489049243270422, "logpdf": -0.715292092592867, "cdf": 0.500000000000012 }, + { "x": 1.6862862997492, "pdf": 0.455970051392373, "logpdf": -0.785328148395677, "cdf": 0.599999999999999 }, + { "x": 1.91973949265777, "pdf": 0.397730897577084, "logpdf": -0.921979639122226, "cdf": 0.700000000000003 }, + { "x": 2.2010753309479, "pdf": 0.311617302264047, "logpdf": -1.16597943936394, "cdf": 0.800000000000009 }, + { "x": 2.6019473978067, "pdf": 0.190248024171792, "logpdf": -1.65942666772506, "cdf": 0.900000000000001 } + ], + "quans": [ + { "q": 0.10, "x": 0.58680768479231 }, + { "q": 0.25, "x": 0.962923340192096 }, + { "q": 0.50, "x": 1.47547909178815 }, + { "q": 0.75, "x": 2.05189157517447 }, + { "q": 0.90, "x": 2.6019473978067 } + ] +}, +{ + "expr": "Rician(5.0, 1.0)", + "dtype": "Rician", + "minimum": 0, + "maximum": "inf", + "properties": { + "scale": 27, + "shape": 12.5 + }, + "points": [ + { "x": 3.83337182711774, "pdf": 0.178067275660391, "logpdf": -1.72559384694809, "cdf": 0.1 }, + { "x": 4.26739311906375, "pdf": 0.283509558779568, "logpdf": -1.26050943934722, "cdf": 0.199999999999999 }, + { "x": 4.5808282949603, "pdf": 0.351696201209667, "logpdf": -1.04498754078411, "cdf": 0.299999999999981 }, + { "x": 4.84891183432576, "pdf": 0.390461020883228, "logpdf": -0.940427133165443, "cdf": 0.400000000000014 }, + { "x": 5.0996760375676, "pdf": 0.402913231155499, "logpdf": -0.909034047523853, "cdf": 0.499999999999981 }, + { "x": 5.35060611077152, "pdf": 0.389944217738904, "logpdf": -0.941751581527125, "cdf": 0.599999999999959 }, + { "x": 5.61923787901142, "pdf": 0.350723907317018, "logpdf": -1.04775595388, "cdf": 0.699999999999992 }, + { "x": 5.93381683616078, "pdf": 0.282226926437332, "logpdf": -1.26504382796596, "cdf": 0.799999999999998 }, + { "x": 6.37038420267928, "pdf": 0.17678589781236, "logpdf": -1.73281589546462, "cdf": 0.899999999999982 } + ], + "quans": [ + { "q": 0.10, "x": 3.83337182711774 }, + { "q": 0.25, "x": 4.43248557648109 }, + { "q": 0.50, "x": 5.0996760375676 }, + { "q": 0.75, "x": 5.76805276699816 }, + { "q": 0.90, "x": 6.37038420267928 } + ] +}, +{ + "expr": "Rician(10.0, 1.0)", + "dtype": "Rician", + "minimum": 0, + "maximum": "inf", + "properties": { + "scale": 102, + "shape": 50 + }, + "points": [ + { "x": 8.77189934889994, "pdf": 0.17602367421233, "logpdf": -1.73713678041993, "cdf": 0.0999999999999889 }, + { "x": 9.21055856310058, "pdf": 0.280747347661651, "logpdf": -1.27030013273982, "cdf": 0.200000000000031 }, + { "x": 9.52691154521682, "pdf": 0.348625118414454, "logpdf": -1.05375809337315, "cdf": 0.299999999999978 }, + { "x": 9.79725334900319, "pdf": 0.387340667844654, "logpdf": -0.948450694502043, "cdf": 0.40000000000009 }, + { "x": 10.0499586252229, "pdf": 0.399938412039171, "logpdf": -0.91644471363081, "cdf": 0.500000000000001 }, + { "x": 10.3026851248306, "pdf": 0.387275589795634, "logpdf": -0.948618721053336, "cdf": 0.599999999999961 }, + { "x": 10.5730967493158, "pdf": 0.348503584620269, "logpdf": -1.05410676298003, "cdf": 0.700000000000032 }, + { "x": 10.8895938007392, "pdf": 0.280589475189091, "logpdf": -1.27086262025283, "cdf": 0.799999999999942 }, + { "x": 11.3285661035151, "pdf": 0.175871273888067, "logpdf": -1.73800294990951, "cdf": 0.900000000000024 } + ], + "quans": [ + { "q": 0.10, "x": 8.77189934889994 }, + { "q": 0.25, "x": 9.37722801591067 }, + { "q": 0.50, "x": 10.0499586252229 }, + { "q": 0.75, "x": 10.7228400133078 }, + { "q": 0.90, "x": 11.3285661035151 } + ] +}, { "expr": "StudentizedRange(2.0, 2.0)", "dtype": "StudentizedRange", diff --git a/test/ref/rdistributions.R b/test/ref/rdistributions.R index 699f1f718..c8b3b1031 100644 --- a/test/ref/rdistributions.R +++ b/test/ref/rdistributions.R @@ -68,6 +68,7 @@ source("continuous/normal.R") source("continuous/normalinversegaussian.R") source("continuous/pareto.R") source("continuous/rayleigh.R") +source("continuous/rician.R") source("continuous/studentizedrange.R") source("continuous/tdist.R") source("continuous/triangulardist.R") diff --git a/test/rician.jl b/test/rician.jl new file mode 100644 index 000000000..5674373b3 --- /dev/null +++ b/test/rician.jl @@ -0,0 +1,97 @@ +@testset "Rician" begin + + d1 = Rician(0.0, 10.0) + @test d1 isa Rician{Float64} + @test params(d1) == (0.0, 10.0) + @test shape(d1) == 0.0 + @test scale(d1) == 200.0 + @test partype(d1) === Float64 + @test eltype(d1) === Float64 + @test rand(d1) isa Float64 + + d2 = Rayleigh(10.0) + @test mean(d1) ≈ mean(d2) + @test var(d1) ≈ var(d2) + @test mode(d1) ≈ mode(d2) + @test median(d1) ≈ median(d2) + @test quantile.(d1, [0.25, 0.45, 0.60, 0.80, 0.90]) ≈ quantile.(d2, [0.25, 0.45, 0.60, 0.80, 0.90]) + @test pdf.(d1, 0.0:0.1:1.0) ≈ pdf.(d2, 0.0:0.1:1.0) + @test cdf.(d1, 0.0:0.1:1.0) ≈ cdf.(d2, 0.0:0.1:1.0) + + d1 = Rician(10.0, 10.0) + @test median(d1) == quantile(d1, 0.5) + x = quantile.(d1, [0.25, 0.45, 0.60, 0.80, 0.90]) + @test all(cdf.(d1, x) .≈ [0.25, 0.45, 0.60, 0.80, 0.90]) + + x = rand(Rician(5.0, 5.0), 100000) + d1 = fit(Rician, x) + @test d1 isa Rician{Float64} + @test params(d1)[1] ≈ 5.0 atol=0.2 + @test params(d1)[2] ≈ 5.0 atol=0.2 + + d1 = Rician(10.0f0, 10.0f0) + @test d1 isa Rician{Float32} + @test params(d1) == (10.0f0, 10.0f0) + @test shape(d1) == 0.5f0 + @test scale(d1) == 300.0f0 + @test partype(d1) === Float32 + @test eltype(d1) === Float64 + @test rand(d1) isa Float64 + + d1 = Rician() + @test d1 isa Rician{Float64} + @test params(d1) == (0.0, 1.0) + + @test pdf(d1, -Inf) == 0.0 + @test pdf(d1, -1) == 0.0 + @test pdf(d1, Inf) == 0.0 + @test isnan(pdf(d1, NaN)) + + @test logpdf(d1, -Inf) == -Inf + @test logpdf(d1, -1) == -Inf + @test logpdf(d1, Inf) == -Inf + @test isnan(logpdf(d1, NaN)) + + @test cdf(d1, -Inf) == 0.0 + @test cdf(d1, -1) == 0.0 + @test cdf(d1, Inf) == 1.0 + @test isnan(cdf(d1, NaN)) + + @test logcdf(d1, -Inf) == -Inf + @test logcdf(d1, -1) == -Inf + @test logcdf(d1, Inf) == 0.0 + @test isnan(logcdf(d1, NaN)) + + @inferred pdf(d1, -Inf32) + @inferred pdf(d1, -1) + @inferred pdf(d1, 1.0) + @inferred pdf(d1, 1.0f0) + @inferred pdf(d1, 1) + @inferred pdf(d1, 1//2) + @inferred pdf(d1, Inf) + + @inferred logpdf(d1, -Inf32) + @inferred logpdf(d1, -1) + @inferred logpdf(d1, 1.0) + @inferred logpdf(d1, 1.0f0) + @inferred logpdf(d1, 1) + @inferred logpdf(d1, 1//2) + @inferred logpdf(d1, Inf) + + @inferred cdf(d1, -Inf32) + @inferred cdf(d1, -1) + @inferred cdf(d1, 1.0) + @inferred cdf(d1, 1.0f0) + @inferred cdf(d1, 1) + @inferred cdf(d1, 1//2) + @inferred cdf(d1, Inf) + + @inferred logcdf(d1, -Inf32) + @inferred logcdf(d1, -1) + @inferred logcdf(d1, 1.0) + @inferred logcdf(d1, 1.0f0) + @inferred logcdf(d1, 1) + @inferred logcdf(d1, 1//2) + @inferred logcdf(d1, Inf) + +end diff --git a/test/runtests.jl b/test/runtests.jl index e2a83166a..a51b8c04c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -62,6 +62,7 @@ const tests = [ "chi", "gumbel", "pdfnorm", + "rician", ] printstyled("Running tests:\n", color=:blue) diff --git a/test/truncate.jl b/test/truncate.jl index 12bfefc22..d521d8ab1 100644 --- a/test/truncate.jl +++ b/test/truncate.jl @@ -81,7 +81,8 @@ function verify_and_test(d::UnivariateDistribution, dct::Dict, n_tsamples::Int) if !(typeof(d) in [Distributions.Truncated{Distributions.NoncentralChisq{Float64},Distributions.Continuous, Float64}, Distributions.Truncated{Distributions.NoncentralF{Float64},Distributions.Continuous, Float64}, Distributions.Truncated{Distributions.NoncentralT{Float64},Distributions.Continuous, Float64}, - Distributions.Truncated{Distributions.StudentizedRange{Float64},Distributions.Continuous, Float64}]) + Distributions.Truncated{Distributions.StudentizedRange{Float64},Distributions.Continuous, Float64}, + Distributions.Truncated{Distributions.Rician{Float64},Distributions.Continuous, Float64}]) @test isapprox(logpdf(d, Dual(float(x))), lp, atol=sqrt(eps())) end # NOTE: this test is disabled as StatsFuns.jl doesn't have generic support for cdf() From 4e2980cb4d15f8df8e679556d333e9f9b9bc1bac Mon Sep 17 00:00:00 2001 From: David Widmann Date: Fri, 1 Oct 2021 00:19:58 +0200 Subject: [PATCH 15/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 2cd3a5c2c..5d68464b5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.16" +version = "0.25.17" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From 0f3183bb113a97dd190130d995be40e070956099 Mon Sep 17 00:00:00 2001 From: st-- Date: Mon, 4 Oct 2021 16:55:55 +0300 Subject: [PATCH 16/58] Document NormalCanon parametrisation in docstring (#1399) * Document NormalCanon parametrisation in docstring To allow users to quickly check the definition of `NormalCanon` from the REPL. * Apply suggestions from code review --- src/univariate/continuous/normalcanon.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/univariate/continuous/normalcanon.jl b/src/univariate/continuous/normalcanon.jl index 250cb0937..55e8affa1 100644 --- a/src/univariate/continuous/normalcanon.jl +++ b/src/univariate/continuous/normalcanon.jl @@ -1,7 +1,10 @@ """ NormalCanon(η, λ) -Canonical Form of Normal distribution +Canonical parametrisation of the Normal distribution with canonical parameters `η` and `λ`. + +The two *canonical parameters* of a normal distribution ``\\mathcal{N}(\\mu, \\sigma^2)`` with mean ``\\mu`` and +standard deviation ``\\sigma`` are ``\\eta = \\sigma^{-2} \\mu`` and ``\\lambda = \\sigma^{-2}``. """ struct NormalCanon{T<:Real} <: ContinuousUnivariateDistribution η::T # σ^(-2) * μ From 6817b1c19561b5ea60832e99985f5a3ba82dd803 Mon Sep 17 00:00:00 2001 From: Davi Sales Barreira Date: Mon, 4 Oct 2021 11:01:22 -0300 Subject: [PATCH 17/58] Update CITATION.bib (#1351) * Update CITATION.bib The `july` in the month is causing an error with the new `PkgCite.jl`. * Update CITATION.bib Co-authored-by: Andreas Noack Co-authored-by: Moritz Schauer Co-authored-by: Andreas Noack --- CITATION.bib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CITATION.bib b/CITATION.bib index dd5839b69..05b42fe0a 100644 --- a/CITATION.bib +++ b/CITATION.bib @@ -30,7 +30,7 @@ @misc{Distributions.jl-2019 Moritz Schauer and other contributors}, title = {{JuliaStats/Distributions.jl: a Julia package for probability distributions and associated functions}}, - month = july, + month = jul, year = 2019, doi = {10.5281/zenodo.2647458}, url = {https://doi.org/10.5281/zenodo.2647458} From 0f35d9971953145fb9eeed73f31d1fef5bd8c4be Mon Sep 17 00:00:00 2001 From: st-- Date: Mon, 4 Oct 2021 23:40:31 +0300 Subject: [PATCH 18/58] meanform(::NormalCanon) (#1400) --- src/univariate/continuous/normalcanon.jl | 1 + test/normal.jl | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/src/univariate/continuous/normalcanon.jl b/src/univariate/continuous/normalcanon.jl index 55e8affa1..10fae8108 100644 --- a/src/univariate/continuous/normalcanon.jl +++ b/src/univariate/continuous/normalcanon.jl @@ -32,6 +32,7 @@ convert(::Type{NormalCanon{T}}, d::NormalCanon{S}) where {T <: Real, S <: Real} convert(::Type{Normal}, d::NormalCanon) = Normal(d.μ, 1 / sqrt(d.λ)) convert(::Type{NormalCanon}, d::Normal) = (λ = 1 / d.σ^2; NormalCanon(λ * d.μ, λ)) +meanform(d::NormalCanon) = convert(Normal, d) canonform(d::Normal) = convert(NormalCanon, d) diff --git a/test/normal.jl b/test/normal.jl index 0a261ccc3..70acf51b5 100644 --- a/test/normal.jl +++ b/test/normal.jl @@ -172,3 +172,12 @@ end @test rand(d, 10) isa Vector{Float64} @test rand(d, (3, 2)) isa Matrix{Float64} end + +@testset "NormalCanon and conversion" begin + @test canonform(Normal()) == NormalCanon() + @test meanform(NormalCanon()) == Normal() + @test meanform(canonform(Normal(0.25, 0.7))) ≈ Normal(0.25, 0.7) + @test convert(NormalCanon, convert(Normal, NormalCanon(0.3, 0.8))) ≈ NormalCanon(0.3, 0.8) + @test mean(canonform(Normal(0.25, 0.7))) ≈ 0.25 + @test std(canonform(Normal(0.25, 0.7))) ≈ 0.7 +end From 2a7a651ad0a9b210335c925c5688f5b0cb38f9b0 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Mon, 4 Oct 2021 22:41:10 +0200 Subject: [PATCH 19/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 5d68464b5..eabf2ae03 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.17" +version = "0.25.18" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From 625b72237a342c8d3bf60ec05541f8cb4a78faff Mon Sep 17 00:00:00 2001 From: agerlach <599421+agerlach@users.noreply.github.com> Date: Sat, 9 Oct 2021 15:18:06 -0400 Subject: [PATCH 20/58] Add `minimum`, `maximum`, `extrema` for `AbstractMvNormal` and `Product` (#1319) * adds minimum, maximum, extrema to AbstractMvNormal and Product distributions * add docstrings and update docs * update docstrrings * add isless tests for extrema * clean-up, remove broadcast in favor of map * move/update minimum/maximum fallback def/docstring Co-authored-by: Adam R Gerlach --- docs/src/multivariate.md | 4 ++++ src/common.jl | 20 ++++++++++++++++++++ src/multivariate/mvnormal.jl | 2 ++ src/multivariate/product.jl | 2 ++ src/univariates.jl | 21 --------------------- test/mvnormal.jl | 4 ++++ test/product.jl | 6 +++++- 7 files changed, 37 insertions(+), 22 deletions(-) diff --git a/docs/src/multivariate.md b/docs/src/multivariate.md index c1edc18cb..be4f5427a 100644 --- a/docs/src/multivariate.md +++ b/docs/src/multivariate.md @@ -71,8 +71,12 @@ invcov(::Distributions.AbstractMvNormal) logdetcov(::Distributions.AbstractMvNormal) sqmahal(::Distributions.AbstractMvNormal, ::AbstractArray) rand(::AbstractRNG, ::Distributions.AbstractMvNormal) +minimum(::Distributions.AbstractMvNormal) +maximum(::Distributions.AbstractMvNormal) +extrema(::Distributions.AbstractMvNormal) ``` + ### MvLogNormal In addition to the methods listed in the common interface above, we also provide the following methods: diff --git a/src/common.jl b/src/common.jl index 36c19a5bc..f2f179835 100644 --- a/src/common.jl +++ b/src/common.jl @@ -138,6 +138,26 @@ value_support(::Type{<:Distribution{VF,VS}}) where {VF,VS} = VS # to be decided: how to handle multivariate/matrixvariate distributions? Broadcast.broadcastable(d::UnivariateDistribution) = Ref(d) +""" + minimum(d::Distribution) + +Return the minimum of the support of `d`. +""" +minimum(d::Distribution) + +""" + maximum(d::Distribution) + +Return the maximum of the support of `d`. +""" +maximum(d::Distribution) + +""" + extrema(d::Distribution) + +Return the minimum and maximum of the support of `d` as a 2-tuple. +""" +Base.extrema(d::Distribution) = minimum(d), maximum(d) ## TODO: the following types need to be improved abstract type SufficientStats end diff --git a/src/multivariate/mvnormal.jl b/src/multivariate/mvnormal.jl index 6e6c8cad0..3827af61a 100644 --- a/src/multivariate/mvnormal.jl +++ b/src/multivariate/mvnormal.jl @@ -80,6 +80,8 @@ abstract type AbstractMvNormal <: ContinuousMultivariateDistribution end insupport(d::AbstractMvNormal, x::AbstractVector) = length(d) == length(x) && all(isfinite, x) +minimum(d::AbstractMvNormal) = fill(eltype(d)(-Inf), length(d)) +maximum(d::AbstractMvNormal) = fill(eltype(d)(Inf), length(d)) mode(d::AbstractMvNormal) = mean(d) modes(d::AbstractMvNormal) = [mean(d)] diff --git a/src/multivariate/product.jl b/src/multivariate/product.jl index 454ffada3..cee21c55d 100644 --- a/src/multivariate/product.jl +++ b/src/multivariate/product.jl @@ -40,6 +40,8 @@ var(d::Product) = var.(d.v) cov(d::Product) = Diagonal(var(d)) entropy(d::Product) = sum(entropy, d.v) insupport(d::Product, x::AbstractVector) = all(insupport.(d.v, x)) +minimum(d::Product) = map(minimum, d.v) +maximum(d::Product) = map(maximum, d.v) """ product_distribution(dists::AbstractVector{<:UnivariateDistribution}) diff --git a/src/univariates.jl b/src/univariates.jl index 73c6eddff..c31bc9a46 100644 --- a/src/univariates.jl +++ b/src/univariates.jl @@ -77,27 +77,6 @@ Get the degrees of freedom. """ dof(d::UnivariateDistribution) -""" - minimum(d::UnivariateDistribution) - -Return the minimum of the support of `d`. -""" -minimum(d::UnivariateDistribution) - -""" - maximum(d::UnivariateDistribution) - -Return the maximum of the support of `d`. -""" -maximum(d::UnivariateDistribution) - -""" - extrema(d::UnivariateDistribution) - -Return the minimum and maximum of the support of `d` as a 2-tuple. -""" -extrema(d::UnivariateDistribution) = (minimum(d), maximum(d)) - """ insupport(d::UnivariateDistribution, x::Any) diff --git a/test/mvnormal.jl b/test/mvnormal.jl index bb24c9e21..934ec2911 100644 --- a/test/mvnormal.jl +++ b/test/mvnormal.jl @@ -30,6 +30,10 @@ function test_mvnormal(g::AbstractMvNormal, n_tsamples::Int=10^6, vs = diag(Σ) @test g == typeof(g)(params(g)...) @test g == deepcopy(g) + @test minimum(g) == fill(-Inf, d) + @test maximum(g) == fill(Inf, d) + @test extrema(g) == (minimum(g), maximum(g)) + @test isless(extrema(g)...) # test sampling for AbstractMatrix (here, a SubArray): if ismissing(rng) diff --git a/test/product.jl b/test/product.jl index 3dc0e8de3..44c3b83c4 100644 --- a/test/product.jl +++ b/test/product.jl @@ -29,7 +29,7 @@ end N = 11 # Construct independent distributions and `Product` distribution from these. ubound = rand(N) - ds = Uniform.(0.0, ubound) + ds = Uniform.(-ubound, ubound) x = rand.(ds) d_product = product_distribution(ds) @test d_product isa Product @@ -43,6 +43,10 @@ end @test entropy(d_product) == sum(entropy.(ds)) @test insupport(d_product, ubound) == true @test insupport(d_product, ubound .+ 1) == false + @test minimum(d_product) == -ubound + @test maximum(d_product) == ubound + @test extrema(d_product) == (-ubound, ubound) + @test isless(extrema(d_product)...) y = rand(d_product) @test y isa typeof(x) From f9dbed590368df15c6c8f2dfc0caa312dd88b6c2 Mon Sep 17 00:00:00 2001 From: Jan Peters Date: Sat, 9 Oct 2021 23:32:21 +0200 Subject: [PATCH 21/58] Update laplace.jl (#1131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update laplace.jl It is not a good idea if variables have a different name (\beta) in the documentation than how they are used in the code (\theta). * Update src/univariate/continuous/laplace.jl Co-authored-by: Mathieu Besançon Co-authored-by: Moritz Schauer Co-authored-by: Mathieu Besançon --- src/univariate/continuous/laplace.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/univariate/continuous/laplace.jl b/src/univariate/continuous/laplace.jl index ebd3945ea..5cf1c171d 100644 --- a/src/univariate/continuous/laplace.jl +++ b/src/univariate/continuous/laplace.jl @@ -1,20 +1,20 @@ """ - Laplace(μ,β) + Laplace(μ,θ) -The *Laplace distribution* with location `μ` and scale `β` has probability density function +The *Laplace distribution* with location `μ` and scale `θ` has probability density function ```math -f(x; \\mu, \\beta) = \\frac{1}{2 \\beta} \\exp \\left(- \\frac{|x - \\mu|}{\\beta} \\right) +f(x; \\mu, \\theta) = \\frac{1}{2 \\theta} \\exp \\left(- \\frac{|x - \\mu|}{\\theta} \\right) ``` ```julia Laplace() # Laplace distribution with zero location and unit scale, i.e. Laplace(0, 1) Laplace(μ) # Laplace distribution with location μ and unit scale, i.e. Laplace(μ, 1) -Laplace(μ, β) # Laplace distribution with location μ and scale β +Laplace(μ, θ) # Laplace distribution with location μ and scale θ -params(d) # Get the parameters, i.e., (μ, β) +params(d) # Get the parameters, i.e., (μ, θ) location(d) # Get the location parameter, i.e. μ -scale(d) # Get the scale parameter, i.e. β +scale(d) # Get the scale parameter, i.e. θ ``` External links From 60c351b72368758a82a56d9def004d2c691f496c Mon Sep 17 00:00:00 2001 From: Michael Fairley Date: Sun, 10 Oct 2021 18:46:21 -0500 Subject: [PATCH 22/58] return -Inf from logdiffcdf if x == y (#1180) * return -Inf from logdiffcdf if x == y * let log1mexp(0) produce -Inf Co-authored-by: Moritz Schauer * added lognormal test for logdiffcdf when x == y Co-authored-by: Moritz Schauer Co-authored-by: David Widmann --- src/univariates.jl | 2 +- test/lognormal.jl | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/univariates.jl b/src/univariates.jl index c31bc9a46..96d091655 100644 --- a/src/univariates.jl +++ b/src/univariates.jl @@ -351,7 +351,7 @@ The natural logarithm of the difference between the cumulative density function function logdiffcdf(d::UnivariateDistribution, x::Real, y::Real) # Promote to ensure that we don't compute logcdf in low precision and then promote _x, _y = promote(x, y) - _x <= _y && throw(ArgumentError("requires x > y.")) + _x < _y && throw(ArgumentError("requires x >= y.")) u = logcdf(d, _x) v = logcdf(d, _y) return u + log1mexp(v - u) diff --git a/test/lognormal.jl b/test/lognormal.jl index b5013cb7f..0806356b7 100644 --- a/test/lognormal.jl +++ b/test/lognormal.jl @@ -11,6 +11,7 @@ isnan_type(::Type{T}, v) where {T} = isnan(v) && v isa T @test logpdf(LogNormal(), Inf) === -Inf @test iszero(logcdf(LogNormal(0, 0), 1)) @test iszero(logcdf(LogNormal(), Inf)) + @test logdiffcdf(LogNormal(), Float32(exp(3)), Float32(exp(3))) === -Inf @test logdiffcdf(LogNormal(), Float32(exp(5)), Float32(exp(3))) ≈ -6.607938594596893 rtol=1e-12 @test logdiffcdf(LogNormal(), Float32(exp(5)), Float64(exp(3))) ≈ -6.60793859457367 rtol=1e-12 @test logdiffcdf(LogNormal(), Float64(exp(5)), Float64(exp(3))) ≈ -6.607938594596893 rtol=1e-12 From 1ef4b689e8933fef41ecbf97a4f8ed72c16f1b47 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Mon, 11 Oct 2021 12:45:33 +0200 Subject: [PATCH 23/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index eabf2ae03..bc35e7a1c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.18" +version = "0.25.19" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From 27fb31ce6098149fa051613483edfafd616de310 Mon Sep 17 00:00:00 2001 From: st-- Date: Mon, 11 Oct 2021 13:47:02 +0300 Subject: [PATCH 24/58] Fix some docstrings (#1404) * fix docstrings * clean up Chernoff --- src/univariate/continuous/beta.jl | 8 ++++---- src/univariate/continuous/betaprime.jl | 8 ++++---- src/univariate/continuous/cauchy.jl | 10 +++++----- src/univariate/continuous/chernoff.jl | 18 ++---------------- src/univariate/continuous/symtriangular.jl | 6 +++--- 5 files changed, 18 insertions(+), 32 deletions(-) diff --git a/src/univariate/continuous/beta.jl b/src/univariate/continuous/beta.jl index 6259b18cd..5bc3f2f2b 100644 --- a/src/univariate/continuous/beta.jl +++ b/src/univariate/continuous/beta.jl @@ -1,5 +1,5 @@ """ - Beta(α,β) + Beta(α, β) The *Beta distribution* has probability density function @@ -15,10 +15,10 @@ independently, then ``X / (X + Y) \\sim \\operatorname{Beta}(\\alpha, \\beta)``. ```julia Beta() # equivalent to Beta(1, 1) -Beta(a) # equivalent to Beta(a, a) -Beta(a, b) # Beta distribution with shape parameters a and b +Beta(α) # equivalent to Beta(α, α) +Beta(α, β) # Beta distribution with shape parameters α and β -params(d) # Get the parameters, i.e. (a, b) +params(d) # Get the parameters, i.e. (α, β) ``` External links diff --git a/src/univariate/continuous/betaprime.jl b/src/univariate/continuous/betaprime.jl index e965cf674..7c30bd7ed 100644 --- a/src/univariate/continuous/betaprime.jl +++ b/src/univariate/continuous/betaprime.jl @@ -1,5 +1,5 @@ """ - BetaPrime(α,β) + BetaPrime(α, β) The *Beta prime distribution* has probability density function @@ -15,10 +15,10 @@ relation ship that if ``X \\sim \\operatorname{Beta}(\\alpha, \\beta)`` then ``\ ```julia BetaPrime() # equivalent to BetaPrime(1, 1) -BetaPrime(a) # equivalent to BetaPrime(a, a) -BetaPrime(a, b) # Beta prime distribution with shape parameters a and b +BetaPrime(α) # equivalent to BetaPrime(α, α) +BetaPrime(α, β) # Beta prime distribution with shape parameters α and β -params(d) # Get the parameters, i.e. (a, b) +params(d) # Get the parameters, i.e. (α, β) ``` External links diff --git a/src/univariate/continuous/cauchy.jl b/src/univariate/continuous/cauchy.jl index d7a187342..5afe79d90 100644 --- a/src/univariate/continuous/cauchy.jl +++ b/src/univariate/continuous/cauchy.jl @@ -9,12 +9,12 @@ f(x; \\mu, \\sigma) = \\frac{1}{\\pi \\sigma \\left(1 + \\left(\\frac{x - \\mu}{ ```julia Cauchy() # Standard Cauchy distribution, i.e. Cauchy(0, 1) -Cauchy(u) # Cauchy distribution with location u and unit scale, i.e. Cauchy(u, 1) -Cauchy(u, b) # Cauchy distribution with location u and scale b +Cauchy(μ) # Cauchy distribution with location μ and unit scale, i.e. Cauchy(μ, 1) +Cauchy(μ, σ) # Cauchy distribution with location μ and scale σ -params(d) # Get the parameters, i.e. (u, b) -location(d) # Get the location parameter, i.e. u -scale(d) # Get the scale parameter, i.e. b +params(d) # Get the parameters, i.e. (μ, σ) +location(d) # Get the location parameter, i.e. μ +scale(d) # Get the scale parameter, i.e. σ ``` External links diff --git a/src/univariate/continuous/chernoff.jl b/src/univariate/continuous/chernoff.jl index 340f68b22..920f05b2c 100644 --- a/src/univariate/continuous/chernoff.jl +++ b/src/univariate/continuous/chernoff.jl @@ -16,7 +16,7 @@ The *Chernoff distribution* is the distribution of the random variable ```math \\underset{t \\in (-\\infty,\\infty)}{\\arg\\max} ( G(t) - t^2 ), ``` -where ``G`` is standard two--sided Brownian motion. +where ``G`` is standard two-sided Brownian motion. The distribution arises as the limit distribution of various cube-root-n consistent estimators, including the isotonic regression estimator of Brunk, the isotonic density estimator of Grenander, @@ -27,21 +27,7 @@ computation of pdf and cdf is based on the algorithm described in Groeneboom and Journal of Computational and Graphical Statistics, 2001. ```julia -Chernoff() -pdf(Chernoff(),x::Real) -cdf(Chernoff(),x::Real) -logpdf(Chernoff(),x::Real) -survivor(Chernoff(),x::Real) -mean(Chernoff()) -var(Chernoff()) -skewness(Chernoff()) -kurtosis(Chernoff()) -kurtosis(Chernoff(), excess::Bool) -mode(Chernoff()) -entropy(Chernoff()) -rand(Chernoff()) -rand(rng, Chernoff() -cdf(Chernoff(),-x) #For tail probabilities, use this instead of 1-cdf(Chernoff(),x) +cdf(Chernoff(),-x) # For tail probabilities, use this instead of 1-cdf(Chernoff(),x) ``` """ struct Chernoff <: ContinuousUnivariateDistribution diff --git a/src/univariate/continuous/symtriangular.jl b/src/univariate/continuous/symtriangular.jl index 2f15f99f7..77f25f68e 100644 --- a/src/univariate/continuous/symtriangular.jl +++ b/src/univariate/continuous/symtriangular.jl @@ -1,5 +1,5 @@ """ - SymTriangularDist(μ,σ) + SymTriangularDist(μ, σ) The *Symmetric triangular distribution* with location `μ` and scale `σ` has probability density function @@ -9,8 +9,8 @@ f(x; \\mu, \\sigma) = \\frac{1}{\\sigma} \\left( 1 - \\left| \\frac{x - \\mu}{\\ ```julia SymTriangularDist() # Symmetric triangular distribution with zero location and unit scale -SymTriangularDist(u) # Symmetric triangular distribution with location μ and unit scale -SymTriangularDist(u, s) # Symmetric triangular distribution with location μ and scale σ +SymTriangularDist(μ) # Symmetric triangular distribution with location μ and unit scale +SymTriangularDist(μ, s) # Symmetric triangular distribution with location μ and scale σ params(d) # Get the parameters, i.e. (μ, σ) location(d) # Get the location parameter, i.e. μ From 42af709116880b5bda1e5768dfa7e6227836c438 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Tue, 12 Oct 2021 12:16:19 +0200 Subject: [PATCH 25/58] Plot pdf of continuous univariate distributions in documentation (#1403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Gadfly dependency Using Gadfly for plotting * Plot standard normal distribution Plot standard normal distribution using Gadlfy. @example for documenting from Documenter * change reverted Cahnge was unneccessary. Plots are not required here * removed Gadfly dependency Nor required here but in docs/Project.toml * Gadfly dependency density plots for continuous distributions * Density Arcsine(0,1) * rm extra ` removed extra ` end of @example * Cauchy(-2,1) density plot * LogNormal(0,1) Density plot * Rayleigh(0.5) Density plot * Weibull(5,1) Density plot * removed @example @example doesnt work when inside @docs. @example moved to univariate.md * @example density plots density plots for Arcsine(0,1), Cauchy(-2,1), LogNormal(0,1), and Rayleigh(0.5) * densities continous univariate Density plots of continous univariate distributions * removed editor settings * compat Gadfly = "1" Gadfly compatibility restricted to 1 * Update docs/Project.toml * evaluating PDF over a fine grid * TriangularDist(0,1,0.5) * Use GR * Preview of docs * Revert modification of docstring of `Arcsine` * Add missing title kwarg back * Apply suggestions from code review * Apply suggestions from code review Co-authored-by: st-- * Improve plot of `Uniform(0, 1)` Co-authored-by: Moritz Schauer Co-authored-by: BMasinde Co-authored-by: Mathieu Besançon Co-authored-by: st-- Co-authored-by: Moritz Schauer --- .github/workflows/DocPreviewCleanup.yml | 26 ++ docs/Project.toml | 2 + docs/make.jl | 5 +- docs/src/univariate.md | 319 ++++++++++++++++++++++++ 4 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/DocPreviewCleanup.yml diff --git a/.github/workflows/DocPreviewCleanup.yml b/.github/workflows/DocPreviewCleanup.yml new file mode 100644 index 000000000..bc29462c0 --- /dev/null +++ b/.github/workflows/DocPreviewCleanup.yml @@ -0,0 +1,26 @@ +name: Doc Preview Cleanup + +on: + pull_request: + types: [closed] + +jobs: + doc-preview-cleanup: + runs-on: ubuntu-latest + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v2 + with: + ref: gh-pages + - name: Delete preview and history + push changes + run: | + if [ -d "previews/PR$PRNUM" ]; then + git config user.name "Documenter.jl" + git config user.email "documenter@juliadocs.github.io" + git rm -rf "previews/PR$PRNUM" + git commit -m "delete preview" + git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree}) + git push --force origin gh-pages-new:gh-pages + fi + env: + PRNUM: ${{ github.event.number }} diff --git a/docs/Project.toml b/docs/Project.toml index 1bc937b99..030c7aa4b 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,7 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" [compat] Documenter = "0.26, 0.27" +GR = "0.61" diff --git a/docs/make.jl b/docs/make.jl index 289a5ec0b..73cfe05d2 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -19,7 +19,8 @@ makedocs( ] ) -deploydocs( +deploydocs(; repo = "github.com/JuliaStats/Distributions.jl.git", - versions = ["stable" => "v^", "v#.#", "dev" => "master"] + versions = ["stable" => "v^", "v#.#", "dev" => "master"], + push_preview=true, ) diff --git a/docs/src/univariate.md b/docs/src/univariate.md index b2bdf6290..88321cc4e 100644 --- a/docs/src/univariate.md +++ b/docs/src/univariate.md @@ -92,57 +92,376 @@ rand!(::AbstractRNG, ::UnivariateDistribution, ::AbstractArray) ## Continuous Distributions +```@setup plotdensity +using Distributions, GR + +# display figures as SVGs +GR.inline("svg") + +# plot probability density of continuous distributions +function plotdensity( + (xmin, xmax), + dist::ContinuousUnivariateDistribution; + npoints=299, + title="", + kwargs..., +) + figure(; + title=title, + xlabel="x", + ylabel="density", + grid=false, + backgroundcolor=0, # white instead of transparent background for dark Documenter scheme + font="Helvetica_Regular", # work around https://github.com/JuliaPlots/Plots.jl/issues/2596 + linewidth=2.0, # thick lines + kwargs..., + ) + return plot(range(xmin, xmax; length=npoints), Base.Fix1(pdf, dist)) +end + +# convenience function with automatic title +function plotdensity( + xmin_xmax, + ::Type{T}, + args=(); + title=string(T) * "(" * join(args, ", ") * ")", + kwargs... +) where {T<:ContinuousUnivariateDistribution} + return plotdensity(xmin_xmax, T(args...); title=title, kwargs...) +end +``` + ```@docs Arcsine +``` +```@example plotdensity +plotdensity((0.001, 0.999), Arcsine, (0, 1)) # hide +``` + +```@docs Beta +``` +```@example plotdensity +plotdensity((0, 1), Beta, (2, 2)) # hide +``` + +```@docs BetaPrime +``` +```@example plotdensity +plotdensity((0, 1), BetaPrime, (1, 2)) # hide +``` + +```@docs Biweight +``` +```@example plotdensity +plotdensity((-1, 3), Biweight, (1, 2)) # hide +``` + +```@docs Cauchy +``` +```@example plotdensity +plotdensity((-12, 5), Cauchy, (-2, 1)) # hide +``` + +```@docs Chernoff +``` +```@example plotdensity +plotdensity((-3, 3), Chernoff) # hide +``` + +```@docs Chi +``` +```@example plotdensity +plotdensity((0.001, 3), Chi, (1,)) # hide +``` + +```@docs Chisq +``` +```@example plotdensity +plotdensity((0, 9), Chisq, (3,)) # hide +``` + +```@docs Cosine +``` +```@example plotdensity +plotdensity((-1, 1), Cosine, (0, 1)) # hide +``` + +```@docs Epanechnikov +``` +```@example plotdensity +plotdensity((-1, 1), Epanechnikov, (0, 1)) # hide +``` + +```@docs Erlang +``` +```@example plotdensity +plotdensity((0, 8), Erlang, (7, 0.5)) # hide +``` + +```@docs Exponential +``` +```@example plotdensity +plotdensity((0, 3.5), Exponential, (0.5,)) # hide +``` + +```@docs FDist +``` +```@example plotdensity +plotdensity((0, 10), FDist, (10, 1)) # hide +``` + +```@docs Frechet +``` +```@example plotdensity +plotdensity((0, 20), Frechet, (1, 1)) # hide +``` + +```@docs Gamma +``` +```@example plotdensity +plotdensity((0, 18), Gamma, (7.5, 1)) # hide +``` + +```@docs GeneralizedExtremeValue +``` +```@example plotdensity +plotdensity((0, 30), GeneralizedExtremeValue, (0, 1, 1)) # hide +``` + +```@docs GeneralizedPareto +``` +```@example plotdensity +plotdensity((0, 20), GeneralizedPareto, (0, 1, 1)) # hide +``` + +```@docs Gumbel +``` +```@example plotdensity +plotdensity((-2, 5), Gumbel, (0, 1)) # hide +``` + +```@docs InverseGamma +``` +```@example plotdensity +plotdensity((0.001, 1), InverseGamma, (3, 0.5)) # hide +``` + +```@docs InverseGaussian +``` +```@example plotdensity +plotdensity((0, 5), InverseGaussian, (1, 1)) # hide +``` + +```@docs Kolmogorov +``` +```@example plotdensity +plotdensity((0, 2), Kolmogorov) # hide +``` + +```@docs KSDist KSOneSided +``` + +```@docs Laplace +``` +```@example plotdensity +plotdensity((-20, 20), Laplace, (0, 4)) # hide +``` + +```@docs Levy +``` +```@example plotdensity +plotdensity((0, 20), Levy, (0, 1)) # hide +``` + +```@docs LocationScale +``` +```@example plotdensity +plotdensity( + (-2, 5), LocationScale(2, 1, Normal(0, 1)); title="LocationScale(2, 1, Normal(0, 1))", +) # hide +``` + +```@docs Logistic +``` +```@example plotdensity +plotdensity((-4, 8), Logistic, (2, 1)) # hide +``` + +```@docs LogitNormal +``` +```@example plotdensity +plotdensity((0, 1), LogitNormal, (0, 1)) # hide +``` + +```@docs LogNormal +``` +```@example plotdensity +plotdensity((0, 5), LogNormal, (0, 1)) # hide +``` + +```@docs NoncentralBeta +``` +```@example plotdensity +plotdensity((0, 1), NoncentralBeta, (2, 3, 1)) # hide +``` + +```@docs NoncentralChisq +``` +```@example plotdensity +plotdensity((0, 20), NoncentralChisq, (2, 3)) # hide +``` + +```@docs NoncentralF +``` +```@example plotdensity +plotdensity((0, 10), NoncentralF, (2, 3, 1)) # hide +``` + +```@docs NoncentralT +``` +```@example plotdensity +plotdensity((-1, 20), NoncentralT, (2, 3)) # hide +``` + +```@docs Normal +``` +```@example plotdensity +plotdensity((-4, 4), Normal, (0, 1)) # hide +``` + +```@docs NormalCanon +``` +```@example plotdensity +plotdensity((-4, 4), NormalCanon, (0, 1)) # hide +``` + +```@docs NormalInverseGaussian +``` +```@example plotdensity +plotdensity((-2, 2), NormalInverseGaussian, (0, 0.5, 0.2, 0.1)) # hide +``` + +```@docs Pareto +``` +```@example plotdensity +plotdensity((1, 8), Pareto, (1, 1)) # hide +``` + +```@docs PGeneralizedGaussian +``` +```@example plotdensity +plotdensity((0, 20), PGeneralizedGaussian, (0.2)) # hide +``` + +```@docs Rayleigh +``` +```@example plotdensity +plotdensity((0, 2), Rayleigh, (0.5)) # hide +``` + +```@docs Rician +``` +```@example plotdensity +plotdensity((0, 5), Rician, (0.5, 1)) # hide +``` + +```@docs Semicircle +``` +```@example plotdensity +plotdensity((-1, 1), Semicircle, (1,)) # hide +``` + +```@docs StudentizedRange SymTriangularDist +``` +```@example plotdensity +# we only need to plot 5 equally spaced points for these parameters and limits # hide +plotdensity((-2, 2), SymTriangularDist, (0, 1); npoints=5) # hide +``` + +```@docs TDist +``` +```@example plotdensity +plotdensity((-5, 5), TDist, (5,)) # hide +``` + +```@docs TriangularDist +``` +```@example plotdensity +# we only need to plot 6 equally spaced points for these parameters and limits # hide +plotdensity((-0.5, 2), TriangularDist, (0, 1.5, 0.5); npoints=6) # hide +``` + +```@docs Triweight +``` +```@example plotdensity +plotdensity((0, 2), Triweight, (1, 1)) # hide +``` + +```@docs Uniform +``` +```@example plotdensity +plotdensity((-0.5, 1.5), Uniform, (0, 1); ylim=(0, 1.5)) # hide +``` + +```@docs VonMises +``` +```@example plotdensity +plotdensity((-π, π), VonMises, (0.5,); xlim=(-π, π), xticks=(π/5, 5), xticklabels=x -> x ≈ -π ? "-π" : (x ≈ π ? "π" : "0")) # hide +``` + +```@docs Weibull ``` +```@example plotdensity +plotdensity((0.001, 3), Weibull, (0.5, 1)) # hide +``` ## Discrete Distributions From fb9ce8a1a3a3c860128c62bba8336140fe7dbef8 Mon Sep 17 00:00:00 2001 From: Seth Axen Date: Fri, 15 Oct 2021 08:53:49 +0200 Subject: [PATCH 26/58] Add LKJCholesky (#1339) * Add CholeskyVariate * Add LKJCholesky * Add constructor * Add properties * Add logpdf evaluation * Add sampling code * Add insupport check * Add mode * Add show and convert * Use R for variable name * Add pdf and loglikelihood * Rename variables * Broadcast sampling * Don't promote type unless necessary * Call promote_eltype not promote_type * Apply suggestions from code review Co-authored-by: David Widmann * Adapt char_uplo and use promote * Replace inner loop with sum * Replace loop with sum * Avoid type term with rationals * Simplify expressions * Use correct variable names * Fix rebase bug * Reuse matrix type * Revert "Call promote_eltype not promote_type" This reverts commit 82d23e81bbccbcf3df711337b3168875e762f9cf. * Revert "Don't promote type unless necessary" This reverts commit e04fd77ba7f608c6a20945f9ca07631198099fef. * Change argument order * Precompute sampler of Beta * Don't call rand! from rand * Add rand! into array * Check support in logpdf * Add method with allocate * Add default RNG method * Add uplo argument * Handle 1x1 case * Add LKJCholesky tests * Add LKJCholesky docstring * Document LKJCholesky * Add note about LKJCholesky to LKJ docstring * Add broken test * Make show test pass for Julia 1.0 * Test loglikelihood for Cholesky input * Fix determination of whether to allocate * Add missing import for include * Tame the log * Handle fill not deepcopying * Test marginals for LKJCholesky * Add incremental cholesky onion sampler * Revert "Add incremental cholesky onion sampler" This reverts commit 6a343f3dbda1a38d80b08a3b162cc1df3634a6cd. * Draw more samples * Test marginal moments * Revert "Revert "Add incremental cholesky onion sampler"" This reverts commit 9fde37c773670963dfb34fc82e28abf33c612bde. * Remove accidental sample from LKJ * Use onion sampler * Use more efficient broadcasting * Specify rng * Account for testing multiple times * Decrease probability of type I error * Test default rng * Test logdetjac term with FiniteDifferences * Avoid sqrt of negative number * Don't broadcast over the rng fails on 1.0 * Remove unused vine method * Use norm for clarity * Use type to avoid transpose * Test that rand! is non-allocating * Revert "Test that rand! is non-allocating" This reverts commit cf00f259003068dbb5038efa3c5f198a2d99867d. * Update src/cholesky/lkjcholesky.jl Co-authored-by: David Widmann * Apply suggestions from code review Co-authored-by: David Widmann * Move utility functions within testset * Documentation improvements * Simplify constructor * Explicitly overload methods * Explicitly prefix with LinearAlgebra * Use view * Add comment explaining reduction * Use Val instead of AbstractTriangular * Apply suggestions from code review Co-authored-by: Moritz Schauer * Add clarifications to docs * Update comparison to LKJ * Move pvalue_kolmogorovsmirnoff to testutils.jl Co-authored-by: David Widmann Co-authored-by: Moritz Schauer --- docs/make.jl | 1 + docs/src/cholesky.md | 15 ++ src/Distributions.jl | 5 +- src/cholesky/lkjcholesky.jl | 264 ++++++++++++++++++++++++++++++++++++ src/common.jl | 6 + src/matrix/lkj.jl | 4 + test/lkjcholesky.jl | 219 ++++++++++++++++++++++++++++++ test/matrixvariates.jl | 15 -- test/runtests.jl | 1 + test/testutils.jl | 15 ++ 10 files changed, 529 insertions(+), 16 deletions(-) create mode 100644 docs/src/cholesky.md create mode 100644 src/cholesky/lkjcholesky.jl create mode 100644 test/lkjcholesky.jl diff --git a/docs/make.jl b/docs/make.jl index 73cfe05d2..0ad7367d5 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -13,6 +13,7 @@ makedocs( "truncate.md", "multivariate.md", "matrix.md", + "cholesky.md", "mixture.md", "fit.md", "extends.md", diff --git a/docs/src/cholesky.md b/docs/src/cholesky.md new file mode 100644 index 000000000..430beaaa0 --- /dev/null +++ b/docs/src/cholesky.md @@ -0,0 +1,15 @@ +# [Cholesky-variate Distributions](@id cholesky-variates) + +*Cholesky-variate distributions* are distributions whose variate forms are `CholeskyVariate`. This means each draw is a factorization of a positive-definite matrix of type `LinearAlgebra.Cholesky` (the object produced by the function `LinearAlgebra.cholesky` applied to a dense positive-definite matrix.) + +## Distributions + +```@docs +LKJCholesky +``` + +## Index + +```@index +Pages = ["cholesky.md"] +``` diff --git a/src/Distributions.jl b/src/Distributions.jl index dfa0231dd..34d0ad798 100644 --- a/src/Distributions.jl +++ b/src/Distributions.jl @@ -38,6 +38,7 @@ export Univariate, Multivariate, Matrixvariate, + CholeskyVariate, Discrete, Continuous, Sampleable, @@ -111,6 +112,7 @@ export Laplace, Levy, LKJ, + LKJCholesky, LocationScale, Logistic, LogNormal, @@ -281,6 +283,7 @@ include("univariates.jl") include("edgeworth.jl") include("multivariates.jl") include("matrixvariates.jl") +include("cholesky/lkjcholesky.jl") include("samplers.jl") # others @@ -325,7 +328,7 @@ Supported distributions: Frechet, FullNormal, FullNormalCanon, Gamma, GeneralizedPareto, GeneralizedExtremeValue, Geometric, Gumbel, Hypergeometric, InverseWishart, InverseGamma, InverseGaussian, IsoNormal, - IsoNormalCanon, Kolmogorov, KSDist, KSOneSided, Laplace, Levy, LKJ, + IsoNormalCanon, Kolmogorov, KSDist, KSOneSided, Laplace, Levy, LKJ, LKJCholesky, Logistic, LogNormal, MatrixBeta, MatrixFDist, MatrixNormal, MatrixReshaped, MatrixTDist, MixtureModel, Multinomial, MultivariateNormal, MvLogNormal, MvNormal, MvNormalCanon, diff --git a/src/cholesky/lkjcholesky.jl b/src/cholesky/lkjcholesky.jl new file mode 100644 index 000000000..12beecfd8 --- /dev/null +++ b/src/cholesky/lkjcholesky.jl @@ -0,0 +1,264 @@ +""" + LKJCholesky(d::Int, η::Real, uplo='L') + +The `LKJCholesky` distribution of size ``d`` with shape parameter ``\\eta`` is a +distribution over `LinearAlgebra.Cholesky` factorisations of ``d\\times d`` real correlation +matrices (positive-definite matrices with ones on the diagonal). + +Variates or samples of the distribution are `LinearAlgebra.Cholesky` objects, as might +be returned by `F = LinearAlgebra.cholesky(R)`, so that `Matrix(F) ≈ R` is a variate or +sample of [`LKJ`](@ref). + +Sampling `LKJCholesky` is faster than sampling `LKJ`, and often having the correlation +matrix in factorized form makes subsequent computations cheaper as well. + +!!! note + `LinearAlgebra.Cholesky` stores either the upper or lower Cholesky factor, related by + `F.U == F.L'`. Both can be accessed with `F.U` and `F.L`, but if the factor + not stored is requested, then a copy is made. The `uplo` parameter specifies whether the + upper (`'U'`) or lower (`'L'`) Cholesky factor is stored when randomly generating + samples. Set `uplo` to `'U'` if the upper factor is desired to avoid allocating a copy + when calling `F.U`. + +See [`LKJ`](@ref) for more details. + +External links + +* Lewandowski D, Kurowicka D, Joe H. + Generating random correlation matrices based on vines and extended onion method, + Journal of Multivariate Analysis (2009), 100(9): 1989-2001 + doi: [10.1016/j.jmva.2009.04.008](https://doi.org/10.1016/j.jmva.2009.04.008) +""" +struct LKJCholesky{T <: Real} <: Distribution{CholeskyVariate,Continuous} + d::Int + η::T + uplo::Char + logc0::T +end + +# ----------------------------------------------------------------------------- +# Constructors +# ----------------------------------------------------------------------------- + +function LKJCholesky(d::Int, η::Real, _uplo::Union{Char,Symbol} = 'L'; check_args = true) + if check_args + d > 0 || throw(ArgumentError("matrix dimension must be positive")) + η > 0 || throw(ArgumentError("shape parameter must be positive")) + end + logc0 = lkj_logc0(d, η) + uplo = _char_uplo(_uplo) + T = Base.promote_eltype(η, logc0) + return LKJCholesky(d, T(η), uplo, T(logc0)) +end + +# adapted from LinearAlgebra.char_uplo +function _char_uplo(uplo::Union{Symbol,Char}) + uplo ∈ (:U, 'U') && return 'U' + uplo ∈ (:L, 'L') && return 'L' + throw(ArgumentError("uplo argument must be either 'U' (upper) or 'L' (lower)")) +end + +# ----------------------------------------------------------------------------- +# REPL display +# ----------------------------------------------------------------------------- + +Base.show(io::IO, d::LKJCholesky) = show(io, d, (:d, :η, :uplo)) + +# ----------------------------------------------------------------------------- +# Conversion +# ----------------------------------------------------------------------------- + +function Base.convert(::Type{LKJCholesky{T}}, d::LKJCholesky) where T <: Real + return LKJCholesky{T}(d.d, T(d.η), d.uplo, T(d.logc0)) +end +function convert(::Type{LKJCholesky{T}}, d::Integer, η::Real, uplo::Char, logc0::Real) where T <: Real + return LKJCholesky{T}(Int(d), T(η), uplo, T(logc0)) +end + +# ----------------------------------------------------------------------------- +# Properties +# ----------------------------------------------------------------------------- + +Base.eltype(::Type{LKJCholesky{T}}) where {T} = T + +function Base.size(d::LKJCholesky) + p = d.d + return (p, p) +end + +function insupport(d::LKJCholesky, R::LinearAlgebra.Cholesky) + p = d.d + factors = R.factors + (isreal(factors) && size(factors, 1) == p) || return false + iinds, jinds = axes(factors) + # check that the diagonal of U'*U or L*L' is all ones + @inbounds if R.uplo === 'U' + for (j, jind) in enumerate(jinds) + col_iinds = view(iinds, 1:j) + sum(abs2, view(factors, col_iinds, jind)) ≈ 1 || return false + end + else # R.uplo === 'L' + for (i, iind) in enumerate(iinds) + row_jinds = view(jinds, 1:i) + sum(abs2, view(factors, iind, row_jinds)) ≈ 1 || return false + end + end + return true +end + +function StatsBase.mode(d::LKJCholesky) + factors = Matrix{eltype(d)}(LinearAlgebra.I, size(d)) + return LinearAlgebra.Cholesky(factors, d.uplo, 0) +end + +StatsBase.params(d::LKJCholesky) = (d.d, d.η, d.uplo) + +@inline partype(::LKJCholesky{T}) where {T <: Real} = T + +# ----------------------------------------------------------------------------- +# Evaluation +# ----------------------------------------------------------------------------- + +function logkernel(d::LKJCholesky, R::LinearAlgebra.Cholesky) + factors = R.factors + p, η = params(d) + c = p + 2(η - 1) + p == 1 && return c * log(first(factors)) + # assuming D = diag(factors) with length(D) = p, + # logp = sum(i -> (c - i) * log(D[i]), 2:p) + logp = sum(Iterators.drop(enumerate(diagind(factors)), 1)) do (i, di) + return (c - i) * log(factors[di]) + end + return logp +end + +function logpdf(d::LKJCholesky, R::LinearAlgebra.Cholesky) + insupport(d, R) || throw(ArgumentError("provided point is not in the support")) + return _logpdf(d, R) +end + +_logpdf(d::LKJCholesky, R::LinearAlgebra.Cholesky) = logkernel(d, R) + d.logc0 + +pdf(d::LKJCholesky, R::LinearAlgebra.Cholesky) = exp(logpdf(d, R)) + +loglikelihood(d::LKJCholesky, R::LinearAlgebra.Cholesky) = logpdf(d, R) +function loglikelihood(d::LKJCholesky, Rs::AbstractArray{<:LinearAlgebra.Cholesky}) + return sum(R -> logpdf(d, R), Rs) +end + +# ----------------------------------------------------------------------------- +# Sampling +# ----------------------------------------------------------------------------- + +function Base.rand(rng::AbstractRNG, d::LKJCholesky) + factors = Matrix{eltype(d)}(undef, size(d)) + R = LinearAlgebra.Cholesky(factors, d.uplo, 0) + return _lkj_cholesky_onion_sampler!(rng, d, R) +end +function Base.rand(rng::AbstractRNG, d::LKJCholesky, dims::Dims) + p = d.d + uplo = d.uplo + T = eltype(d) + TM = Matrix{T} + Rs = Array{LinearAlgebra.Cholesky{T,TM}}(undef, dims) + for i in eachindex(Rs) + factors = TM(undef, p, p) + Rs[i] = R = LinearAlgebra.Cholesky(factors, uplo, 0) + _lkj_cholesky_onion_sampler!(rng, d, R) + end + return Rs +end + +Random.rand!(d::LKJCholesky, R::LinearAlgebra.Cholesky) = Random.rand!(GLOBAL_RNG, d, R) +function Random.rand!(rng::AbstractRNG, d::LKJCholesky, R::LinearAlgebra.Cholesky) + return _lkj_cholesky_onion_sampler!(rng, d, R) +end + +function Random.rand!( + rng::AbstractRNG, + d::LKJCholesky, + Rs::AbstractArray{<:LinearAlgebra.Cholesky{T,TM}}, + allocate::Bool, +) where {T,TM} + p = d.d + uplo = d.uplo + if allocate + for i in eachindex(Rs) + Rs[i] = _lkj_cholesky_onion_sampler!( + rng, + d, + LinearAlgebra.Cholesky(TM(undef, p, p), uplo, 0), + ) + end + else + for i in eachindex(Rs) + _lkj_cholesky_onion_sampler!(rng, d, Rs[i]) + end + end + return Rs +end +function Random.rand!( + rng::AbstractRNG, + d::LKJCholesky, + Rs::AbstractArray{<:LinearAlgebra.Cholesky{<:Real}}, +) + allocate = any(!isassigned(Rs, i) for i in eachindex(Rs)) || any(R -> size(R, 1) != d.d, Rs) + return Random.rand!(rng, d, Rs, allocate) +end + +# +# onion method +# + +function _lkj_cholesky_onion_sampler!( + rng::AbstractRNG, + d::LKJCholesky, + R::LinearAlgebra.Cholesky, +) + if R.uplo === 'U' + _lkj_cholesky_onion_tri!(rng, R.factors, d.d, d.η, Val(:U)) + else + _lkj_cholesky_onion_tri!(rng, R.factors, d.d, d.η, Val(:L)) + end + return R +end + +function _lkj_cholesky_onion_tri!( + rng::AbstractRNG, + A::AbstractMatrix, + d::Int, + η::Real, + ::Val{uplo}, +) where {uplo} + # Section 3.2 in LKJ (2009 JMA) + # reformulated to incrementally construct Cholesky factor as mentioned in Section 5 + # equivalent steps in algorithm in reference are marked. + @assert size(A) == (d, d) + A[1, 1] = 1 + d > 1 || return R + β = η + (d - 2)//2 + # 1. Initialization + w0 = 2 * rand(rng, Beta(β, β)) - 1 + @inbounds if uplo === :L + A[2, 1] = w0 + else + A[1, 2] = w0 + end + @inbounds A[2, 2] = sqrt(1 - w0^2) + # 2. Loop, each iteration k adds row/column k+1 + for k in 2:(d - 1) + # (a) + β -= 1//2 + # (b) + y = rand(rng, Beta(k//2, β)) + # (c)-(e) + # w is directionally uniform vector of length √y + @inbounds w = @views uplo === :L ? A[k + 1, 1:k] : A[1:k, k + 1] + Random.randn!(rng, w) + rmul!(w, sqrt(y) / norm(w)) + # normalize so new row/column has unit norm + @inbounds A[k + 1, k + 1] = sqrt(1 - y) + end + # 3. + return A +end diff --git a/src/common.jl b/src/common.jl index f2f179835..243e1a591 100644 --- a/src/common.jl +++ b/src/common.jl @@ -16,6 +16,12 @@ const Univariate = ArrayLikeVariate{0} const Multivariate = ArrayLikeVariate{1} const Matrixvariate = ArrayLikeVariate{2} +""" +`F <: CholeskyVariate` specifies that the variate or a sample is of type +`LinearAlgebra.Cholesky`. +""" +abstract type CholeskyVariate <: VariateForm end + """ `S <: ValueSupport` specifies the support of sample elements, either discrete or continuous. diff --git a/src/matrix/lkj.jl b/src/matrix/lkj.jl index a181c4597..06af0e1f5 100644 --- a/src/matrix/lkj.jl +++ b/src/matrix/lkj.jl @@ -17,6 +17,10 @@ f(\\mathbf{R};\\eta) = \\left[\\prod_{k=1}^{d-1}\\pi^{\\frac{k}{2}} If ``\\eta = 1``, then the LKJ distribution is uniform over [the space of correlation matrices](https://www.jstor.org/stable/2684832). + +!!! note + if a Cholesky factor of the correlation matrix is desired, it is more efficient to + use [`LKJCholesky`](@ref), which avoids factorizing the matrix. """ struct LKJ{T <: Real, D <: Integer} <: ContinuousMatrixDistribution d::D diff --git a/test/lkjcholesky.jl b/test/lkjcholesky.jl new file mode 100644 index 000000000..32840f14a --- /dev/null +++ b/test/lkjcholesky.jl @@ -0,0 +1,219 @@ +using Distributions +using Random +using LinearAlgebra +using Test +using FiniteDifferences + +@testset "LKJCholesky" begin + function test_draw(d::LKJCholesky, x; check_uplo=true) + @test insupport(d, x) + check_uplo && @test x.uplo == d.uplo + end + function test_draws(d::LKJCholesky, xs; check_uplo=true, nkstests=1) + @test all(x -> insupport(d, x), xs) + check_uplo && @test all(x -> x.uplo == d.uplo, xs) + + p = d.d + dmat = LKJ(p, d.η) + marginal = Distributions._marginal(dmat) + ndraws = length(xs) + zs = Array{eltype(d)}(undef, p, p, ndraws) + for k in 1:ndraws + zs[:, :, k] = Matrix(xs[k]) + end + + @testset "LKJCholesky marginal moments" begin + @test mean(zs; dims=3)[:, :, 1] ≈ I atol=0.1 + @test var(zs; dims=3)[:, :, 1] ≈ var(marginal) * (ones(p, p) - I) atol=0.1 + @testset for n in 2:5 + for i in 1:p, j in 1:(i-1) + @test moment(zs[i, j, :], n) ≈ moment(rand(marginal, ndraws), n) atol=0.1 + end + end + end + + @testset "LKJCholesky marginal KS test" begin + α = 0.01 + L = sum(1:(p - 1)) + for i in 1:p, j in 1:(i-1) + @test pvalue_kolmogorovsmirnoff(zs[i, j, :], marginal) >= α / L / nkstests + end + end + end + + # Compute logdetjac of ϕ: L → L L' where only strict lower triangle of L and L L' are unique + function cholesky_inverse_logdetjac(L) + size(L, 1) == 1 && return 0.0 + J = jacobian(central_fdm(5, 1), cholesky_vec_to_corr_vec, stricttril_to_vec(L))[1] + return logabsdet(J)[1] + end + stricttril_to_vec(L) = [L[i, j] for i in axes(L, 1) for j in 1:(i - 1)] + function vec_to_stricttril(l) + n = length(l) + p = Int((1 + sqrt(8n + 1)) / 2) + L = similar(l, p, p) + fill!(L, 0) + k = 1 + for i in 1:p, j in 1:(i - 1) + L[i, j] = l[k] + k += 1 + end + return L + end + function cholesky_vec_to_corr_vec(l) + L = vec_to_stricttril(l) + for i in axes(L, 1) + w = view(L, i, 1:(i-1)) + wnorm = norm(w) + if wnorm > 1 + w ./= wnorm + wnorm = 1 + end + L[i, i] = sqrt(1 - wnorm^2) + end + return stricttril_to_vec(L * L') + end + + @testset "Constructors" begin + @testset for p in (4, 5), η in (2, 3.5) + d = LKJCholesky(p, η) + @test d.d == p + @test d.η == η + @test d.uplo == 'L' + @test d.logc0 == LKJ(p, η).logc0 + end + + @test_throws ArgumentError LKJCholesky(0, 2) + @test_throws ArgumentError LKJCholesky(4, 0.0) + @test_throws ArgumentError LKJCholesky(4, -1) + + for uplo in (:U, 'U') + d = LKJCholesky(4, 2, uplo) + @test d.uplo === 'U' + end + for uplo in (:L, 'L') + d = LKJCholesky(4, 2, uplo) + @test d.uplo === 'L' + end + @test_throws ArgumentError LKJCholesky(4, 2, :F) + @test_throws ArgumentError LKJCholesky(4, 2, 'F') + end + + @testset "REPL display" begin + d = LKJCholesky(5, 1) + @test sprint(show, d) == "$(typeof(d))(\nd: 5\nη: 1.0\nuplo: L\n)\n" + end + + @testset "Conversion" begin + d = LKJCholesky(5, 3.5) + df0_1 = convert(LKJCholesky{Float32}, d) + @test df0_1 isa LKJCholesky{Float32} + @test df0_1.d == d.d + @test df0_1.η ≈ d.η + @test df0_1.uplo == d.uplo + @test df0_1.logc0 ≈ d.logc0 + + df0_2 = convert(LKJCholesky{BigFloat}, d.d, d.η, d.uplo, d.logc0) + @test df0_2 isa LKJCholesky{BigFloat} + @test df0_2.d == d.d + @test df0_2.η ≈ d.η + @test df0_2.uplo == d.uplo + @test df0_2.logc0 ≈ d.logc0 + end + + @testset "properties" begin + @testset for p in (4, 5), η in (2, 3.5), uplo in ('L', 'U') + d = LKJCholesky(p, η, uplo) + @test d.d == p + @test size(d) == (p, p) + @test Distributions.params(d) == (d.d, d.η, d.uplo) + @test partype(d) <: Float64 + + m = mode(d) + @test m isa Cholesky{eltype(d)} + @test Matrix(m) ≈ I + end + @test_broken partype(LKJCholesky(2, 4f0)) <: Float32 + + @testset "insupport" begin + @test insupport(LKJCholesky(40, 2, 'U'), cholesky(rand(LKJ(40, 2)))) + @test insupport(LKJCholesky(40, 2), cholesky(rand(LKJ(40, 2)))) + @test !insupport(LKJCholesky(40, 2), cholesky(rand(LKJ(41, 2)))) + z = rand(LKJ(40, 1)) + z .+= exp(Symmetric(randn(size(z)))) .* 1e-8 + x = cholesky(z) + @test !insupport(LKJCholesky(4, 2), x) + end + end + + @testset "Evaluation" begin + @testset for p in (1, 4, 10), η in (0.5, 1, 3) + d = LKJ(p, η) + dchol = LKJCholesky(p, η) + z = rand(d) + x = cholesky(z) + x_L = typeof(x)(Matrix(x.L), 'L', x.info) + logdetJ = sum(i -> (p - i) * log(x.UL[i, i]), 1:p) + logdetJ_approx = cholesky_inverse_logdetjac(x.L) + @test logdetJ ≈ logdetJ_approx + + @test logpdf(dchol, x) ≈ logpdf(d, z) + logdetJ + @test logpdf(dchol, x_L) ≈ logpdf(dchol, x) + + @test pdf(dchol, x) ≈ exp(logpdf(dchol, x)) + @test pdf(dchol, x_L) ≈ pdf(dchol, x) + + @test loglikelihood(dchol, x) ≈ logpdf(dchol, x) + xs = cholesky.(rand(d, 10)) + @test loglikelihood(dchol, xs) ≈ sum(logpdf(dchol, x) for x in xs) + end + end + + @testset "Sampling" begin + rng = MersenneTwister(66) + nkstests = 4 # use for appropriate Bonferroni correction for KS test + + @testset "rand" begin + @testset for p in (2, 4, 10), η in (0.5, 1, 3), uplo in ('L', 'U') + d = LKJCholesky(p, η, uplo) + test_draw(d, rand(rng, d)) + test_draws(d, rand(rng, d, 10^4); nkstests=nkstests) + end + @test_broken rand(rng, LKJCholesky(5, Inf)) ≈ I + end + + @testset "rand!" begin + @testset for p in (2, 4, 10), η in (0.5, 1, 3), uplo in ('L', 'U') + d = LKJCholesky(p, η, uplo) + x = Cholesky(Matrix{Float64}(undef, p, p), uplo, 0) + rand!(rng, d, x) + test_draw(d, x) + x = Cholesky(Matrix{Float64}(undef, p, p), uplo, 0) + rand!(d, x) + test_draw(d, x) + + # test that uplo of Cholesky object is respected + x2 = Cholesky(Matrix{Float64}(undef, p, p), uplo == 'L' ? 'U' : 'L', 0) + rand!(rng, d, x2) + test_draw(d, x2; check_uplo = false) + + # allocating + xs = Vector{typeof(x)}(undef, 10^4) + rand!(rng, d, xs) + test_draws(d, xs; nkstests=nkstests) + + F2 = cholesky(exp(Symmetric(randn(rng, p, p)))) + xs2 = [deepcopy(F2) for _ in 1:10^4] + xs2[1] = cholesky(exp(Symmetric(randn(rng, p + 1, p + 1)))) + rand!(rng, d, xs2) + test_draws(d, xs2; nkstests=nkstests) + + # non-allocating + F3 = cholesky(exp(Symmetric(randn(rng, p, p)))) + xs3 = [deepcopy(F3) for _ in 1:10^4] + rand!(rng, d, xs3) + test_draws(d, xs3; check_uplo = uplo == 'U', nkstests=nkstests) + end + end + end +end \ No newline at end of file diff --git a/test/matrixvariates.jl b/test/matrixvariates.jl index 55082cb1f..f8b1fbe23 100644 --- a/test/matrixvariates.jl +++ b/test/matrixvariates.jl @@ -185,21 +185,6 @@ function test_against_univariate(D::MatrixDistribution, d::UnivariateDistributio nothing end -# Equivalent to `ExactOneSampleKSTest` in HypothesisTests.jl -# We implement it here to avoid a circular dependency on HypothesisTests -# that causes test failures when preparing a breaking release of Distributions -function pvalue_kolmogorovsmirnoff(x::AbstractVector, d::UnivariateDistribution) - # compute maximum absolute deviation from the empirical cdf - n = length(x) - cdfs = sort!(map(Base.Fix1(cdf, d), x)) - dmax = maximum(zip(cdfs, (0:(n-1))/n, (1:n)/n)) do (cdf, lower, upper) - return max(cdf - lower, upper - cdf) - end - - # compute asymptotic p-value (see `KSDist`) - return ccdf(KSDist(n), dmax) -end - function test_draws_against_univariate_cdf(D::MatrixDistribution, d::UnivariateDistribution) α = 0.025 M = 100000 diff --git a/test/runtests.jl b/test/runtests.jl index a51b8c04c..1313eff57 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -39,6 +39,7 @@ const tests = [ "edgeworth", "matrixreshaped", "matrixvariates", + "lkjcholesky", "vonmisesfisher", "conversion", "convolution", diff --git a/test/testutils.jl b/test/testutils.jl index 7785c32fd..6a8e93bf0 100644 --- a/test/testutils.jl +++ b/test/testutils.jl @@ -590,3 +590,18 @@ function fdm(f, at) FiniteDifferences.central_fdm(5, 1)(x -> f([at[1:i-1]; x; at[i+1:end]]), at[i]) end end + +# Equivalent to `ExactOneSampleKSTest` in HypothesisTests.jl +# We implement it here to avoid a circular dependency on HypothesisTests +# that causes test failures when preparing a breaking release of Distributions +function pvalue_kolmogorovsmirnoff(x::AbstractVector, d::UnivariateDistribution) + # compute maximum absolute deviation from the empirical cdf + n = length(x) + cdfs = sort!(map(Base.Fix1(cdf, d), x)) + dmax = maximum(zip(cdfs, (0:(n-1))/n, (1:n)/n)) do (cdf, lower, upper) + return max(cdf - lower, upper - cdf) + end + + # compute asymptotic p-value (see `KSDist`) + return ccdf(KSDist(n), dmax) +end From 1c8208c5e6476565aa0677ef4692f575d8dc3fe8 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Fri, 15 Oct 2021 08:56:34 +0200 Subject: [PATCH 27/58] Bump version --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index bc35e7a1c..aeb326ec2 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.19" +version = "0.25.20" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From c7f2fcede3ceae5b3526b6470502d1a2219e512b Mon Sep 17 00:00:00 2001 From: David Widmann Date: Sun, 24 Oct 2021 09:58:47 +0200 Subject: [PATCH 28/58] Improve `pdf`, `logpdf`, `cdf`, and `ccdf` of `Uniform` (#1411) --- Project.toml | 2 +- src/univariate/continuous/uniform.jl | 25 ++++++++++++++----------- test/univariates.jl | 24 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/Project.toml b/Project.toml index aeb326ec2..bbbe06526 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.20" +version = "0.25.21" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/src/univariate/continuous/uniform.jl b/src/univariate/continuous/uniform.jl index fa7cfffb9..277d9f8d5 100644 --- a/src/univariate/continuous/uniform.jl +++ b/src/univariate/continuous/uniform.jl @@ -70,20 +70,23 @@ entropy(d::Uniform) = log(d.b - d.a) #### Evaluation -pdf(d::Uniform{T}, x::Real) where {T<:Real} = insupport(d, x) ? 1 / (d.b - d.a) : zero(T) -logpdf(d::Uniform{T}, x::Real) where {T<:Real} = insupport(d, x) ? -log(d.b - d.a) : -T(Inf) +function pdf(d::Uniform, x::Real) + val = inv(d.b - d.a) + return insupport(d, x) ? val : zero(val) +end +function logpdf(d::Uniform, x::Real) + diff = d.b - d.a + return insupport(d, x) ? -log(diff) : log(zero(diff)) +end gradlogpdf(d::Uniform{T}, x::Real) where {T<:Real} = zero(T) -function cdf(d::Uniform{T}, x::Real) where T<:Real - (a, b) = params(d) - x <= a ? zero(T) : - x >= d.b ? one(T) : (x - a) / (b - a) +function cdf(d::Uniform, x::Real) + a, b = params(d) + return clamp((x - a) / (b - a), 0, 1) end - -function ccdf(d::Uniform{T}, x::Real) where T<:Real - (a, b) = params(d) - x <= a ? one(T) : - x >= d.b ? zero(T) : (b - x) / (b - a) +function ccdf(d::Uniform, x::Real) + a, b = params(d) + return clamp((b - x) / (b - a), 0, 1) end quantile(d::Uniform, p::Real) = d.a + p * (d.b - d.a) diff --git a/test/univariates.jl b/test/univariates.jl index 4fa2fadc9..7a6b85172 100644 --- a/test/univariates.jl +++ b/test/univariates.jl @@ -175,3 +175,27 @@ end @test invlogcdf(d, log(0.2)) isa Int @test invlogccdf(d, log(0.6)) isa Int end + +@testset "Uniform type inference" begin + for T in (Int, Float32) + d = Uniform{T}(T(2), T(3)) + FT = float(T) + XFT = promote_type(FT, Float64) + + @test @inferred(pdf(d, 1.5)) === zero(FT) + @test @inferred(pdf(d, 2.5)) === one(FT) + @test @inferred(pdf(d, 3.5)) === zero(FT) + + @test @inferred(logpdf(d, 1.5)) === FT(-Inf) + @test @inferred(logpdf(d, 2.5)) === -zero(FT) # negative zero + @test @inferred(logpdf(d, 3.5)) === FT(-Inf) + + @test @inferred(cdf(d, 1.5)) === zero(XFT) + @test @inferred(cdf(d, 2.5)) === XFT(1//2) + @test @inferred(cdf(d, 3.5)) === one(XFT) + + @test @inferred(ccdf(d, 1.5)) === one(XFT) + @test @inferred(ccdf(d, 2.5)) === XFT(1//2) + @test @inferred(ccdf(d, 3.5)) === zero(XFT) + end +end \ No newline at end of file From 9ae5ab2744283696cfb0dfc5b2305108e4cb1784 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Tue, 26 Oct 2021 23:21:01 +0200 Subject: [PATCH 29/58] Remove warning in constructor of `Wishart` (#1410) * Remove warning in constructor of `Wishart` * Remove `warn` argument and deprecate constructors * Bump version --- Project.toml | 2 +- src/deprecates.jl | 5 +++++ src/matrix/wishart.jl | 23 +++++++++++------------ test/matrixvariates.jl | 13 ++++++++++--- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/Project.toml b/Project.toml index bbbe06526..db0b2862f 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.21" +version = "0.25.22" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/src/deprecates.jl b/src/deprecates.jl index dc303951e..906d8ee28 100644 --- a/src/deprecates.jl +++ b/src/deprecates.jl @@ -41,3 +41,8 @@ for fun in [:pdf, :logpdf, end @deprecate pdf(d::DiscreteUnivariateDistribution) pdf.(Ref(d), support(d)) + +# Wishart constructors +@deprecate Wishart(df::Real, S::AbstractPDMat, warn::Bool) Wishart(df, S) +@deprecate Wishart(df::Real, S::Matrix, warn::Bool) Wishart(df, S) +@deprecate Wishart(df::Real, S::Cholesky, warn::Bool) Wishart(df, S) diff --git a/src/matrix/wishart.jl b/src/matrix/wishart.jl index e9ccdc262..efd0199f8 100644 --- a/src/matrix/wishart.jl +++ b/src/matrix/wishart.jl @@ -42,29 +42,28 @@ end # Constructors # ----------------------------------------------------------------------------- -function Wishart(df::T, S::AbstractPDMat{T}, warn::Bool = true) where T<:Real +function Wishart(df::T, S::AbstractPDMat{T}) where T<:Real df > 0 || throw(ArgumentError("df must be positive. got $(df).")) p = dim(S) - rnk = p singular = df <= p - 1 if singular - isinteger(df) || throw(ArgumentError("singular df must be an integer. got $(df).")) - rnk = convert(Integer, df) - warn && @warn("got df <= dim - 1; returning a singular Wishart") + isinteger(df) || throw( + ArgumentError("df of a singular Wishart distribution must be an integer (got $df)") + ) end + rnk::Integer = ifelse(singular, df, p) logc0 = wishart_logc0(df, S, rnk) - R = Base.promote_eltype(T, logc0) - prom_S = convert(AbstractArray{T}, S) - Wishart{R, typeof(prom_S), typeof(rnk)}(R(df), prom_S, R(logc0), rnk, singular) + _df, _logc0 = promote(df, logc0) + Wishart{typeof(_df), typeof(S), typeof(rnk)}(_df, S, _logc0, rnk, singular) end -function Wishart(df::Real, S::AbstractPDMat, warn::Bool = true) +function Wishart(df::Real, S::AbstractPDMat) T = Base.promote_eltype(df, S) - Wishart(T(df), convert(AbstractArray{T}, S), warn) + Wishart(T(df), convert(AbstractArray{T}, S)) end -Wishart(df::Real, S::Matrix, warn::Bool = true) = Wishart(df, PDMat(S), warn) -Wishart(df::Real, S::Cholesky, warn::Bool = true) = Wishart(df, PDMat(S), warn) +Wishart(df::Real, S::Matrix) = Wishart(df, PDMat(S)) +Wishart(df::Real, S::Cholesky) = Wishart(df, PDMat(S)) # ----------------------------------------------------------------------------- # REPL display diff --git a/test/matrixvariates.jl b/test/matrixvariates.jl index f8b1fbe23..23acb5db7 100644 --- a/test/matrixvariates.jl +++ b/test/matrixvariates.jl @@ -327,6 +327,11 @@ function test_special(dist::Type{Wishart}) ν, Σ = _rand_params(Wishart, Float64, n, n) d = Wishart(ν, Σ) H = rand(d, M) + @testset "deprecations" begin + for warn in (true, false) + @test @test_deprecated(Wishart(n - 1, Σ, warn)) == Wishart(n - 1, Σ) + end + end @testset "meanlogdet" begin @test isapprox(Distributions.meanlogdet(d), mean(logdet.(H)), atol = 0.1) end @@ -350,15 +355,17 @@ function test_special(dist::Type{Wishart}) end end @testset "Check Singular Branch" begin - X = H[1] - rank1 = Wishart(n - 2, Σ, false) - rank2 = Wishart(n - 1, Σ, false) + # Check that no warnings are shown: #1410 + rank1 = @test_logs Wishart(n - 2, Σ) + rank2 = @test_logs Wishart(n - 1, Σ) test_draw(rank1) test_draw(rank2) test_draws(rank1, rand(rank1, 10^6)) test_draws(rank2, rand(rank2, 10^6)) test_cov(rank1) test_cov(rank2) + + X = H[1] @test Distributions.singular_wishart_logkernel(d, X) ≈ Distributions.nonsingular_wishart_logkernel(d, X) @test Distributions.singular_wishart_logc0(n, ν, d.S, rank(d)) ≈ Distributions.nonsingular_wishart_logc0(n, ν, d.S) @test logpdf(d, X) ≈ Distributions.singular_wishart_logkernel(d, X) + Distributions.singular_wishart_logc0(n, ν, d.S, rank(d)) From 1a94057451301dbd13f6dce0a498788dca56f7e5 Mon Sep 17 00:00:00 2001 From: Benoit Pasquier <4486578+briochemc@users.noreply.github.com> Date: Thu, 28 Oct 2021 19:58:35 +1100 Subject: [PATCH 30/58] Add gradlogpdf to Uniform (#1133) * Add gradlogpdf to Uniform * Fix type output of Uniform gradlogpdf * Update src/univariate/continuous/uniform.jl Co-authored-by: David Widmann * Add gradlogpdf(Uniform) test Co-authored-by: David Widmann --- src/univariate/continuous/uniform.jl | 2 +- test/gradlogpdf.jl | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/univariate/continuous/uniform.jl b/src/univariate/continuous/uniform.jl index 277d9f8d5..fc04a9546 100644 --- a/src/univariate/continuous/uniform.jl +++ b/src/univariate/continuous/uniform.jl @@ -78,7 +78,7 @@ function logpdf(d::Uniform, x::Real) diff = d.b - d.a return insupport(d, x) ? -log(diff) : log(zero(diff)) end -gradlogpdf(d::Uniform{T}, x::Real) where {T<:Real} = zero(T) +gradlogpdf(d::Uniform, x::Real) = zero(partype(d)) / oneunit(x) function cdf(d::Uniform, x::Real) a, b = params(d) diff --git a/test/gradlogpdf.jl b/test/gradlogpdf.jl index da4ab7e42..5f54340ff 100644 --- a/test/gradlogpdf.jl +++ b/test/gradlogpdf.jl @@ -4,18 +4,20 @@ using Test # Test for gradlogpdf on univariate distributions -@test isapprox(gradlogpdf(Beta(1.5, 3.0), 0.7) , -5.9523809523809526 , atol=1.0e-8) -@test isapprox(gradlogpdf(Chi(5.0), 5.5) , -4.7727272727272725 , atol=1.0e-8) -@test isapprox(gradlogpdf(Chisq(7.0), 12.0) , -0.29166666666666663, atol=1.0e-8) -@test isapprox(gradlogpdf(Exponential(2.0), 7.0) , -0.5 , atol=1.0e-8) -@test isapprox(gradlogpdf(Gamma(9.0, 0.5), 11.0) , -1.2727272727272727 , atol=1.0e-8) -@test isapprox(gradlogpdf(Gumbel(3.5, 1.0), 4.0) , -1.6065306597126334 , atol=1.0e-8) -@test isapprox(gradlogpdf(Laplace(7.0), 34.0) , -1.0 , atol=1.0e-8) -@test isapprox(gradlogpdf(Logistic(-6.0), 1.0) , -0.9981778976111987 , atol=1.0e-8) -@test isapprox(gradlogpdf(LogNormal(5.5), 2.0) , 1.9034264097200273 , atol=1.0e-8) -@test isapprox(gradlogpdf(Normal(-4.5, 2.0), 1.6), -1.525 , atol=1.0e-8) -@test isapprox(gradlogpdf(TDist(8.0), 9.1) , -0.9018830525272548 , atol=1.0e-8) -@test isapprox(gradlogpdf(Weibull(2.0), 3.5) , -6.714285714285714 , atol=1.0e-8) +@test isapprox(gradlogpdf(Beta(1.5, 3.0), 0.7) , -5.9523809523809526 , atol=1.0e-8) +@test isapprox(gradlogpdf(Chi(5.0), 5.5) , -4.7727272727272725 , atol=1.0e-8) +@test isapprox(gradlogpdf(Chisq(7.0), 12.0) , -0.29166666666666663, atol=1.0e-8) +@test isapprox(gradlogpdf(Exponential(2.0), 7.0) , -0.5 , atol=1.0e-8) +@test isapprox(gradlogpdf(Gamma(9.0, 0.5), 11.0) , -1.2727272727272727 , atol=1.0e-8) +@test isapprox(gradlogpdf(Gumbel(3.5, 1.0), 4.0) , -1.6065306597126334 , atol=1.0e-8) +@test isapprox(gradlogpdf(Laplace(7.0), 34.0) , -1.0 , atol=1.0e-8) +@test isapprox(gradlogpdf(Logistic(-6.0), 1.0) , -0.9981778976111987 , atol=1.0e-8) +@test isapprox(gradlogpdf(LogNormal(5.5), 2.0) , 1.9034264097200273 , atol=1.0e-8) +@test isapprox(gradlogpdf(Normal(-4.5, 2.0), 1.6) , -1.525 , atol=1.0e-8) +@test isapprox(gradlogpdf(TDist(8.0), 9.1) , -0.9018830525272548 , atol=1.0e-8) +@test isapprox(gradlogpdf(Weibull(2.0), 3.5) , -6.714285714285714 , atol=1.0e-8) +@test isapprox(gradlogpdf(Uniform(-1.0, 1.0), 0.3), 0.0 , atol=1.0e-8) + # Test for gradlogpdf on multivariate distributions From 0dc59b2c1f7666f11f23f00360e24409b4811aad Mon Sep 17 00:00:00 2001 From: David Widmann Date: Thu, 28 Oct 2021 12:57:05 +0200 Subject: [PATCH 31/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index db0b2862f..30e9fefb0 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.22" +version = "0.25.23" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From 586625859ce234cccbc152f00e681e5657f8a56f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 30 Oct 2021 09:04:19 +0200 Subject: [PATCH 32/58] CompatHelper: bump compat for GR to 0.62 for package docs, (keep existing compat) (#1412) Co-authored-by: CompatHelper Julia --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index 030c7aa4b..fe1618933 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -4,4 +4,4 @@ GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" [compat] Documenter = "0.26, 0.27" -GR = "0.61" +GR = "0.61, 0.62" From d8380fcea41ff1e8ab92d0f9a9f3f95d70b20b45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Galy-Fajou?= Date: Thu, 4 Nov 2021 01:14:18 +0100 Subject: [PATCH 33/58] Analytic KL divergences (#1414) * Added all kl divergence methods * Add tests and correct mcexpectation * Missing reference * Update runtests.jl * Adressing most comments * Fixes Normal and MvNormal * Fix bug * Fix bug exponential * Passing tests * Apply suggestions from code review Co-authored-by: David Widmann * Externalisation of the function * Fix typo * Use invoke instead * Added suggestions * Fix for passing test with lambda=0 * Wrong name for logdiff * Rewriting of expectations for univarates * Typo fix * Update src/univariate/continuous/exponential.jl Co-authored-by: David Widmann * Deprecates 3-arguments expectation * Moved functional tests to kldivergences.jl * Apply suggestions from code review Co-authored-by: David Widmann * Proper removal of functionals.jl * Version bump * Update src/deprecates.jl Co-authored-by: David Widmann * Changed name to functionals.jl and fixed struct placement * Reformulated tests * Update tests * Fix tests * Do not export `expectation` Co-authored-by: David Widmann Co-authored-by: David Widmann --- Project.toml | 2 +- src/Distributions.jl | 1 + src/deprecates.jl | 4 + src/functionals.jl | 41 ++++---- src/multivariate/mvnormal.jl | 13 +++ src/univariate/continuous/beta.jl | 6 ++ src/univariate/continuous/exponential.jl | 5 + src/univariate/continuous/gamma.jl | 8 ++ src/univariate/continuous/inversegamma.jl | 5 + src/univariate/continuous/normal.jl | 9 ++ src/univariate/discrete/poisson.jl | 7 ++ test/functionals.jl | 113 ++++++++++++++++++++-- test/runtests.jl | 2 +- 13 files changed, 188 insertions(+), 28 deletions(-) diff --git a/Project.toml b/Project.toml index 30e9fefb0..ae31d1f56 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.23" +version = "0.25.24" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/src/Distributions.jl b/src/Distributions.jl index 34d0ad798..5c4ebccfc 100644 --- a/src/Distributions.jl +++ b/src/Distributions.jl @@ -204,6 +204,7 @@ export islowerbounded, isbounded, hasfinitesupport, + kldivergence, # kl divergence between distributions kurtosis, # kurtosis of the distribution logccdf, # ccdf returning log-probability logcdf, # cdf returning log-probability diff --git a/src/deprecates.jl b/src/deprecates.jl index 906d8ee28..5c0cc11b1 100644 --- a/src/deprecates.jl +++ b/src/deprecates.jl @@ -46,3 +46,7 @@ end @deprecate Wishart(df::Real, S::AbstractPDMat, warn::Bool) Wishart(df, S) @deprecate Wishart(df::Real, S::Matrix, warn::Bool) Wishart(df, S) @deprecate Wishart(df::Real, S::Cholesky, warn::Bool) Wishart(df, S) + +# Deprecate 3 arguments expectation +@deprecate expectation(distr::DiscreteUnivariateDistribution, g::Function, epsilon::Real) expectation(distr, g; epsilon=epsilon) false +@deprecate expectation(distr::ContinuousUnivariateDistribution, g::Function, epsilon::Real) expectation(distr, g) false diff --git a/src/functionals.jl b/src/functionals.jl index fac016d07..57e72c88b 100644 --- a/src/functionals.jl +++ b/src/functionals.jl @@ -1,27 +1,24 @@ -function getEndpoints(distr::UnivariateDistribution, epsilon::Real) - (left,right) = map(x -> quantile(distr,x), (0,1)) - leftEnd = left!=-Inf ? left : quantile(distr, epsilon) - rightEnd = right!=-Inf ? right : quantile(distr, 1-epsilon) - (leftEnd, rightEnd) -end - -function expectation(distr::ContinuousUnivariateDistribution, g::Function, epsilon::Real) - f = x->pdf(distr,x) - (leftEnd, rightEnd) = getEndpoints(distr, epsilon) - quadgk(x -> f(x)*g(x), leftEnd, rightEnd)[1] +function expectation(distr::ContinuousUnivariateDistribution, g::Function; kwargs...) + return first(quadgk(x -> pdf(distr, x) * g(x), extrema(distr)...; kwargs...)) end ## Assuming that discrete distributions only take integer values. -function expectation(distr::DiscreteUnivariateDistribution, g::Function, epsilon::Real) - f = x->pdf(distr,x) - (leftEnd, rightEnd) = getEndpoints(distr, epsilon) - sum(x -> f(x)*g(x), leftEnd:rightEnd) +function expectation(distr::DiscreteUnivariateDistribution, g::Function; epsilon::Real=1e-10) + mindist, maxdist = extrema(distr) + # We want to avoid taking values up to infinity + minval = isfinite(mindist) ? mindist : quantile(distr, epsilon) + maxval = isfinite(maxdist) ? maxdist : quantile(distr, 1 - epsilon) + return sum(x -> pdf(distr, x) * g(x), minval:maxval) end -function expectation(distr::UnivariateDistribution, g::Function) - expectation(distr, g, 1e-10) +function expectation(distr::MultivariateDistribution, g::Function; nsamples::Int=100, rng::AbstractRNG=GLOBAL_RNG) + nsamples > 0 || throw(ArgumentError("number of samples should be > 0")) + # We use a function barrier to work around type instability of `sampler(dist)` + return mcexpectation(rng, g, sampler(distr), nsamples) end +mcexpectation(rng, f, sampler, n) = sum(f, rand(rng, sampler) for _ in 1:n) / n + ## Leave undefined until we've implemented a numerical integration procedure # function entropy(distr::UnivariateDistribution) # pf = typeof(distr)<:ContinuousDistribution ? pdf : pmf @@ -29,6 +26,10 @@ end # expectation(distr, x -> -log(f(x))) # end -function kldivergence(P::UnivariateDistribution, Q::UnivariateDistribution) - expectation(P, x -> let p = pdf(P,x); (p > 0)*log(p/pdf(Q,x)) end) -end +function kldivergence(P::Distribution{V}, Q::Distribution{V}; kwargs...) where {V<:VariateForm} + function logdiff(x) + logp = logpdf(P, x) + return (logp > oftype(logp, -Inf)) * (logp - logpdf(Q, x)) + end + expectation(P, logdiff; kwargs...) +end \ No newline at end of file diff --git a/src/multivariate/mvnormal.jl b/src/multivariate/mvnormal.jl index 3827af61a..cef84c17b 100644 --- a/src/multivariate/mvnormal.jl +++ b/src/multivariate/mvnormal.jl @@ -100,6 +100,18 @@ end mvnormal_c0(g::AbstractMvNormal) = -(length(g) * convert(eltype(g), log2π) + logdetcov(g))/2 +function kldivergence(p::AbstractMvNormal, q::AbstractMvNormal) + # This is the generic implementation for AbstractMvNormal, you might need to specialize for your type + length(p) == length(q) || + throw(DimensionMismatch("Distributions p and q have different dimensions $(length(p)) and $(length(q))")) + # logdetcov is used separately from _cov for any potential optimization done there + return (tr(_cov(q) \ _cov(p)) + sqmahal(q, mean(p)) - length(p) + logdetcov(q) - logdetcov(p)) / 2 +end + +# This is a workaround to take advantage of the PDMats objects for MvNormal and avoid copies as Matrix +# TODO: Remove this once `cov(::MvNormal)` returns the PDMats object +_cov(d::AbstractMvNormal) = cov(d) + """ invcov(d::AbstractMvNormal) @@ -242,6 +254,7 @@ params(d::MvNormal) = (d.μ, d.Σ) var(d::MvNormal) = diag(d.Σ) cov(d::MvNormal) = Matrix(d.Σ) +_cov(d::MvNormal) = d.Σ invcov(d::MvNormal) = Matrix(inv(d.Σ)) logdetcov(d::MvNormal) = logdet(d.Σ) diff --git a/src/univariate/continuous/beta.jl b/src/univariate/continuous/beta.jl index 5bc3f2f2b..fa85733c3 100644 --- a/src/univariate/continuous/beta.jl +++ b/src/univariate/continuous/beta.jl @@ -107,6 +107,12 @@ function entropy(d::Beta) (s - 2) * digamma(s) end +function kldivergence(p::Beta, q::Beta) + αp, βp = params(p) + αq, βq = params(q) + return logbeta(αq, βq) - logbeta(αp, βp) + (αp - αq) * digamma(αp) + + (βp - βq) * digamma(βp) + (αq - αp + βq - βp) * digamma(αp + βp) +end #### Evaluation diff --git a/src/univariate/continuous/exponential.jl b/src/univariate/continuous/exponential.jl index 6c00447db..5c395c962 100644 --- a/src/univariate/continuous/exponential.jl +++ b/src/univariate/continuous/exponential.jl @@ -60,6 +60,11 @@ kurtosis(::Exponential{T}) where {T} = T(6) entropy(d::Exponential{T}) where {T} = one(T) + log(d.θ) +function kldivergence(p::Exponential, q::Exponential) + λq_over_λp = scale(q) / scale(p) + return -logmxp1(λq_over_λp) +end + #### Evaluation zval(d::Exponential, x::Real) = max(x / d.θ, 0) diff --git a/src/univariate/continuous/gamma.jl b/src/univariate/continuous/gamma.jl index fb6ac1bcc..de550cb35 100644 --- a/src/univariate/continuous/gamma.jl +++ b/src/univariate/continuous/gamma.jl @@ -79,6 +79,14 @@ mgf(d::Gamma, t::Real) = (1 - t * d.θ)^(-d.α) cf(d::Gamma, t::Real) = (1 - im * t * d.θ)^(-d.α) +function kldivergence(p::Gamma, q::Gamma) + # We use the parametrization with the scale θ + αp, θp = params(p) + αq, θq = params(q) + θp_over_θq = θp / θq + return (αp - αq) * digamma(αp) - loggamma(αp) + loggamma(αq) - + αq * log(θp_over_θq) + αp * (θp_over_θq - 1) +end #### Evaluation & Sampling diff --git a/src/univariate/continuous/inversegamma.jl b/src/univariate/continuous/inversegamma.jl index 75e18bd50..2c28f5605 100644 --- a/src/univariate/continuous/inversegamma.jl +++ b/src/univariate/continuous/inversegamma.jl @@ -83,6 +83,11 @@ function entropy(d::InverseGamma) α + loggamma(α) - (1 + α) * digamma(α) + log(θ) end +function kldivergence(p::InverseGamma, q::InverseGamma) + # We can reuse the implementation of Gamma + return kldivergence(p.invd, q.invd) +end + #### Evaluation diff --git a/src/univariate/continuous/normal.jl b/src/univariate/continuous/normal.jl index c4c646666..fe5feab84 100644 --- a/src/univariate/continuous/normal.jl +++ b/src/univariate/continuous/normal.jl @@ -75,6 +75,15 @@ kurtosis(d::Normal{T}) where {T<:Real} = zero(T) entropy(d::Normal) = (log2π + 1)/2 + log(d.σ) +function kldivergence(p::Normal, q::Normal) + μp = mean(p) + σ²p = var(p) + μq = mean(q) + σ²q = var(q) + σ²p_over_σ²q = σ²p / σ²q + return (abs2(μp - μq) / σ²q - logmxp1(σ²p_over_σ²q)) / 2 +end + #### Evaluation # Helpers diff --git a/src/univariate/discrete/poisson.jl b/src/univariate/discrete/poisson.jl index edce7dc0d..75fa25b36 100644 --- a/src/univariate/discrete/poisson.jl +++ b/src/univariate/discrete/poisson.jl @@ -84,6 +84,13 @@ function entropy(d::Poisson{T}) where T<:Real end end +function kldivergence(p::Poisson, q::Poisson) + λp = rate(p) + λq = rate(q) + # `false` is a strong zero and ensures that `λp = 0` is handled correctly + # we don't use `xlogy` since it returns `NaN` for `λp = λq = 0` + return λq - λp + (λp > 0) * (λp * log(λp / λq)) +end ### Evaluation diff --git a/test/functionals.jl b/test/functionals.jl index 1fb384db4..9c8007ad3 100644 --- a/test/functionals.jl +++ b/test/functionals.jl @@ -1,6 +1,107 @@ -using Test -using Distributions: Categorical, kldivergence, expectation, Normal -@test kldivergence(Categorical([0.0, 0.1, 0.9]), Categorical([0.1, 0.1, 0.8])) ≥ 0 -@test kldivergence(Categorical([0.0, 0.1, 0.9]), Categorical([0.1, 0.1, 0.8])) ≈ - kldivergence([0.0, 0.1, 0.9], [0.1, 0.1, 0.8]) -@test expectation(Normal(0.0, 1.0), identity, 1e-10) ≤ 1e-9 +# Struct to test AbstractMvNormal methods +struct CholeskyMvNormal{M,T} <: Distributions.AbstractMvNormal + m::M + L::T +end + +# Constructor for diagonal covariance matrices used in the tests belows +function CholeskyMvNormal(m::Vector, Σ::Diagonal) + L = Diagonal(map(sqrt, Σ.diag)) + return CholeskyMvNormal{typeof(m),typeof(L)}(m, L) +end + +Distributions.length(p::CholeskyMvNormal) = length(p.m) +Distributions.mean(p::CholeskyMvNormal) = p.m +Distributions.cov(p::CholeskyMvNormal) = p.L * p.L' +Distributions.logdetcov(p::CholeskyMvNormal) = 2 * logdet(p.L) +function Distributions.sqmahal(p::CholeskyMvNormal, x::AbstractVector) + return sum(abs2, p.L \ (mean(p) - x)) +end +function Distributions._rand!(rng::AbstractRNG, p::CholeskyMvNormal, x::Vector) + return x .= p.m .+ p.L * randn!(rng, x) +end + +@testset "Expectations" begin + # univariate distributions + for d in (Normal(), Poisson(2.0), Binomial(10, 0.4)) + @test Distributions.expectation(d, identity) ≈ mean(d) atol=1e-3 + @test @test_deprecated(Distributions.expectation(d, identity, 1e-10)) ≈ mean(d) atol=1e-3 + end + + # multivariate distribution + d = MvNormal([1.5, -0.5], I) + @test Distributions.expectation(d, identity; nsamples=10_000) ≈ mean(d) atol=1e-2 +end + +@testset "KL divergences" begin + function test_kl(p, q) + @test kldivergence(p, q) >= 0 + @test kldivergence(p, p) ≈ 0 atol=1e-1 + @test kldivergence(q, q) ≈ 0 atol=1e-1 + if p isa UnivariateDistribution + @test kldivergence(p, q) ≈ invoke(kldivergence, Tuple{UnivariateDistribution,UnivariateDistribution}, p, q) atol=1e-1 + elseif p isa MultivariateDistribution + @test kldivergence(p, q) ≈ invoke(kldivergence, Tuple{MultivariateDistribution,MultivariateDistribution}, p, q; nsamples=10000) atol=1e-1 + end + end + + @testset "univariate" begin + @testset "Beta" begin + p = Beta(2, 10) + q = Beta(3, 5) + test_kl(p, q) + end + @testset "Categorical" begin + @test kldivergence(Categorical([0.0, 0.1, 0.9]), Categorical([0.1, 0.1, 0.8])) ≥ 0 + @test kldivergence(Categorical([0.0, 0.1, 0.9]), Categorical([0.1, 0.1, 0.8])) ≈ + kldivergence([0.0, 0.1, 0.9], [0.1, 0.1, 0.8]) + end + @testset "Exponential" begin + p = Exponential(2.0) + q = Exponential(3.0) + test_kl(p, q) + end + @testset "Gamma" begin + p = Gamma(2.0, 1.0) + q = Gamma(3.0, 2.0) + test_kl(p, q) + end + @testset "InverseGamma" begin + p = InverseGamma(2.0, 1.0) + q = InverseGamma(3.0, 2.0) + test_kl(p, q) + end + @testset "Normal" begin + p = Normal(0, 1) + q = Normal(0.5, 0.5) + test_kl(p, q) + end + @testset "Poisson" begin + p = Poisson(4.0) + q = Poisson(3.0) + test_kl(p, q) + + # special case (test function also checks `kldivergence(p0, p0)`) + p0 = Poisson(0.0) + test_kl(p0, p) + end + end + + @testset "multivariate" begin + @testset "AbstractMvNormal" begin + p_mvnormal = MvNormal([0.2, -0.8], Diagonal([0.5, 0.75])) + q_mvnormal = MvNormal([1.5, 0.5], Diagonal([1.0, 0.2])) + test_kl(p_mvnormal, q_mvnormal) + + p_cholesky = CholeskyMvNormal([0.2, -0.8], Diagonal([0.5, 0.75])) + q_cholesky = CholeskyMvNormal([1.5, 0.5], Diagonal([1.0, 0.2])) + test_kl(p_cholesky, q_cholesky) + + # check consistency and mixed computations + v = kldivergence(p_mvnormal, q_mvnormal) + @test kldivergence(p_mvnormal, q_cholesky) ≈ v + @test kldivergence(p_cholesky, q_mvnormal) ≈ v + @test kldivergence(p_cholesky, q_cholesky) ≈ v + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 1313eff57..ad1d6c989 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -53,7 +53,6 @@ const tests = [ "pgeneralizedgaussian", "product", "discretenonparametric", - "functionals", "chernoff", "univariate_bounds", "negativebinomial", @@ -64,6 +63,7 @@ const tests = [ "gumbel", "pdfnorm", "rician", + "functionals", ] printstyled("Running tests:\n", color=:blue) From 2fbe852643916fef120415ad13fa6ed5f28a0d23 Mon Sep 17 00:00:00 2001 From: Oliver Schulz Date: Tue, 9 Nov 2021 17:31:20 +0100 Subject: [PATCH 34/58] Support DensityInterface API (#1416) * Support DensityInterface API * Increase version number to v0.25.25 Co-authored-by: David Widmann --- Project.toml | 4 +++- docs/make.jl | 1 + docs/src/density_interface.md | 5 +++++ src/Distributions.jl | 5 +++++ src/density_interface.jl | 19 +++++++++++++++++++ test/density_interface.jl | 25 +++++++++++++++++++++++++ test/runtests.jl | 3 ++- 7 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 docs/src/density_interface.md create mode 100644 src/density_interface.jl create mode 100644 test/density_interface.jl diff --git a/Project.toml b/Project.toml index ae31d1f56..57003b6ac 100644 --- a/Project.toml +++ b/Project.toml @@ -1,10 +1,11 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.24" +version = "0.25.25" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" +DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d" FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" PDMats = "90014a1f-27ba-587c-ab20-58faa44d9150" @@ -19,6 +20,7 @@ StatsFuns = "4c63d2b9-4356-54db-8cca-17b64c39e42c" [compat] ChainRulesCore = "1" +DensityInterface = "0.3.2" FillArrays = "0.9, 0.10, 0.11, 0.12" PDMats = "0.10, 0.11" QuadGK = "2" diff --git a/docs/make.jl b/docs/make.jl index 0ad7367d5..a92bc4679 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -17,6 +17,7 @@ makedocs( "mixture.md", "fit.md", "extends.md", + "density_interface.md", ] ) diff --git a/docs/src/density_interface.md b/docs/src/density_interface.md new file mode 100644 index 000000000..5c01357df --- /dev/null +++ b/docs/src/density_interface.md @@ -0,0 +1,5 @@ +# Support for DensityInterface + +`Distributions` supports [`DensityInterface`](https://github.com/JuliaMath/DensityInterface.jl) for distributions. + +For *single* variate values `x`, `DensityInterface.logdensityof(d::Distribution, x)` is equivalent to `logpdf(d, x)` and `DensityInterface.densityof(d::Distribution, x)` is equivalent to `pdf(d, x)`. diff --git a/src/Distributions.jl b/src/Distributions.jl index 5c4ebccfc..580db0e63 100644 --- a/src/Distributions.jl +++ b/src/Distributions.jl @@ -27,6 +27,8 @@ using SpecialFunctions import ChainRulesCore +import DensityInterface + export # re-export Statistics mean, median, quantile, std, var, cov, cor, @@ -299,6 +301,9 @@ include("pdfnorm.jl") include("mixtures/mixturemodel.jl") include("mixtures/unigmm.jl") +# Implementation of DensityInterface API +include("density_interface.jl") + include("deprecates.jl") """ diff --git a/src/density_interface.jl b/src/density_interface.jl new file mode 100644 index 000000000..3049a6903 --- /dev/null +++ b/src/density_interface.jl @@ -0,0 +1,19 @@ +@inline DensityInterface.hasdensity(::Distribution) = true + +for (di_func, d_func) in ((:logdensityof, :logpdf), (:densityof, :pdf)) + @eval begin + DensityInterface.$di_func(d::Distribution, x) = $d_func(d, x) + + function DensityInterface.$di_func(d::UnivariateDistribution, x::AbstractArray) + throw(ArgumentError("$(DensityInterface.$di_func) doesn't support multiple samples as an argument")) + end + + function DensityInterface.$di_func(d::MultivariateDistribution, x::AbstractMatrix) + throw(ArgumentError("$(DensityInterface.$di_func) doesn't support multiple samples as an argument")) + end + + function DensityInterface.$di_func(d::MatrixDistribution, x::AbstractArray{<:AbstractMatrix{<:Real}}) + throw(ArgumentError("$(DensityInterface.$di_func) doesn't support multiple samples as an argument")) + end + end +end diff --git a/test/density_interface.jl b/test/density_interface.jl new file mode 100644 index 000000000..e4988986d --- /dev/null +++ b/test/density_interface.jl @@ -0,0 +1,25 @@ +@testset "DensityInterface" begin + using DensityInterface + + d_uv_continous = Normal(-1.5, 2.3) + d_uv_discrete = Poisson(4.7) + d_mv = MvNormal([2.3 0.4; 0.4 1.2]) + d_av = Distributions.MatrixReshaped(MvNormal(rand(10)), 2, 5) + + @testset "Distribution" begin + for d in (d_uv_continous, d_uv_discrete, d_mv, d_av) + x = rand(d) + ref_logd_at_x = logpdf(d, x) + DensityInterface.test_density_interface(d, x, ref_logd_at_x) + + # Stricter than required by test_density_interface: + @test logfuncdensity(logdensityof(d)) === d + end + + for di_func in (logdensityof, densityof) + @test_throws ArgumentError di_func(d_uv_continous, [rand(d_uv_continous) for i in 1:3]) + @test_throws ArgumentError di_func(d_mv, hcat([rand(d_mv) for i in 1:3]...)) + @test_throws ArgumentError di_func(d_av, [rand(d_av) for i in 1:3]) + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index ad1d6c989..f04731d7f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -63,7 +63,8 @@ const tests = [ "gumbel", "pdfnorm", "rician", - "functionals", + "functionals", + "density_interface", ] printstyled("Running tests:\n", color=:blue) From 5d879bacf208ab7531ff9d438a8d6c75e30aa8d1 Mon Sep 17 00:00:00 2001 From: willtebbutt Date: Wed, 10 Nov 2021 15:00:25 +0000 Subject: [PATCH 35/58] MvNormal Test Suite inside src (#1418) * Move test_mvnormal into src * Bump patch * Test seedless rand methods --- Project.toml | 3 +- src/Distributions.jl | 3 ++ src/test_utils.jl | 96 ++++++++++++++++++++++++++++++++++++++++++++ test/mvnormal.jl | 88 +--------------------------------------- 4 files changed, 102 insertions(+), 88 deletions(-) create mode 100644 src/test_utils.jl diff --git a/Project.toml b/Project.toml index 57003b6ac..2be01427a 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.25" +version = "0.25.26" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" @@ -17,6 +17,7 @@ SpecialFunctions = "276daf66-3868-5448-9aa4-cd146d93841b" Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" StatsFuns = "4c63d2b9-4356-54db-8cca-17b64c39e42c" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] ChainRulesCore = "1" diff --git a/src/Distributions.jl b/src/Distributions.jl index 580db0e63..3211bec3d 100644 --- a/src/Distributions.jl +++ b/src/Distributions.jl @@ -304,6 +304,9 @@ include("mixtures/unigmm.jl") # Implementation of DensityInterface API include("density_interface.jl") +# Testing utilities for other packages which implement distributions. +include("test_utils.jl") + include("deprecates.jl") """ diff --git a/src/test_utils.jl b/src/test_utils.jl new file mode 100644 index 000000000..98d1f8a7a --- /dev/null +++ b/src/test_utils.jl @@ -0,0 +1,96 @@ +module TestUtils + +using Distributions +using LinearAlgebra +using Random +using Test + + +__rand(::Nothing, args...) = rand(args...) +__rand(rng::AbstractRNG, args...) = rand(rng, args...) + +__rand!(::Nothing, args...) = rand!(args...) +__rand!(rng::AbstractRNG, args...) = rand!(rng, args...) + +""" + test_mvnormal( + g::AbstractMvNormal, n_tsamples::Int=10^6, rng::AbstractRNG=Random.GLOBAL_RNG + ) + +Test that `AbstractMvNormal` implements the expected API. +""" +function test_mvnormal( + g::AbstractMvNormal, n_tsamples::Int=10^6, rng::Union{AbstractRNG, Nothing}=nothing +) + d = length(g) + μ = mean(g) + Σ = cov(g) + @test length(μ) == d + @test size(Σ) == (d, d) + @test var(g) ≈ diag(Σ) + @test entropy(g) ≈ 0.5 * logdet(2π * ℯ * Σ) + ldcov = logdetcov(g) + @test ldcov ≈ logdet(Σ) + vs = diag(Σ) + @test g == typeof(g)(params(g)...) + @test g == deepcopy(g) + @test minimum(g) == fill(-Inf, d) + @test maximum(g) == fill(Inf, d) + @test extrema(g) == (minimum(g), maximum(g)) + @test isless(extrema(g)...) + + # test sampling for AbstractMatrix (here, a SubArray): + subX = view(__rand(rng, d, 2d), :, 1:d) + @test isa(__rand!(rng, g, subX), SubArray) + + # sampling + @test isa(__rand(rng, g), Vector{Float64}) + X = __rand(rng, g, n_tsamples) + emp_mu = vec(mean(X, dims=2)) + Z = X .- emp_mu + emp_cov = (Z * Z') * inv(n_tsamples) + + mean_atols = 8 .* sqrt.(vs ./ n_tsamples) + cov_atols = 10 .* sqrt.(vs .* vs') ./ sqrt.(n_tsamples) + for i = 1:d + @test isapprox(emp_mu[i], μ[i], atol=mean_atols[i]) + end + for i = 1:d, j = 1:d + @test isapprox(emp_cov[i,j], Σ[i,j], atol=cov_atols[i,j]) + end + + X = rand(MersenneTwister(14), g, n_tsamples) + Y = rand(MersenneTwister(14), g, n_tsamples) + @test X == Y + emp_mu = vec(mean(X, dims=2)) + Z = X .- emp_mu + emp_cov = (Z * Z') * inv(n_tsamples) + for i = 1:d + @test isapprox(emp_mu[i] , μ[i] , atol=mean_atols[i]) + end + for i = 1:d, j = 1:d + @test isapprox(emp_cov[i,j], Σ[i,j], atol=cov_atols[i,j]) + end + + + # evaluation of sqmahal & logpdf + U = X .- μ + sqm = vec(sum(U .* (Σ \ U), dims=1)) + for i = 1:min(100, n_tsamples) + @test sqmahal(g, X[:,i]) ≈ sqm[i] + end + @test sqmahal(g, X) ≈ sqm + + lp = -0.5 .* sqm .- 0.5 * (d * log(2.0 * pi) + ldcov) + for i = 1:min(100, n_tsamples) + @test logpdf(g, X[:,i]) ≈ lp[i] + end + @test logpdf(g, X) ≈ lp + + # log likelihood + @test loglikelihood(g, X) ≈ sum(i -> Distributions._logpdf(g, X[:,i]), 1:n_tsamples) + @test loglikelihood(g, X[:, 1]) ≈ logpdf(g, X[:, 1]) + @test loglikelihood(g, [X[:, i] for i in axes(X, 2)]) ≈ loglikelihood(g, X) +end + +end diff --git a/test/mvnormal.jl b/test/mvnormal.jl index 934ec2911..d5f99ab90 100644 --- a/test/mvnormal.jl +++ b/test/mvnormal.jl @@ -10,92 +10,6 @@ using LinearAlgebra, Random, Test using SparseArrays using FillArrays -import Distributions: distrname - - - -####### Core testing procedure - -function test_mvnormal(g::AbstractMvNormal, n_tsamples::Int=10^6, - rng::Union{AbstractRNG, Missing} = missing) - d = length(g) - μ = mean(g) - Σ = cov(g) - @test length(μ) == d - @test size(Σ) == (d, d) - @test var(g) ≈ diag(Σ) - @test entropy(g) ≈ 0.5 * logdet(2π * ℯ * Σ) - ldcov = logdetcov(g) - @test ldcov ≈ logdet(Σ) - vs = diag(Σ) - @test g == typeof(g)(params(g)...) - @test g == deepcopy(g) - @test minimum(g) == fill(-Inf, d) - @test maximum(g) == fill(Inf, d) - @test extrema(g) == (minimum(g), maximum(g)) - @test isless(extrema(g)...) - - # test sampling for AbstractMatrix (here, a SubArray): - if ismissing(rng) - subX = view(rand(d, 2d), :, 1:d) - @test isa(rand!(g, subX), SubArray) - else - subX = view(rand(rng, d, 2d), :, 1:d) - @test isa(rand!(rng, g, subX), SubArray) - end - - # sampling - if ismissing(rng) - @test isa(rand(g), Vector{Float64}) - X = rand(g, n_tsamples) - else - @test isa(rand(rng, g), Vector{Float64}) - X = rand(rng, g, n_tsamples) - end - emp_mu = vec(mean(X, dims=2)) - Z = X .- emp_mu - emp_cov = (Z * Z') * inv(n_tsamples) - for i = 1:d - @test isapprox(emp_mu[i], μ[i], atol=sqrt(vs[i] / n_tsamples) * 8.0) - end - for i = 1:d, j = 1:d - @test isapprox(emp_cov[i,j], Σ[i,j], atol=sqrt(vs[i] * vs[j]) * 10.0 / sqrt(n_tsamples)) - end - - X = rand(MersenneTwister(14), g, n_tsamples) - Y = rand(MersenneTwister(14), g, n_tsamples) - @test X == Y - emp_mu = vec(mean(X, dims=2)) - Z = X .- emp_mu - emp_cov = (Z * Z') * inv(n_tsamples) - for i = 1:d - @test isapprox(emp_mu[i] , μ[i] , atol=sqrt(vs[i] / n_tsamples) * 8.0) - end - for i = 1:d, j = 1:d - @test isapprox(emp_cov[i,j], Σ[i,j], atol=sqrt(vs[i] * vs[j]) * 10.0 / sqrt(n_tsamples)) - end - - - # evaluation of sqmahal & logpdf - U = X .- μ - sqm = vec(sum(U .* (Σ \ U), dims=1)) - for i = 1:min(100, n_tsamples) - @test sqmahal(g, X[:,i]) ≈ sqm[i] - end - @test sqmahal(g, X) ≈ sqm - - lp = -0.5 .* sqm .- 0.5 * (d * log(2.0 * pi) + ldcov) - for i = 1:min(100, n_tsamples) - @test logpdf(g, X[:,i]) ≈ lp[i] - end - @test logpdf(g, X) ≈ lp - - # log likelihood - @test loglikelihood(g, X) ≈ sum([Distributions._logpdf(g, X[:,i]) for i in 1:size(X, 2)]) - @test loglikelihood(g, X[:, 1]) ≈ logpdf(g, X[:, 1]) - @test loglikelihood(g, [X[:, i] for i in axes(X, 2)]) ≈ loglikelihood(g, X) -end - ###### General Testing @testset "MvNormal tests" begin @@ -136,7 +50,7 @@ end @test mean(g) ≈ μ @test cov(g) ≈ Σ @test invcov(g) ≈ inv(Σ) - test_mvnormal(g, 10^4) + Distributions.TestUtils.test_mvnormal(g, 10^4) # conversion between mean form and canonical form if isa(g, MvNormal) From feea09df4f68cecd56115986084eecd76a72d04f Mon Sep 17 00:00:00 2001 From: David Widmann Date: Wed, 10 Nov 2021 18:52:22 +0100 Subject: [PATCH 36/58] Remove `truncated(d, ::Integer, ::Integer)` (#1419) --- Project.toml | 2 +- src/truncate.jl | 2 -- src/truncated/uniform.jl | 4 +--- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 2be01427a..bcb8a7059 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.26" +version = "0.25.27" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/src/truncate.jl b/src/truncate.jl index e040cd968..6521ed579 100644 --- a/src/truncate.jl +++ b/src/truncate.jl @@ -39,8 +39,6 @@ function truncated(d::UnivariateDistribution, l::T, u::T) where {T <: Real} Truncated(d, promote(l, u, lcdf, ucdf, tp, logtp)...) end -truncated(d::UnivariateDistribution, l::Integer, u::Integer) = truncated(d, float(l), float(u)) - """ Truncated diff --git a/src/truncated/uniform.jl b/src/truncated/uniform.jl index a8681922c..3e345cde1 100644 --- a/src/truncated/uniform.jl +++ b/src/truncated/uniform.jl @@ -2,6 +2,4 @@ ##### Shortcut for truncating uniform distributions. ##### -truncated(d::Uniform, l::T, u::T) where {T <: Real} = Uniform(promote(max(l, d.a), min(u, d.b))...) - -truncated(d::Uniform, l::Integer, u::Integer) = truncated(d, float(l), float(u)) +truncated(d::Uniform, l::T, u::T) where {T <: Real} = Uniform(max(l, d.a), min(u, d.b)) From ce08a92eb71921e5ceb1e71de3f78aa72c12363c Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Thu, 11 Nov 2021 14:07:48 +0100 Subject: [PATCH 37/58] add LogUniform (#1349) * add LogUniform * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * add basic tests for LogUniform * fix * document LogUniform * improve LogUniform docs * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * Update test/loguniform.jl Co-authored-by: David Widmann * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * improve LogUniform tests * Update src/truncated/loguniform.jl Co-authored-by: David Widmann * cosmetics * Update src/truncated/loguniform.jl Co-authored-by: David Widmann * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * plot LogUniform denisty in docs * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * add more comments * Update src/univariate/continuous/loguniform.jl Allow `kwargs` Co-authored-by: David Widmann Co-authored-by: David Widmann Co-authored-by: Moritz Schauer --- docs/src/univariate.md | 7 +++ src/Distributions.jl | 1 + src/truncate.jl | 1 + src/truncated/loguniform.jl | 1 + src/univariate/continuous/loguniform.jl | 64 ++++++++++++++++++++++++ src/univariates.jl | 1 + test/loguniform.jl | 65 +++++++++++++++++++++++++ test/runtests.jl | 1 + 8 files changed, 141 insertions(+) create mode 100644 src/truncated/loguniform.jl create mode 100644 src/univariate/continuous/loguniform.jl create mode 100644 test/loguniform.jl diff --git a/docs/src/univariate.md b/docs/src/univariate.md index 88321cc4e..f9f1922a7 100644 --- a/docs/src/univariate.md +++ b/docs/src/univariate.md @@ -327,6 +327,13 @@ LogNormal plotdensity((0, 5), LogNormal, (0, 1)) # hide ``` +```@docs +LogUniform +``` +```@example plotdensity +plotdensity((0, 11), LogUniform, (1, 10)) # hide +``` + ```@docs NoncentralBeta ``` diff --git a/src/Distributions.jl b/src/Distributions.jl index 3211bec3d..52c19f770 100644 --- a/src/Distributions.jl +++ b/src/Distributions.jl @@ -118,6 +118,7 @@ export LocationScale, Logistic, LogNormal, + LogUniform, LogitNormal, MatrixBeta, MatrixFDist, diff --git a/src/truncate.jl b/src/truncate.jl index 6521ed579..ba76b7757 100644 --- a/src/truncate.jl +++ b/src/truncate.jl @@ -172,3 +172,4 @@ _use_multline_show(d::Truncated) = _use_multline_show(d.untruncated) include(joinpath("truncated", "normal.jl")) include(joinpath("truncated", "exponential.jl")) include(joinpath("truncated", "uniform.jl")) +include(joinpath("truncated", "loguniform.jl")) diff --git a/src/truncated/loguniform.jl b/src/truncated/loguniform.jl new file mode 100644 index 000000000..d4814fc3a --- /dev/null +++ b/src/truncated/loguniform.jl @@ -0,0 +1 @@ +truncated(d::LogUniform, lo::T, hi::T) where {T<:Real} = LogUniform(max(d.a, lo), min(d.b, hi)) diff --git a/src/univariate/continuous/loguniform.jl b/src/univariate/continuous/loguniform.jl new file mode 100644 index 000000000..f2f74970c --- /dev/null +++ b/src/univariate/continuous/loguniform.jl @@ -0,0 +1,64 @@ +""" + LogUniform(a,b) + +A positive random variable `X` is log-uniformly with parameters `a` and `b` if the logarithm of `X` is `Uniform(log(a), log(b))`. +The *log uniform* distribution is also known as *reciprocal distribution*. +```julia +LogUniform(1,10) +``` +External links + +* [Log uniform distribution on Wikipedia](https://en.wikipedia.org/wiki/Reciprocal_distribution) +""" +struct LogUniform{T<:Real} <: ContinuousUnivariateDistribution + a::T + b::T + LogUniform{T}(a::T, b::T) where {T <: Real} = new{T}(a, b) +end + +function LogUniform(a::T, b::T; check_args=true) where {T <: Real} + check_args && @check_args(LogUniform, 0 < a < b) + LogUniform{T}(a, b) +end + +LogUniform(a::Real, b::Real; kwargs...) = LogUniform(promote(a, b)...; kwargs...) + +convert(::Type{LogUniform{T}}, d::LogUniform) where {T<:Real} = LogUniform(T(d.a), T(d.b)) +Base.minimum(d::LogUniform) = d.a +Base.maximum(d::LogUniform) = d.b + +#### Parameters +params(d::LogUniform) = (d.a, d.b) +partype(::LogUniform{T}) where {T<:Real} = T + +#### Statistics + +function mean(d::LogUniform) + a, b = params(d) + (b - a) / log(b/a) +end +function var(d::LogUniform) + a, b = params(d) + log_ba = log(b/a) + (b^2 - a^2) / (2*log_ba) - ((b-a)/ log_ba)^2 +end +mode(d::LogUniform) = d.a +modes(d::LogUniform) = partype(d)[] + +#### Evaluation +function pdf(d::LogUniform, x::Real) + x1, a, b = promote(x, params(d)...) # ensure e.g. pdf(LogUniform(1,2), 1f0)::Float32 + res = inv(x1 * log(b / a)) + return insupport(d, x1) ? res : zero(res) +end +function cdf(d::LogUniform, x::Real) + x1, a, b = promote(x, params(d)...) # ensure e.g. cdf(LogUniform(1,2), 1f0)::Float32 + x1 = clamp(x1, a, b) + return log(x1 / a) / log(b / a) +end +logpdf(d::LogUniform, x::Real) = log(pdf(d,x)) + +function quantile(d::LogUniform, p::Real) + p1,a,b = promote(p, params(d)...) # ensure e.g. quantile(LogUniform(1,2), 1f0)::Float32 + exp(p1 * log(b/a)) * a +end diff --git a/src/univariates.jl b/src/univariates.jl index 96d091655..52073863b 100644 --- a/src/univariates.jl +++ b/src/univariates.jl @@ -710,6 +710,7 @@ const continuous_distributions = [ "triangular", "triweight", "uniform", + "loguniform", # depends on Uniform "vonmises", "weibull" ] diff --git a/test/loguniform.jl b/test/loguniform.jl new file mode 100644 index 000000000..ee662b0e6 --- /dev/null +++ b/test/loguniform.jl @@ -0,0 +1,65 @@ +module TestLogUniform +using Test +using Distributions +import Random + +@testset "LogUniform" begin + rng = Random.MersenneTwister(0) + + @test pdf(LogUniform(1f0, 2f0), 1) isa Float32 + @test pdf(LogUniform(1, 2), 1f0) isa Float32 + @test pdf(LogUniform(1, 2), 1) isa Float64 + @test quantile(LogUniform(1, 2), 1) isa Float64 + @test quantile(LogUniform(1, 2), 1f0) isa Float32 + @testset "$f" for f in [pdf, cdf, quantile, logpdf, logcdf] + @test @inferred(f(LogUniform(1,2), 1)) isa Float64 + @test @inferred(f(LogUniform(1,2), 1.0)) isa Float64 + @test @inferred(f(LogUniform(1.0,2), 1.0)) isa Float64 + @test @inferred(f(LogUniform(1.0f0,2), 1)) isa Float32 + @test @inferred(f(LogUniform(1.0f0,2), 1f0)) isa Float32 + @test @inferred(f(LogUniform(1,2), 1f0)) isa Float32 + end + + d = LogUniform(1,10) + @test eltype(d) === Float64 + @test 1 <= rand(rng, d) <= 10 + @test rand(rng, d) isa eltype(d) + @test @inferred(quantile(d, 0)) ≈ 1 + @test quantile(d, 0.5) ≈ sqrt(10) # geomean + @test quantile(d, 1) ≈ 10 + @test mode(d) ≈ 1 + @test !insupport(d, 0) + @test @inferred(minimum(d)) === 1 + @test @inferred(maximum(d)) === 10 + @test partype(d) === Int + @test truncated(d, 2, 14) === LogUniform(2,10) + + # numbers obtained by calling scipy.stats.loguniform + @test @inferred(std(d) ) ≈ 2.49399867607628 + @test @inferred(mean(d) ) ≈ 3.908650337129266 + @test @inferred(pdf(d, 1.0001)) ≈ 0.43425105679757203 + @test @inferred(pdf(d, 5 )) ≈ 0.08685889638065035 + @test @inferred(pdf(d, 9.9999)) ≈ 0.04342988248915007 + @test @inferred(cdf(d, 1.0001)) ≈ 4.342727686266485e-05 + @test @inferred(cdf(d, 5 )) ≈ 0.6989700043360187 + @test @inferred(cdf(d, 9.9999)) ≈ 0.999995657033466 + @test @inferred(median(d) ) ≈ 3.1622776601683795 + @test @inferred(logpdf(d, 5) ) ≈ -2.443470357682056 + + for _ in 1:10 + lo = rand(rng) + hi = lo + 10*rand(rng) + dist = LogUniform(lo,hi) + q = rand(rng) + @test cdf(dist, quantile(dist, q)) ≈ q + + u = Uniform(log(lo), log(hi)) + @test exp(quantile(u, q)) ≈ quantile(dist, q) + @test exp(median(u)) ≈ median(dist) + x = rand(rng, dist) + @test cdf(u, log(x)) ≈ cdf(dist, x) + end +end + + +end#module diff --git a/test/runtests.jl b/test/runtests.jl index f04731d7f..6b7833bb5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -11,6 +11,7 @@ import JSON import ForwardDiff const tests = [ + "loguniform", "arcsine", "dirac", "truncate", From 25034b1242b29c06960ce92095dbc9b904fd6847 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Thu, 11 Nov 2021 14:11:18 +0100 Subject: [PATCH 38/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index bcb8a7059..e19111a02 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.27" +version = "0.25.28" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From 16d61d9d013403ab564793818dffbfab65a47ff4 Mon Sep 17 00:00:00 2001 From: Jan Weidner Date: Fri, 12 Nov 2021 01:02:10 +0100 Subject: [PATCH 39/58] add entropy(::LogUniform) (#1421) * add entropy(::LogUniform) * add kldivergence(::LogUniform, ::LogUniform) * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann * Update src/univariate/continuous/loguniform.jl Co-authored-by: David Widmann Co-authored-by: David Widmann --- src/univariate/continuous/loguniform.jl | 11 +++++++++++ test/loguniform.jl | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/univariate/continuous/loguniform.jl b/src/univariate/continuous/loguniform.jl index f2f74970c..cbfe1890e 100644 --- a/src/univariate/continuous/loguniform.jl +++ b/src/univariate/continuous/loguniform.jl @@ -45,6 +45,10 @@ end mode(d::LogUniform) = d.a modes(d::LogUniform) = partype(d)[] +function entropy(d::LogUniform) + a,b = params(d) + log(a * b) / 2 + log(log(b / a)) +end #### Evaluation function pdf(d::LogUniform, x::Real) x1, a, b = promote(x, params(d)...) # ensure e.g. pdf(LogUniform(1,2), 1f0)::Float32 @@ -62,3 +66,10 @@ function quantile(d::LogUniform, p::Real) p1,a,b = promote(p, params(d)...) # ensure e.g. quantile(LogUniform(1,2), 1f0)::Float32 exp(p1 * log(b/a)) * a end + +function kldivergence(p::LogUniform, q::LogUniform) + ap, bp, aq, bq = promote(params(p)..., params(q)...) + finite = aq <= ap < bp <= bq + res = log(log(bq / aq) / log(bp / ap)) + return finite ? res : oftype(res, Inf) +end diff --git a/test/loguniform.jl b/test/loguniform.jl index ee662b0e6..649c147b9 100644 --- a/test/loguniform.jl +++ b/test/loguniform.jl @@ -58,6 +58,26 @@ import Random @test exp(median(u)) ≈ median(dist) x = rand(rng, dist) @test cdf(u, log(x)) ≈ cdf(dist, x) + + @test @inferred(entropy(dist)) ≈ Distributions.expectation(dist, x->-logpdf(dist,x)) + end + + @test kldivergence(LogUniform(1,2), LogUniform(1,2)) ≈ 0 atol=100eps(Float64) + @test isfinite(kldivergence(LogUniform(1,2), LogUniform(1,10))) + @test kldivergence(LogUniform(1.1,10), LogUniform(1,2)) === Inf + @test kldivergence(LogUniform(0.1,10), LogUniform(1,2)) === Inf + @test kldivergence(LogUniform(0.1,1), LogUniform(1,2)) === Inf + @test @inferred(kldivergence(LogUniform(0.1f0,1), LogUniform(1,2))) === Inf32 + + for _ in 1:10 + aq = 10*rand(rng) + ap = aq + 10*rand(rng) + bp = ap + 10*rand(rng) + bq = bp + 10*rand(rng) + p = LogUniform(ap, bp) + q = LogUniform(aq, bq) + @test @inferred(kldivergence(p, q)) ≈ + kldivergence(Uniform(log(ap), log(bp)), Uniform(log(aq), log(bq))) end end From 837d3128bfbb30a0aa00e7372b99313f30da635f Mon Sep 17 00:00:00 2001 From: David Widmann Date: Fri, 12 Nov 2021 01:02:47 +0100 Subject: [PATCH 40/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index e19111a02..76fc50457 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.28" +version = "0.25.29" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From 8887d7a16a2801dc869ab42ba03cad03efc5ed9d Mon Sep 17 00:00:00 2001 From: David Widmann Date: Fri, 12 Nov 2021 15:56:49 +0100 Subject: [PATCH 41/58] Change order of arguments in `expectation` (#1420) * Change order of arguments in `expectation` * Fix deprecation --- src/deprecates.jl | 7 ++++--- src/functionals.jl | 11 +++++------ test/binomial.jl | 4 ++-- test/functionals.jl | 11 ++++++++--- test/loguniform.jl | 2 +- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/deprecates.jl b/src/deprecates.jl index 5c0cc11b1..9b36f4c60 100644 --- a/src/deprecates.jl +++ b/src/deprecates.jl @@ -47,6 +47,7 @@ end @deprecate Wishart(df::Real, S::Matrix, warn::Bool) Wishart(df, S) @deprecate Wishart(df::Real, S::Cholesky, warn::Bool) Wishart(df, S) -# Deprecate 3 arguments expectation -@deprecate expectation(distr::DiscreteUnivariateDistribution, g::Function, epsilon::Real) expectation(distr, g; epsilon=epsilon) false -@deprecate expectation(distr::ContinuousUnivariateDistribution, g::Function, epsilon::Real) expectation(distr, g) false +# Deprecate 3 arguments expectation and once with function in second place +@deprecate expectation(distr::DiscreteUnivariateDistribution, g::Function, epsilon::Real) expectation(g, distr; epsilon=epsilon) false +@deprecate expectation(distr::ContinuousUnivariateDistribution, g::Function, epsilon::Real) expectation(g, distr) false +@deprecate expectation(distr::Union{UnivariateDistribution,MultivariateDistribution}, g::Function; kwargs...) expectation(g, distr; kwargs...) false diff --git a/src/functionals.jl b/src/functionals.jl index 57e72c88b..2fc2ee47a 100644 --- a/src/functionals.jl +++ b/src/functionals.jl @@ -1,9 +1,9 @@ -function expectation(distr::ContinuousUnivariateDistribution, g::Function; kwargs...) +function expectation(g, distr::ContinuousUnivariateDistribution; kwargs...) return first(quadgk(x -> pdf(distr, x) * g(x), extrema(distr)...; kwargs...)) end ## Assuming that discrete distributions only take integer values. -function expectation(distr::DiscreteUnivariateDistribution, g::Function; epsilon::Real=1e-10) +function expectation(g, distr::DiscreteUnivariateDistribution; epsilon::Real=1e-10) mindist, maxdist = extrema(distr) # We want to avoid taking values up to infinity minval = isfinite(mindist) ? mindist : quantile(distr, epsilon) @@ -11,7 +11,7 @@ function expectation(distr::DiscreteUnivariateDistribution, g::Function; epsilon return sum(x -> pdf(distr, x) * g(x), minval:maxval) end -function expectation(distr::MultivariateDistribution, g::Function; nsamples::Int=100, rng::AbstractRNG=GLOBAL_RNG) +function expectation(g, distr::MultivariateDistribution; nsamples::Int=100, rng::AbstractRNG=GLOBAL_RNG) nsamples > 0 || throw(ArgumentError("number of samples should be > 0")) # We use a function barrier to work around type instability of `sampler(dist)` return mcexpectation(rng, g, sampler(distr), nsamples) @@ -27,9 +27,8 @@ mcexpectation(rng, f, sampler, n) = sum(f, rand(rng, sampler) for _ in 1:n) / n # end function kldivergence(P::Distribution{V}, Q::Distribution{V}; kwargs...) where {V<:VariateForm} - function logdiff(x) + return expectation(P; kwargs...) do x logp = logpdf(P, x) return (logp > oftype(logp, -Inf)) * (logp - logpdf(Q, x)) end - expectation(P, logdiff; kwargs...) -end \ No newline at end of file +end diff --git a/test/binomial.jl b/test/binomial.jl index 10dedb8ce..9fd3251f2 100644 --- a/test/binomial.jl +++ b/test/binomial.jl @@ -23,8 +23,8 @@ for (p, n) in [(0.6, 10), (0.8, 6), (0.5, 40), (0.04, 20), (1., 100), (0., 10), end # Test calculation of expectation value for Binomial distribution -@test Distributions.expectation(Binomial(6), identity) ≈ 3.0 -@test Distributions.expectation(Binomial(10, 0.2), x->-x) ≈ -2.0 +@test Distributions.expectation(identity, Binomial(6)) ≈ 3.0 +@test Distributions.expectation(x -> -x, Binomial(10, 0.2)) ≈ -2.0 # Test mode @test Distributions.mode(Binomial(100, 0.4)) == 40 diff --git a/test/functionals.jl b/test/functionals.jl index 9c8007ad3..16cbadee3 100644 --- a/test/functionals.jl +++ b/test/functionals.jl @@ -24,13 +24,18 @@ end @testset "Expectations" begin # univariate distributions for d in (Normal(), Poisson(2.0), Binomial(10, 0.4)) - @test Distributions.expectation(d, identity) ≈ mean(d) atol=1e-3 - @test @test_deprecated(Distributions.expectation(d, identity, 1e-10)) ≈ mean(d) atol=1e-3 + m = Distributions.expectation(identity, d) + @test m ≈ mean(d) atol=1e-3 + @test Distributions.expectation(x -> (x - mean(d))^2, d) ≈ var(d) atol=1e-3 + + @test @test_deprecated(Distributions.expectation(d, identity, 1e-10)) == m + @test @test_deprecated(Distributions.expectation(d, identity)) == m end # multivariate distribution d = MvNormal([1.5, -0.5], I) - @test Distributions.expectation(d, identity; nsamples=10_000) ≈ mean(d) atol=1e-2 + @test Distributions.expectation(identity, d; nsamples=10_000) ≈ mean(d) atol=5e-2 + @test @test_deprecated(Distributions.expectation(d, identity; nsamples=10_000)) ≈ mean(d) atol=5e-2 end @testset "KL divergences" begin diff --git a/test/loguniform.jl b/test/loguniform.jl index 649c147b9..11975780a 100644 --- a/test/loguniform.jl +++ b/test/loguniform.jl @@ -59,7 +59,7 @@ import Random x = rand(rng, dist) @test cdf(u, log(x)) ≈ cdf(dist, x) - @test @inferred(entropy(dist)) ≈ Distributions.expectation(dist, x->-logpdf(dist,x)) + @test @inferred(entropy(dist)) ≈ Distributions.expectation(x->-logpdf(dist,x), dist) end @test kldivergence(LogUniform(1,2), LogUniform(1,2)) ≈ 0 atol=100eps(Float64) From b2a27d6be3a6af95145de8f6124494c81c278065 Mon Sep 17 00:00:00 2001 From: Oliver Schulz Date: Tue, 16 Nov 2021 22:10:36 +0100 Subject: [PATCH 42/58] Support DensityInterface v0.4 (#1427) * Support DensityInterface v0.4 * Drop support for DensityInterface v0.3 * Increase package version to 0.25.30 * HasDensity needs to be prefixed with DensityInterface Co-authored-by: David Widmann Co-authored-by: David Widmann --- Project.toml | 4 ++-- docs/src/density_interface.md | 2 ++ src/density_interface.jl | 2 +- test/density_interface.jl | 3 --- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 76fc50457..7a809f39c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.29" +version = "0.25.30" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" @@ -21,7 +21,7 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [compat] ChainRulesCore = "1" -DensityInterface = "0.3.2" +DensityInterface = "0.4" FillArrays = "0.9, 0.10, 0.11, 0.12" PDMats = "0.10, 0.11" QuadGK = "2" diff --git a/docs/src/density_interface.md b/docs/src/density_interface.md index 5c01357df..1663a4368 100644 --- a/docs/src/density_interface.md +++ b/docs/src/density_interface.md @@ -2,4 +2,6 @@ `Distributions` supports [`DensityInterface`](https://github.com/JuliaMath/DensityInterface.jl) for distributions. +A probability distribution has a probability density, so `DensityInterface.DensityKind(::Distribution) === HasDensity()`. + For *single* variate values `x`, `DensityInterface.logdensityof(d::Distribution, x)` is equivalent to `logpdf(d, x)` and `DensityInterface.densityof(d::Distribution, x)` is equivalent to `pdf(d, x)`. diff --git a/src/density_interface.jl b/src/density_interface.jl index 3049a6903..f9a30aeed 100644 --- a/src/density_interface.jl +++ b/src/density_interface.jl @@ -1,4 +1,4 @@ -@inline DensityInterface.hasdensity(::Distribution) = true +@inline DensityInterface.DensityKind(::Distribution) = DensityInterface.HasDensity() for (di_func, d_func) in ((:logdensityof, :logpdf), (:densityof, :pdf)) @eval begin diff --git a/test/density_interface.jl b/test/density_interface.jl index e4988986d..4a9a00614 100644 --- a/test/density_interface.jl +++ b/test/density_interface.jl @@ -11,9 +11,6 @@ x = rand(d) ref_logd_at_x = logpdf(d, x) DensityInterface.test_density_interface(d, x, ref_logd_at_x) - - # Stricter than required by test_density_interface: - @test logfuncdensity(logdensityof(d)) === d end for di_func in (logdensityof, densityof) From f74f79a6b1f1d24d17bbb460aed725c81b53b2e5 Mon Sep 17 00:00:00 2001 From: Siddhartha Bagaria Date: Wed, 17 Nov 2021 01:51:45 -0800 Subject: [PATCH 43/58] Fix precondition for dirichlet_mode (#1426) --- src/multivariate/dirichlet.jl | 3 ++- test/dirichlet.jl | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/multivariate/dirichlet.jl b/src/multivariate/dirichlet.jl index 28f5dd3cd..afac39e5d 100644 --- a/src/multivariate/dirichlet.jl +++ b/src/multivariate/dirichlet.jl @@ -111,6 +111,7 @@ function entropy(d::Dirichlet) end function dirichlet_mode!(r::AbstractVector{<:Real}, α::AbstractVector{<:Real}, α0::Real) + all(x -> x > 1, α) || error("Dirichlet has a mode only when alpha[i] > 1 for all i") k = length(α) inv_s = inv(α0 - k) @. r = inv_s * (α - 1) @@ -118,7 +119,7 @@ function dirichlet_mode!(r::AbstractVector{<:Real}, α::AbstractVector{<:Real}, end function dirichlet_mode(α::AbstractVector{<:Real}, α0::Real) - all(αi < 1 for αi in α) || error("Dirichlet has a mode only when alpha[i] > 1 for all i") + all(x -> x > 1, α) || error("Dirichlet has a mode only when alpha[i] > 1 for all i") inv_s = inv(α0 - length(α)) r = map(α) do αi inv_s * (αi - 1) diff --git a/test/dirichlet.jl b/test/dirichlet.jl index d8b70a6db..1b3a18b52 100644 --- a/test/dirichlet.jl +++ b/test/dirichlet.jl @@ -21,9 +21,14 @@ rng = MersenneTwister(123) @test d.alpha0 == 6 @test mean(d) ≈ fill(1/3, 3) + @test mode(d) ≈ fill(1/3, 3) @test cov(d) ≈ [8 -4 -4; -4 8 -4; -4 -4 8] / (36 * 7) @test var(d) ≈ diag(cov(d)) + r = Vector{Float64}(undef, 3) + Distributions.dirichlet_mode!(r, d.alpha, d.alpha0) + @test r ≈ fill(1/3, 3) + @test pdf(Dirichlet([1, 1]), [0, 1]) ≈ 1 @test pdf(Dirichlet([1f0, 1f0]), [0f0, 1f0]) ≈ 1 @test typeof(pdf(Dirichlet([1f0, 1f0]), [0f0, 1f0])) === Float32 From 31cc7d230ffb06511e71aa2375354f889f05b25a Mon Sep 17 00:00:00 2001 From: David Widmann Date: Wed, 17 Nov 2021 10:53:01 +0100 Subject: [PATCH 44/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 7a809f39c..192fbd6cb 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.30" +version = "0.25.31" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From b2aa8384ce565cc0c97d21753f8c3ce66410d960 Mon Sep 17 00:00:00 2001 From: Rik Huijzer Date: Thu, 18 Nov 2021 21:32:31 +0100 Subject: [PATCH 45/58] Make symbols in deprecation warnings more explicit (#1428) * Use fill instead of Fill * Update mvnormal.jl * Fix typo * Update deprecations in mvnormalcanon --- src/multivariate/mvnormal.jl | 6 +++--- src/multivariate/mvnormalcanon.jl | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/multivariate/mvnormal.jl b/src/multivariate/mvnormal.jl index cef84c17b..34f672b1a 100644 --- a/src/multivariate/mvnormal.jl +++ b/src/multivariate/mvnormal.jl @@ -217,10 +217,10 @@ Construct a multivariate normal distribution with zero mean and covariance matri MvNormal(Σ::AbstractMatrix{<:Real}) = MvNormal(Zeros{eltype(Σ)}(size(Σ, 1)), Σ) # deprecated constructors with standard deviations -Base.@deprecate MvNormal(μ::AbstractVector{<:Real}, σ::AbstractVector{<:Real}) MvNormal(μ, Diagonal(map(abs2, σ))) +Base.@deprecate MvNormal(μ::AbstractVector{<:Real}, σ::AbstractVector{<:Real}) MvNormal(μ, LinearAlgebra.Diagonal(map(abs2, σ))) Base.@deprecate MvNormal(μ::AbstractVector{<:Real}, σ::Real) MvNormal(μ, σ^2 * I) -Base.@deprecate MvNormal(σ::AbstractVector{<:Real}) MvNormal(Diagonal(map(abs2, σ))) -Base.@deprecate MvNormal(d::Int, σ::Real) MvNormal(Diagonal(Fill(σ^2, d))) +Base.@deprecate MvNormal(σ::AbstractVector{<:Real}) MvNormal(LinearAlgebra.Diagonal(map(abs2, σ))) +Base.@deprecate MvNormal(d::Int, σ::Real) MvNormal(LinearAlgebra.Diagonal(FillArrays.Fill(σ^2, d))) Base.eltype(::Type{<:MvNormal{T}}) where {T} = T diff --git a/src/multivariate/mvnormalcanon.jl b/src/multivariate/mvnormalcanon.jl index 91c9596d3..3dd2430b4 100644 --- a/src/multivariate/mvnormalcanon.jl +++ b/src/multivariate/mvnormalcanon.jl @@ -110,10 +110,10 @@ precision matrix `J`. MvNormalCanon(J::AbstractMatrix{<:Real}) = MvNormalCanon(Zeros{eltype(J)}(size(J, 1)), J) # Deprecated constructors -Base.@deprecate MvNormalCanon(h::AbstractVector{<:Real}, prec::AbstractVector{<:Real}) MvNormalCanon(h, Diagonal(prec)) +Base.@deprecate MvNormalCanon(h::AbstractVector{<:Real}, prec::AbstractVector{<:Real}) MvNormalCanon(h, LinearAlgebra.Diagonal(prec)) Base.@deprecate MvNormalCanon(h::AbstractVector{<:Real}, prec::Real) MvNormalCanon(h, prec * I) -Base.@deprecate MvNormalCanon(prec::AbstractVector) MvNormalCanon(Diagonal(prec)) -Base.@deprecate MvNormalCanon(d::Int, prec::Real) MvNormalCanon(Diagonal(Fill(prec, d))) +Base.@deprecate MvNormalCanon(prec::AbstractVector) MvNormalCanon(LinearAlgebra.Diagonal(prec)) +Base.@deprecate MvNormalCanon(d::Int, prec::Real) MvNormalCanon(LinearAlgebra.Diagonal(FillArrays.Fill(prec, d))) ### Show From 9f494537197b41a383996a20d32bfe336d07068f Mon Sep 17 00:00:00 2001 From: Alexander Date: Sun, 21 Nov 2021 20:44:20 +0300 Subject: [PATCH 46/58] Rayleigh cdf always nonnegative (#1430) --- src/univariate/continuous/rayleigh.jl | 3 ++- test/continuous.jl | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/univariate/continuous/rayleigh.jl b/src/univariate/continuous/rayleigh.jl index 0b93dcdc5..541e11d3c 100644 --- a/src/univariate/continuous/rayleigh.jl +++ b/src/univariate/continuous/rayleigh.jl @@ -79,7 +79,8 @@ end function logccdf(d::Rayleigh, x::Real) z = - x^2 / (2 * d.σ^2) - return x > 0 ? z : isnan(x) ? oftype(z, NaN) : zero(x) + # return negative zero so that cdf is +0, not -0 + return x > 0 ? z : isnan(x) ? oftype(z, NaN) : -zero(z) end ccdf(d::Rayleigh, x::Real) = exp(logccdf(d, x)) diff --git a/test/continuous.jl b/test/continuous.jl index bddcf4bbd..3a1da94b9 100644 --- a/test/continuous.jl +++ b/test/continuous.jl @@ -98,3 +98,9 @@ end @test var(d) ≈ δ * α^2 / g^3 @test skewness(d) ≈ 3β/(α*sqrt(δ*g)) end + +@testset "edge cases" begin + # issue #1371: cdf should not return -0.0 + @test cdf(Rayleigh(1), 0) === 0.0 + @test cdf(Rayleigh(1), -10) === 0.0 +end From 135f7646eae6b3ff6f266bd520b2a8d4dff77bbc Mon Sep 17 00:00:00 2001 From: David Widmann Date: Tue, 23 Nov 2021 17:50:24 +0100 Subject: [PATCH 47/58] Fix Wishart and MvNormal bugs (#1429) * Fix Wishart bugs * Update src/matrix/wishart.jl * Fix `MvNormal` --- Project.toml | 2 +- src/matrix/wishart.jl | 88 ++++++++++++++++++++---------------- src/multivariate/mvnormal.jl | 8 ++-- test/mvnormal.jl | 5 ++ 4 files changed, 59 insertions(+), 44 deletions(-) diff --git a/Project.toml b/Project.toml index 192fbd6cb..35364b0b5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.31" +version = "0.25.32" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/src/matrix/wishart.jl b/src/matrix/wishart.jl index efd0199f8..b459f979c 100644 --- a/src/matrix/wishart.jl +++ b/src/matrix/wishart.jl @@ -69,7 +69,7 @@ Wishart(df::Real, S::Cholesky) = Wishart(df, PDMat(S)) # REPL display # ----------------------------------------------------------------------------- -show(io::IO, d::Wishart) = show_multline(io, d, [(:df, d.df), (:S, Matrix(d.S))]) +show(io::IO, d::Wishart) = show_multline(io, d, [(:df, d.df), (:S, d.S)]) # ----------------------------------------------------------------------------- # Conversion @@ -107,45 +107,45 @@ params(d::Wishart) = (d.df, d.S) mean(d::Wishart) = d.df * Matrix(d.S) function mode(d::Wishart) - r = d.df - dim(d) - 1.0 - r > 0.0 || throw(ArgumentError("mode is only defined when df > p + 1")) + r = d.df - dim(d) - 1 + r > 0 || throw(ArgumentError("mode is only defined when df > p + 1")) return Matrix(d.S) * r end function meanlogdet(d::Wishart) - d.singular && return -Inf + logdet_S = logdet(d.S) p = dim(d) - df = d.df - v = logdet(d.S) + p * logtwo - for i = 1:p - v += digamma(0.5 * (df - (i - 1))) + v = logdet_S + p * oftype(logdet_S, logtwo) + df = oftype(logdet_S, d.df) + for i in 0:(p - 1) + v += digamma((df - i) / 2) end - return v + return d.singular ? oftype(v, -Inf) : v end function entropy(d::Wishart) d.singular && throw(ArgumentError("entropy not defined for singular Wishart.")) p = dim(d) df = d.df - -d.logc0 - 0.5 * (df - p - 1) * meanlogdet(d) + 0.5 * df * p + return -d.logc0 - ((df - p - 1) * meanlogdet(d) - df * p) / 2 end # Gupta/Nagar (1999) Theorem 3.3.15.i function cov(d::Wishart, i::Integer, j::Integer, k::Integer, l::Integer) - S = Matrix(d.S) - d.df * (S[i, k] * S[j, l] + S[i, l] * S[j, k]) + S = d.S + return d.df * (S[i, k] * S[j, l] + S[i, l] * S[j, k]) end function var(d::Wishart, i::Integer, j::Integer) - S = Matrix(d.S) - d.df * (S[i, i] * S[j, j] + S[i, j] ^ 2) + S = d.S + return d.df * (S[i, i] * S[j, j] + S[i, j] ^ 2) end # ----------------------------------------------------------------------------- # Evaluation # ----------------------------------------------------------------------------- -function wishart_logc0(df::Real, S::AbstractPDMat, rnk::Integer) +function wishart_logc0(df::T, S::AbstractPDMat{T}, rnk::Integer) where {T<:Real} p = dim(S) if df <= p - 1 return singular_wishart_logc0(p, df, S, rnk) @@ -163,30 +163,28 @@ function logkernel(d::Wishart, X::AbstractMatrix) end # Singular Wishart pdf: Theorem 6 in Uhlig (1994 AoS) -function singular_wishart_logc0(p::Integer, df::Real, S::AbstractPDMat, rnk::Integer) - h_df = df / 2 - -h_df * (logdet(S) + p * typeof(df)(logtwo)) - logmvgamma(rnk, h_df) + (rnk*(rnk - p) / 2)*typeof(df)(logπ) +function singular_wishart_logc0(p::Integer, df::T, S::AbstractPDMat{T}, rnk::Integer) where {T<:Real} + logdet_S = logdet(S) + h_df = oftype(logdet_S, df) / 2 + return -h_df * (logdet_S + p * oftype(logdet_S, logtwo)) - logmvgamma(rnk, h_df) + ((rnk * (rnk - p)) // 2) * oftype(logdet_S, logπ) end function singular_wishart_logkernel(d::Wishart, X::AbstractMatrix) - df = d.df p = dim(d) r = rank(d) L = eigvals(Hermitian(X), (p - r + 1):p) - 0.5 * ((df - (p + 1)) * sum(log.(L)) - tr(d.S \ X)) + return ((d.df - (p + 1)) * sum(log, L) - tr(d.S \ X)) / 2 end # Nonsingular Wishart pdf -function nonsingular_wishart_logc0(p::Integer, df::Real, S::AbstractPDMat) - h_df = df / 2 - -h_df * (logdet(S) + p * typeof(df)(logtwo)) - logmvgamma(p, h_df) +function nonsingular_wishart_logc0(p::Integer, df::T, S::AbstractPDMat{T}) where {T<:Real} + logdet_S = logdet(S) + h_df = oftype(logdet_S, df) / 2 + return -h_df * (logdet_S + p * oftype(logdet_S, logtwo)) - logmvgamma(p, h_df) end function nonsingular_wishart_logkernel(d::Wishart, X::AbstractMatrix) - df = d.df - p = dim(d) - Xcf = cholesky(X) - 0.5 * ((df - (p + 1)) * logdet(Xcf) - tr(d.S \ X)) + return ((d.df - (dim(d) + 1)) * logdet(cholesky(X)) - tr(d.S \ X)) / 2 end # ----------------------------------------------------------------------------- @@ -195,16 +193,18 @@ end function _rand!(rng::AbstractRNG, d::Wishart, A::AbstractMatrix) if d.singular - A .= zero(eltype(A)) - A[:, 1:rank(d)] = randn(rng, dim(d), rank(d)) + axes2 = axes(A, 2) + r = rank(d) + randn!(rng, view(A, :, axes2[1:r])) + fill!(view(A, :, axes2[(r + 1):end]), zero(eltype(A))) else - _wishart_genA!(rng, dim(d), d.df, A) + _wishart_genA!(rng, A, d.df) end unwhiten!(d.S, A) A .= A * A' end -function _wishart_genA!(rng::AbstractRNG, p::Int, df::Real, A::AbstractMatrix) +function _wishart_genA!(rng::AbstractRNG, A::AbstractMatrix, df::Real) # Generate the matrix A in the Bartlett decomposition # # A is a lower triangular matrix, with @@ -212,13 +212,19 @@ function _wishart_genA!(rng::AbstractRNG, p::Int, df::Real, A::AbstractMatrix) # A(i, j) ~ sqrt of Chisq(df - i + 1) when i == j # ~ Normal() when i > j # - A .= zero(eltype(A)) - for i = 1:p - @inbounds A[i,i] = rand(rng, Chi(df - i + 1.0)) - end - for j in 1:p-1, i in j+1:p - @inbounds A[i,j] = randn(rng) + T = eltype(A) + z = zero(T) + axes1 = axes(A, 1) + @inbounds for (j, jdx) in enumerate(axes(A, 2)), (i, idx) in enumerate(axes1) + A[idx, jdx] = if i < j + z + elseif i > j + randn(rng, T) + else + rand(rng, Chi(df - i + 1)) + end end + return A end # ----------------------------------------------------------------------------- @@ -229,13 +235,15 @@ function _univariate(d::Wishart) check_univariate(d) df, S = params(d) α = df / 2 - β = 2Matrix(S)[1] + β = 2 * first(S) return Gamma(α, β) end function _rand_params(::Type{Wishart}, elty, n::Int, p::Int) n == p || throw(ArgumentError("dims must be equal for Wishart")) - ν = elty( n - 1 + abs(10randn()) ) - S = (X = 2rand(elty, n, n) .- 1; X * X') + ν = elty(n - 1 + abs(10 * randn())) + X = rand(elty, n, n) + X .= 2 .* X .- 1 + S = X * X' return ν, S end diff --git a/src/multivariate/mvnormal.jl b/src/multivariate/mvnormal.jl index 34f672b1a..c72f0e82c 100644 --- a/src/multivariate/mvnormal.jl +++ b/src/multivariate/mvnormal.jl @@ -94,11 +94,13 @@ rand(::AbstractRNG, ::Distributions.AbstractMvNormal) function entropy(d::AbstractMvNormal) ldcd = logdetcov(d) - T = typeof(ldcd) - (length(d) * (T(log2π) + one(T)) + ldcd)/2 + return (length(d) * (oftype(ldcd, log2π) + 1) + ldcd) / 2 end -mvnormal_c0(g::AbstractMvNormal) = -(length(g) * convert(eltype(g), log2π) + logdetcov(g))/2 +function mvnormal_c0(d::AbstractMvNormal) + ldcd = logdetcov(d) + return - (length(d) * oftype(ldcd, log2π) + ldcd) / 2 +end function kldivergence(p::AbstractMvNormal, q::AbstractMvNormal) # This is the generic implementation for AbstractMvNormal, you might need to specialize for your type diff --git a/test/mvnormal.jl b/test/mvnormal.jl index d5f99ab90..0020e82c4 100644 --- a/test/mvnormal.jl +++ b/test/mvnormal.jl @@ -284,4 +284,9 @@ end @test rand(d) isa Vector{Float64} @test rand(d, 10) isa Matrix{Float64} @test rand(d, (3, 2)) isa Matrix{Vector{Float64}} + + # evaluation of `logpdf` + # (bug fixed by https://github.com/JuliaStats/Distributions.jl/pull/1429) + x = rand(d) + @test logpdf(d, x) ≈ logpdf(Normal(), x[1]) + logpdf(Normal(), x[2]) end From 337529f688e236b9e4d5d9451b0e3149280f931a Mon Sep 17 00:00:00 2001 From: David Widmann Date: Thu, 25 Nov 2021 18:24:42 +0100 Subject: [PATCH 48/58] Add integration tests (#1439) * Add integration tests * Update .github/workflows/IntegrationTest.yml --- .github/workflows/{ci.yml => CI.yml} | 8 ++++ .github/workflows/IntegrationTest.yml | 61 +++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) rename .github/workflows/{ci.yml => CI.yml} (89%) create mode 100644 .github/workflows/IntegrationTest.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/CI.yml similarity index 89% rename from .github/workflows/ci.yml rename to .github/workflows/CI.yml index 69a044571..9d478dc99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/CI.yml @@ -1,4 +1,5 @@ name: CI + on: pull_request: branches: @@ -7,6 +8,13 @@ on: branches: - master tags: '*' + +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: only if it is a pull request build. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} diff --git a/.github/workflows/IntegrationTest.yml b/.github/workflows/IntegrationTest.yml new file mode 100644 index 000000000..fc315aa58 --- /dev/null +++ b/.github/workflows/IntegrationTest.yml @@ -0,0 +1,61 @@ +name: IntegrationTest + +on: + pull_request: + branches: + - master + push: + branches: + - master + +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: only if it is a pull request build. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} + +jobs: + test: + name: ${{ matrix.package.repo }}/${{ matrix.package.group }} + runs-on: ubuntu-latest + env: + GROUP: ${{ matrix.package.group }} + strategy: + fail-fast: false + matrix: + package: + - {user: TuringLang, repo: DistributionsAD.jl, group: Others} + - {user: TuringLang, repo: DistributionsAD.jl, group: Tracker} + - {user: TuringLang, repo: DistributionsAD.jl, group: ReverseDiff} + - {user: TuringLang, repo: DistributionsAD.jl, group: Zygote} + #- {user: TuringLang, repo: DistributionsAD.jl, group: ForwardDiff} takes > 1 hour + + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: 1 + arch: x64 + - uses: julia-actions/julia-buildpkg@latest + - name: Clone Downstream + uses: actions/checkout@v2 + with: + repository: ${{ matrix.package.user }}/${{ matrix.package.repo }} + path: downstream + - name: Load this and run the downstream tests + shell: julia --color=yes --project=downstream {0} + run: | + using Pkg + try + # force it to use this PR's version of the package + Pkg.develop(PackageSpec(path=".")) # resolver may fail with main deps + Pkg.update() + Pkg.test() # resolver may fail with test time deps + catch err + err isa Pkg.Resolve.ResolverError || rethrow() + # If we can't resolve that means this is incompatible by SemVer and this is fine + # It means we marked this as a breaking change, so we don't need to worry about + # Mistakenly introducing a breaking change, as we have intentionally made one + @info "Not compatible with this release. No problem." exception=err + exit(0) # Exit immediately, as a success + end From d6985ad5f08990123b9ba84431626f22f1a408bd Mon Sep 17 00:00:00 2001 From: David Widmann Date: Sat, 27 Nov 2021 01:03:36 +0100 Subject: [PATCH 49/58] Generalize (log)pdf, rand(!), and MatrixReshaped (#1437) * Generalize (log)pdf, rand(!), and MatrixReshaped * Relax checks of `logpdf!` and `pdf!` * Add shortcuts for `convert(::Type{<:Multinomial}, ...)` * Add `loglikelihood` definitions * Update documentation --- Project.toml | 2 +- docs/make.jl | 1 + docs/src/matrix.md | 8 +- docs/src/reshape.md | 9 + src/Distributions.jl | 4 +- src/common.jl | 278 +++++++++++++++++++++++++++ src/deprecates.jl | 6 + src/eachvariate.jl | 32 +++ src/genericrand.jl | 140 +++++++++++++- src/matrix/matrixnormal.jl | 8 +- src/matrix/matrixreshaped.jl | 75 -------- src/matrix/matrixtdist.jl | 2 +- src/matrixvariates.jl | 163 +--------------- src/mixtures/mixturemodel.jl | 11 +- src/multivariate/multinomial.jl | 8 +- src/multivariate/mvnormal.jl | 8 +- src/multivariate/mvtdist.jl | 2 - src/multivariates.jl | 171 +--------------- src/reshaped.jl | 163 ++++++++++++++++ src/samplers/multinomial.jl | 56 +----- src/samplers/vonmisesfisher.jl | 14 +- src/univariate/continuous/gumbel.jl | 2 +- src/univariate/continuous/uniform.jl | 2 +- src/univariates.jl | 39 ++-- test/matrixreshaped.jl | 143 ++++---------- test/reshaped.jl | 151 +++++++++++++++ test/runtests.jl | 1 + 27 files changed, 866 insertions(+), 633 deletions(-) create mode 100644 docs/src/reshape.md create mode 100644 src/eachvariate.jl delete mode 100644 src/matrix/matrixreshaped.jl create mode 100644 src/reshaped.jl create mode 100644 test/reshaped.jl diff --git a/Project.toml b/Project.toml index 35364b0b5..701b05f43 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.32" +version = "0.25.33" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/docs/make.jl b/docs/make.jl index a92bc4679..b451eecd2 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -13,6 +13,7 @@ makedocs( "truncate.md", "multivariate.md", "matrix.md", + "reshape.md", "cholesky.md", "mixture.md", "fit.md", diff --git a/docs/src/matrix.md b/docs/src/matrix.md index 5b832b778..6d352f19a 100644 --- a/docs/src/matrix.md +++ b/docs/src/matrix.md @@ -23,10 +23,9 @@ Distributions.rank(::MatrixDistribution) mean(::MatrixDistribution) var(::MatrixDistribution) cov(::MatrixDistribution) -pdf{T<:Real}(d::MatrixDistribution, x::AbstractMatrix{T}) -logpdf{T<:Real}(d::MatrixDistribution, x::AbstractMatrix{T}) +pdf(d::MatrixDistribution, x::AbstractMatrix{<:Real}) +logpdf(d::MatrixDistribution, x::AbstractMatrix{<:Real}) Distributions._rand!(::AbstractRNG, ::MatrixDistribution, A::AbstractMatrix) -vec(d::MatrixDistribution) ``` ## Distributions @@ -35,7 +34,6 @@ vec(d::MatrixDistribution) MatrixNormal Wishart InverseWishart -MatrixReshaped MatrixTDist MatrixBeta MatrixFDist @@ -45,7 +43,7 @@ LKJ ## Internal Methods (for creating your own matrix-variate distributions) ```@docs -Distributions._logpdf(d::MatrixDistribution, x::AbstractArray) +Distributions._logpdf(d::MatrixDistribution, x::AbstractMatrix{<:Real}) ``` ## Index diff --git a/docs/src/reshape.md b/docs/src/reshape.md new file mode 100644 index 000000000..d381d74fc --- /dev/null +++ b/docs/src/reshape.md @@ -0,0 +1,9 @@ +# Reshaping distributions + +Distributions of array variates such as `MultivariateDistribution`s and +`MatrixDistribution`s can be reshaped. + +```@docs +reshape +vec +``` diff --git a/src/Distributions.jl b/src/Distributions.jl index 52c19f770..798fbe526 100644 --- a/src/Distributions.jl +++ b/src/Distributions.jl @@ -274,6 +274,7 @@ include("common.jl") # implementation helpers include("utils.jl") +include("eachvariate.jl") # generic functions include("show.jl") @@ -291,6 +292,7 @@ include("cholesky/lkjcholesky.jl") include("samplers.jl") # others +include("reshaped.jl") include("truncate.jl") include("conversion.jl") include("convolution.jl") @@ -340,7 +342,7 @@ Supported distributions: InverseWishart, InverseGamma, InverseGaussian, IsoNormal, IsoNormalCanon, Kolmogorov, KSDist, KSOneSided, Laplace, Levy, LKJ, LKJCholesky, Logistic, LogNormal, MatrixBeta, MatrixFDist, MatrixNormal, - MatrixReshaped, MatrixTDist, MixtureModel, Multinomial, + MatrixTDist, MixtureModel, Multinomial, MultivariateNormal, MvLogNormal, MvNormal, MvNormalCanon, MvNormalKnownCov, MvTDist, NegativeBinomial, NoncentralBeta, NoncentralChisq, NoncentralF, NoncentralHypergeometric, NoncentralT, Normal, NormalCanon, diff --git a/src/common.jl b/src/common.jl index 243e1a591..d677ee256 100644 --- a/src/common.jl +++ b/src/common.jl @@ -42,6 +42,10 @@ Any `Sampleable` implements the `Base.rand` method. """ abstract type Sampleable{F<:VariateForm,S<:ValueSupport} end + +variate_form(::Type{<:Sampleable{VF}}) where {VF} = VF +value_support(::Type{<:Sampleable{<:VariateForm,VS}}) where {VS} = VS + """ length(s::Sampleable) @@ -165,6 +169,280 @@ Return the minimum and maximum of the support of `d` as a 2-tuple. """ Base.extrema(d::Distribution) = minimum(d), maximum(d) +""" + pdf(d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:Real,N}) where {N} + +Evaluate the probability density function of `d` at `x`. + +This function checks if the size of `x` is compatible with distribution `d`. This check can +be disabled by using `@inbounds`. + +# Implementation + +Instead of `pdf` one should implement `_pdf(d, x)` which does not have to check the size of +`x`. However, since the default definition of `pdf(d, x)` falls back to `logpdf(d, x)` +usually it is sufficient to implement `logpdf`. + +See also: [`logpdf`](@ref). +""" +@inline function pdf( + d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:Real,N} +) where {N} + @boundscheck begin + size(x) == size(d) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + return _pdf(d, x) +end + +function _pdf(d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:Real,N}) where {N} + return exp(@inbounds logpdf(d, x)) +end + +""" + logpdf(d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:Real,N}) where {N} + +Evaluate the probability density function of `d` at `x`. + +This function checks if the size of `x` is compatible with distribution `d`. This check can +be disabled by using `@inbounds`. + +# Implementation + +Instead of `logpdf` one should implement `_logpdf(d, x)` which does not have to check the +size of `x`. + +See also: [`pdf`](@ref). +""" +@inline function logpdf( + d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:Real,N} +) where {N} + @boundscheck begin + size(x) == size(d) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + return _logpdf(d, x) +end + +# `_logpdf` should be implemented and has no default definition +# _logpdf(d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:Real,N}) where {N} + +# TODO: deprecate? +""" + pdf(d::Distribution{ArrayLikeVariate{N}}, x) where {N} + +Evaluate the probability density function of `d` at every element in a collection `x`. + +This function checks for every element of `x` if its size is compatible with distribution +`d`. This check can be disabled by using `@inbounds`. + +Here, `x` can be +- an array of dimension `> N` with `size(x)[1:N] == size(d)`, or +- an array of arrays `xi` of dimension `N` with `size(xi) == size(d)`. +""" +Base.@propagate_inbounds function pdf( + d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:AbstractArray{<:Real,N}}, +) where {N} + return map(Base.Fix1(pdf, d), x) +end + +@inline function pdf( + d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:Real,M}, +) where {N,M} + @boundscheck begin + M > N || + throw(DimensionMismatch( + "number of dimensions of `x` ($M) must be greater than number of dimensions of `d` ($N)" + )) + ntuple(i -> size(x, i), Val(N)) == size(d) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + return @inbounds map(Base.Fix1(pdf, d), eachvariate(x, variate_form(typeof(d)))) +end + +""" + logpdf(d::Distribution{ArrayLikeVariate{N}}, x) where {N} + +Evaluate the logarithm of the probability density function of `d` at every element in a +collection `x`. + +This function checks for every element of `x` if its size is compatible with distribution +`d`. This check can be disabled by using `@inbounds`. + +Here, `x` can be +- an array of dimension `> N` with `size(x)[1:N] == size(d)`, or +- an array of arrays `xi` of dimension `N` with `size(xi) == size(d)`. +""" +Base.@propagate_inbounds function logpdf( + d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:AbstractArray{<:Real,N}}, +) where {N} + return map(Base.Fix1(logpdf, d), x) +end + +@inline function logpdf( + d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:Real,M}, +) where {N,M} + @boundscheck begin + M > N || + throw(DimensionMismatch( + "number of dimensions of `x` ($M) must be greater than number of dimensions of `d` ($N)" + )) + ntuple(i -> size(x, i), Val(N)) == size(d) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + return @inbounds map(Base.Fix1(logpdf, d), eachvariate(x, variate_form(typeof(d)))) +end + +""" + pdf!(out, d::Distribution{ArrayLikeVariate{N}}, x) where {N} + +Evaluate the probability density function of `d` at every element in a collection `x` and +save the results in `out`. + +This function checks if the size of `out` is compatible with `d` and `x` and for every +element of `x` if its size is compatible with distribution `d`. These checks can be disabled +by using `@inbounds`. + +Here, `x` can be +- an array of dimension `> N` with `size(x)[1:N] == size(d)`, or +- an array of arrays `xi` of dimension `N` with `size(xi) == size(d)`. + +# Implementation + +Instead of `pdf!` one should implement `_pdf!(out, d, x)` which does not have to check the +size of `out` and `x`. However, since the default definition of `_pdf!(out, d, x)` falls +back to `logpdf!` usually it is sufficient to implement `logpdf!`. + +See also: [`logpdf!`](@ref). +""" +Base.@propagate_inbounds function pdf!( + out::AbstractArray{<:Real}, + d::Distribution{ArrayLikeVariate{N}}, + x::AbstractArray{<:AbstractArray{<:Real,N},M} +) where {N,M} + return map!(Base.Fix1(pdf, d), out, x) +end + +Base.@propagate_inbounds function logpdf!( + out::AbstractArray{<:Real}, + d::Distribution{ArrayLikeVariate{N}}, + x::AbstractArray{<:AbstractArray{<:Real,N},M} +) where {N,M} + return map!(Base.Fix1(logpdf, d), out, x) +end + +@inline function pdf!( + out::AbstractArray{<:Real}, + d::Distribution{ArrayLikeVariate{N}}, + x::AbstractArray{<:Real,M}, +) where {N,M} + @boundscheck begin + M > N || + throw(DimensionMismatch( + "number of dimensions of `x` ($M) must be greater than number of dimensions of `d` ($N)" + )) + ntuple(i -> size(x, i), Val(N)) == size(d) || + throw(DimensionMismatch("inconsistent array dimensions")) + length(out) == prod(i -> size(x, i), (N + 1):M) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + return _pdf!(out, d, x) +end + +function _pdf!( + out::AbstractArray{<:Real}, + d::Distribution{<:ArrayLikeVariate}, + x::AbstractArray{<:Real}, +) + @inbounds logpdf!(out, d, x) + map!(exp, out, out) + return out +end + +""" + logpdf!(out, d::Distribution{ArrayLikeVariate{N}}, x) where {N} + +Evaluate the logarithm of the probability density function of `d` at every element in a +collection `x` and save the results in `out`. + +This function checks if the size of `out` is compatible with `d` and `x` and for every +element of `x` if its size is compatible with distribution `d`. These checks can be disabled +by using `@inbounds`. + +Here, `x` can be +- an array of dimension `> N` with `size(x)[1:N] == size(d)`, or +- an array of arrays `xi` of dimension `N` with `size(xi) == size(d)`. + +# Implementation + +Instead of `logpdf!` one should implement `_logpdf!(out, d, x)` which does not have to check +the size of `out` and `x`. + +See also: [`pdf!`](@ref). +""" +@inline function logpdf!( + out::AbstractArray{<:Real}, + d::Distribution{ArrayLikeVariate{N}}, + x::AbstractArray{<:Real,M}, +) where {N,M} + @boundscheck begin + M > N || + throw(DimensionMismatch( + "number of dimensions of `x` ($M) must be greater than number of dimensions of `d` ($N)" + )) + ntuple(i -> size(x, i), Val(N)) == size(d) || + throw(DimensionMismatch("inconsistent array dimensions")) + length(out) == prod(i -> size(x, i), (N + 1):M) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + return _logpdf!(out, d, x) +end + +# default definition +function _logpdf!( + out::AbstractArray{<:Real}, + d::Distribution{<:ArrayLikeVariate}, + x::AbstractArray{<:Real}, +) + @inbounds map!(Base.Fix1(logpdf, d), out, eachvariate(x, variate_form(typeof(d)))) + return out +end + +""" + loglikelihood(d::Distribution{ArrayLikeVariate{N}}, x) where {N} + +The log-likelihood of distribution `d` with respect to all variate(s) contained in `x`. + +Here, `x` can be any output of `rand(d, dims...)` and `rand!(d, x)`. For instance, `x` can +be +- an array of dimension `N` with `size(x) == size(d)`, +- an array of dimension `N + 1` with `size(x)[1:N] == size(d)`, or +- an array of arrays `xi` of dimension `N` with `size(xi) == size(d)`. +""" +Base.@propagate_inbounds function loglikelihood( + d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:Real,N}, +) where {N} + return logpdf(d, x) +end +@inline function loglikelihood( + d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:Real,M}, +) where {N,M} + @boundscheck begin + M > N || + throw(DimensionMismatch( + "number of dimensions of `x` ($M) must be greater than number of dimensions of `d` ($N)" + )) + ntuple(i -> size(x, i), Val(N)) == size(d) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + return @inbounds sum(Base.Fix1(logpdf, d), eachvariate(x, ArrayLikeVariate{N})) +end +Base.@propagate_inbounds function loglikelihood( + d::Distribution{ArrayLikeVariate{N}}, x::AbstractArray{<:AbstractArray{<:Real,N}}, +) where {N} + return sum(Base.Fix1(logpdf, d), x) +end + ## TODO: the following types need to be improved abstract type SufficientStats end abstract type IncompleteDistribution end diff --git a/src/deprecates.jl b/src/deprecates.jl index 9b36f4c60..cf4031b33 100644 --- a/src/deprecates.jl +++ b/src/deprecates.jl @@ -51,3 +51,9 @@ end @deprecate expectation(distr::DiscreteUnivariateDistribution, g::Function, epsilon::Real) expectation(g, distr; epsilon=epsilon) false @deprecate expectation(distr::ContinuousUnivariateDistribution, g::Function, epsilon::Real) expectation(g, distr) false @deprecate expectation(distr::Union{UnivariateDistribution,MultivariateDistribution}, g::Function; kwargs...) expectation(g, distr; kwargs...) false + +# Deprecate `MatrixReshaped` +const MatrixReshaped{S<:ValueSupport,D<:MultivariateDistribution{S}} = ReshapedDistribution{2,S,D} +@deprecate MatrixReshaped( + d::MultivariateDistribution, n::Integer, p::Integer=n +) reshape(d, (n, p)) diff --git a/src/eachvariate.jl b/src/eachvariate.jl new file mode 100644 index 000000000..701be99fa --- /dev/null +++ b/src/eachvariate.jl @@ -0,0 +1,32 @@ +## AbstractArray wrapper for collection of variates +## Similar to https://github.com/JuliaLang/julia/pull/32310 - replace with EachSlice? +struct EachVariate{V,P,A,T,N} <: AbstractArray{T,N} + parent::P + axes::A +end + +function EachVariate{V}(x::AbstractArray{<:Real,M}) where {V,M} + ax = ntuple(i -> axes(x, i + V), Val(M - V)) + T = typeof(view(x, ntuple(i -> i <= V ? Colon() : firstindex(x, i), Val(M))...)) + return EachVariate{V,typeof(x),typeof(ax),T,M-V}(x, ax) +end + +Base.IteratorSize(::Type{EachVariate{V,P,A,T,N}}) where {V,P,A,T,N} = Base.HasShape{N}() + +Base.axes(x::EachVariate) = x.axes + +Base.size(x::EachVariate) = map(length, x.axes) +Base.size(x::EachVariate, d::Int) = 1 <= ndims(x) ? length(axes(x)[d]) : 1 + +# We don't need `setindex!` (currently), therefore only `getindex` is implemented +Base.@propagate_inbounds function Base.getindex( + x::EachVariate{V,P,A,T,N}, I::Vararg{Int,N}, +) where {V,P,A,T,N} + return view(x.parent, ntuple(_ -> Colon(), Val(V))..., I...) +end + +# optimization for univariate distributions +eachvariate(x::AbstractArray{<:Real}, ::Type{Univariate}) = x +function eachvariate(x::AbstractArray{<:Real}, ::Type{ArrayLikeVariate{N}}) where {N} + return EachVariate{N}(x) +end diff --git a/src/genericrand.jl b/src/genericrand.jl index f54c2748e..58914e75d 100644 --- a/src/genericrand.jl +++ b/src/genericrand.jl @@ -19,13 +19,47 @@ Generate `n` samples from `s`. The form of the returned object depends on the va Generate an array of samples from `s` whose shape is determined by the given dimensions. """ -rand(s::Sampleable) = rand(GLOBAL_RNG, s) +rand(s::Sampleable, dims::Int...) = rand(GLOBAL_RNG, s, dims...) rand(s::Sampleable, dims::Dims) = rand(GLOBAL_RNG, s, dims) -rand(s::Sampleable, dim1::Int, moredims::Int...) = - rand(GLOBAL_RNG, s, (dim1, moredims...)) rand(rng::AbstractRNG, s::Sampleable, dim1::Int, moredims::Int...) = rand(rng, s, (dim1, moredims...)) +# default fallback (redefined for univariate distributions) +function rand(rng::AbstractRNG, s::Sampleable{<:ArrayLikeVariate}) + return @inbounds rand!(rng, s, Array{eltype(s)}(undef, size(s))) +end + +# multiple samples +function rand(rng::AbstractRNG, s::Sampleable{Univariate}, dims::Dims) + out = Array{eltype(s)}(undef, dims) + return @inbounds rand!(rng, sampler(s), out) +end +function rand( + rng::AbstractRNG, s::Sampleable{<:ArrayLikeVariate}, dims::Dims, +) + sz = size(s) + ax = map(Base.OneTo, dims) + out = [Array{eltype(s)}(undef, sz) for _ in Iterators.product(ax...)] + return @inbounds rand!(rng, sampler(s), out, false) +end + +# these are workarounds for sampleables that incorrectly base `eltype` on the parameters +function rand(rng::AbstractRNG, s::Sampleable{<:ArrayLikeVariate,Continuous}) + return @inbounds rand!(rng, sampler(s), Array{float(eltype(s))}(undef, size(s))) +end +function rand(rng::AbstractRNG, s::Sampleable{Univariate,Continuous}, dims::Dims) + out = Array{float(eltype(s))}(undef, dims) + return @inbounds rand!(rng, sampler(s), out) +end +function rand( + rng::AbstractRNG, s::Sampleable{<:ArrayLikeVariate,Continuous}, dims::Dims, +) + sz = size(s) + ax = map(Base.OneTo, dims) + out = [Array{float(eltype(s))}(undef, sz) for _ in Iterators.product(ax...)] + return @inbounds rand!(rng, sampler(s), out, false) +end + """ rand!([rng::AbstractRNG,] s::Sampleable, A::AbstractArray) @@ -40,10 +74,102 @@ form as specified above. The rules are summarized as below: matrices with each element for a sample matrix. """ function rand! end -rand!(s::Sampleable, X::AbstractArray{<:AbstractArray}, allocate::Bool) = - rand!(GLOBAL_RNG, s, X, allocate) -rand!(s::Sampleable, X::AbstractArray) = rand!(GLOBAL_RNG, s, X) -rand!(rng::AbstractRNG, s::Sampleable, X::AbstractArray) = _rand!(rng, s, X) +Base.@propagate_inbounds rand!(s::Sampleable, X::AbstractArray) = rand!(GLOBAL_RNG, s, X) +Base.@propagate_inbounds function rand!(rng::AbstractRNG, s::Sampleable, X::AbstractArray) + return _rand!(rng, s, X) +end + +# default definitions for arraylike variates +@inline function rand!( + rng::AbstractRNG, + s::Sampleable{ArrayLikeVariate{N}}, + x::AbstractArray{<:Real,N}, +) where {N} + @boundscheck begin + size(x) == size(s) || throw(DimensionMismatch("inconsistent array dimensions")) + end + return _rand!(rng, s, x) +end + +@inline function rand!( + rng::AbstractRNG, + s::Sampleable{ArrayLikeVariate{N}}, + x::AbstractArray{<:Real,M}, +) where {N,M} + @boundscheck begin + M > N || + throw(DimensionMismatch( + "number of dimensions of `x` ($M) must be greater than number of dimensions of `s` ($N)" + )) + ntuple(i -> size(x, i), Val(N)) == size(s) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + # the function barrier fixes performance issues if `sampler(s)` is type unstable + return _rand!(rng, sampler(s), x) +end + +function _rand!( + rng::AbstractRNG, + s::Sampleable{<:ArrayLikeVariate}, + x::AbstractArray{<:Real}, +) + @inbounds for xi in eachvariate(x, variate_form(typeof(s))) + rand!(rng, s, xi) + end + return x +end + +Base.@propagate_inbounds function rand!( + rng::AbstractRNG, + s::Sampleable{ArrayLikeVariate{N}}, + x::AbstractArray{<:AbstractArray{<:Real,N}}, +) where {N} + sz = size(s) + allocate = !all(isassigned(x, i) && size(@inbounds x[i]) == sz for i in eachindex(x)) + return rand!(rng, s, x, allocate) +end + +Base.@propagate_inbounds function rand!( + s::Sampleable{ArrayLikeVariate{N}}, + x::AbstractArray{<:AbstractArray{<:Real,N}}, + allocate::Bool, +) where {N} + return rand!(GLOBAL_RNG, s, x, allocate) +end +@inline function rand!( + rng::AbstractRNG, + s::Sampleable{ArrayLikeVariate{N}}, + x::AbstractArray{<:AbstractArray{<:Real,N}}, + allocate::Bool, +) where {N} + @boundscheck begin + if !allocate + sz = size(s) + all(size(xi) == sz for xi in x) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + end + # the function barrier fixes performance issues if `sampler(s)` is type unstable + return _rand!(rng, sampler(s), x, allocate) +end + +function _rand!( + rng::AbstractRNG, + s::Sampleable{ArrayLikeVariate{N}}, + x::AbstractArray{<:AbstractArray{<:Real,N}}, + allocate::Bool, +) where {N} + if allocate + @inbounds for i in eachindex(x) + x[i] = rand(rng, s) + end + else + @inbounds for xi in x + rand!(rng, s, xi) + end + end + return x +end """ sampler(d::Distribution) -> Sampleable diff --git a/src/matrix/matrixnormal.jl b/src/matrix/matrixnormal.jl index d34fdf6c7..eda55aadb 100644 --- a/src/matrix/matrixnormal.jl +++ b/src/matrix/matrixnormal.jl @@ -89,7 +89,7 @@ mean(d::MatrixNormal) = d.M mode(d::MatrixNormal) = d.M -cov(d::MatrixNormal, ::Val{true}=Val(true)) = Matrix(kron(d.V, d.U)) +cov(d::MatrixNormal) = Matrix(kron(d.V, d.U)) cov(d::MatrixNormal, ::Val{false}) = ((n, p) = size(d); reshape(cov(d), n, p, n, p)) @@ -128,12 +128,6 @@ function _rand!(rng::AbstractRNG, d::MatrixNormal, Y::AbstractMatrix) Y .= d.M .+ A * X * B end -# ----------------------------------------------------------------------------- -# Transformation -# ----------------------------------------------------------------------------- - -vec(d::MatrixNormal) = MvNormal(vec(d.M), kron(d.V, d.U)) - # ----------------------------------------------------------------------------- # Test utils # ----------------------------------------------------------------------------- diff --git a/src/matrix/matrixreshaped.jl b/src/matrix/matrixreshaped.jl deleted file mode 100644 index 53cf2be3b..000000000 --- a/src/matrix/matrixreshaped.jl +++ /dev/null @@ -1,75 +0,0 @@ -""" - MatrixReshaped(D, n, p) -```julia -D::MultivariateDistribution base distribution -n::Integer number of rows -p::Integer number of columns -``` -Reshapes a multivariate distribution into a matrix distribution with n rows and -p columns. - -""" -struct MatrixReshaped{S<:ValueSupport,D<:MultivariateDistribution{S}} <: - MatrixDistribution{S} - d::D - num_rows::Int - num_cols::Int - function MatrixReshaped( - d::D, - n::N, - p::N, - ) where { - D<:MultivariateDistribution{ - S, - }, - } where {S<:ValueSupport} where {N<:Integer} - (n > 0 && p > 0) || throw(ArgumentError("n and p should be positive")) - n * p == length(d) || - throw(ArgumentError("Dimensions provided ($n x $p) do not match source distribution of length $(length(d))")) - return new{S,D}(d, n, p) - end -end - -MatrixReshaped(D::MultivariateDistribution, n::Integer) = - MatrixReshaped(D, n, n) - -show(io::IO, d::MatrixReshaped) = - show_multline(io, d, [(:num_rows, d.num_rows), (:num_cols, d.num_cols)]) - - -# ----------------------------------------------------------------------------- -# Properties -# ----------------------------------------------------------------------------- - -size(d::MatrixReshaped) = (d.num_rows, d.num_cols) - -length(d::MatrixReshaped) = length(d.d) - -rank(d::MatrixReshaped) = minimum(size(d)) - -function insupport(d::MatrixReshaped, X::AbstractMatrix) - return isreal(X) && size(d) == size(X) && insupport(d.d, vec(X)) -end - -mean(d::MatrixReshaped) = reshape(mean(d.d), size(d)) -mode(d::MatrixReshaped) = reshape(mode(d.d), size(d)) -cov(d::MatrixReshaped, ::Val{true} = Val(true)) = - reshape(cov(d.d), prod(size(d)), prod(size(d))) -cov(d::MatrixReshaped, ::Val{false}) = - ((n, p) = size(d); reshape(cov(d), n, p, n, p)) -var(d::MatrixReshaped) = reshape(var(d.d), size(d)) - -params(d::MatrixReshaped) = (d.d, d.num_rows, d.num_cols) - -@inline partype( - d::MatrixReshaped{S,<:MultivariateDistribution{S}}, -) where {S<:Real} = S - -_logpdf(d::MatrixReshaped, X::AbstractMatrix) = logpdf(d.d, vec(X)) - -function _rand!(rng::AbstractRNG, d::MatrixReshaped, Y::AbstractMatrix) - rand!(rng, d.d, view(Y, :)) - return Y -end - -vec(d::MatrixReshaped) = d.d diff --git a/src/matrix/matrixtdist.jl b/src/matrix/matrixtdist.jl index b2d25bab9..5e2a0f9a9 100644 --- a/src/matrix/matrixtdist.jl +++ b/src/matrix/matrixtdist.jl @@ -111,7 +111,7 @@ end mode(d::MatrixTDist) = d.M -cov(d::MatrixTDist, ::Val{true}=Val(true)) = d.ν <= 2 ? throw(ArgumentError("cov only defined for df > 2")) : Matrix(kron(d.Ω, d.Σ)) ./ (d.ν - 2) +cov(d::MatrixTDist) = d.ν <= 2 ? throw(ArgumentError("cov only defined for df > 2")) : Matrix(kron(d.Ω, d.Σ)) ./ (d.ν - 2) cov(d::MatrixTDist, ::Val{false}) = ((n, p) = size(d); reshape(cov(d), n, p, n, p)) diff --git a/src/matrixvariates.jl b/src/matrixvariates.jl index c7ac4ed4d..2e3ed72d5 100644 --- a/src/matrixvariates.jl +++ b/src/matrixvariates.jl @@ -24,14 +24,6 @@ The rank of each sample from the distribution `d`. """ LinearAlgebra.rank(d::MatrixDistribution) -""" - vec(d::MatrixDistribution) - -If known, returns a `MultivariateDistribution` instance representing the -distribution of vec(X), where X is a random matrix with distribution `d`. -""" -Base.vec(d::MatrixDistribution) - """ inv(d::MatrixDistribution) @@ -59,7 +51,7 @@ var(d::MatrixDistribution) = ((n, p) = size(d); [var(d, i, j) for i in 1:n, j in Compute the covariance matrix for `vec(X)`, where `X` is a random matrix with distribution `d`. """ -function cov(d::MatrixDistribution, ::Val{true}=Val(true)) +function cov(d::MatrixDistribution) M = length(d) V = zeros(partype(d), M, M) iter = CartesianIndices(size(d)) @@ -72,6 +64,7 @@ function cov(d::MatrixDistribution, ::Val{true}=Val(true)) end return V + tril(V, -1)' end +cov(d::MatrixDistribution, ::Val{true}) = cov(d) """ cov(d::MatrixDistribution, flattened = Val(false)) @@ -83,155 +76,10 @@ function cov(d::MatrixDistribution, ::Val{false}) [cov(d, i, j, k, l) for i in 1:n, j in 1:p, k in 1:n, l in 1:p] end -""" - _rand!(::AbstractRNG, ::MatrixDistribution, A::AbstractMatrix) - -Sample the matrix distribution and store the result in `A`. -Must be implemented by matrix-variate distributions. -""" -_rand!(::AbstractRNG, ::MatrixDistribution, A::AbstractMatrix) - -## sampling - -# multivariate with pre-allocated 3D array -function _rand!(rng::AbstractRNG, s::Sampleable{Matrixvariate}, - m::AbstractArray{<:Real, 3}) - @boundscheck (size(m, 1), size(m, 2)) == (size(s, 1), size(s, 2)) || - throw(DimensionMismatch("Output size inconsistent with matrix size.")) - smp = sampler(s) - for i in Base.OneTo(size(m,3)) - _rand!(rng, smp, view(m,:,:,i)) - end - return m -end - -# multiple matrix-variates with pre-allocated array of maybe pre-allocated matrices -rand!(rng::AbstractRNG, s::Sampleable{Matrixvariate}, - X::AbstractArray{<:AbstractMatrix}) = - @inbounds rand!(rng, s, X, - !all([isassigned(X,i) for i in eachindex(X)]) || - (sz = size(s); !all(size(x) == sz for x in X))) - -function rand!(rng::AbstractRNG, s::Sampleable{Matrixvariate}, - X::AbstractArray{M}, allocate::Bool) where M <: AbstractMatrix - smp = sampler(s) - if allocate - for i in eachindex(X) - X[i] = _rand!(rng, smp, M(undef, size(s))) - end - else - for x in X - rand!(rng, smp, x) - end - end - return X -end - -# multiple matrix-variates, must allocate array of arrays -rand(rng::AbstractRNG, s::Sampleable{Matrixvariate}, dims::Dims) = - rand!(rng, s, Array{Matrix{eltype(s)}}(undef, dims), true) -rand(rng::AbstractRNG, s::Sampleable{Matrixvariate,Continuous}, dims::Dims) = - rand!(rng, s, Array{Matrix{float(eltype(s))}}(undef, dims), true) - -# single matrix-variate, must allocate one matrix -rand(rng::AbstractRNG, s::Sampleable{Matrixvariate}) = - _rand!(rng, s, Matrix{eltype(s)}(undef, size(s))) -rand(rng::AbstractRNG, s::Sampleable{Matrixvariate,Continuous}) = - _rand!(rng, s, Matrix{float(eltype(s))}(undef, size(s))) - -# single matrix-variate with pre-allocated matrix -function rand!(rng::AbstractRNG, s::Sampleable{Matrixvariate}, - A::AbstractMatrix{<:Real}) - @boundscheck size(A) == size(s) || - throw(DimensionMismatch("Output size inconsistent with matrix size.")) - return _rand!(rng, s, A) -end - # pdf & logpdf -_logpdf(d::MatrixDistribution, X::AbstractMatrix) = logkernel(d, X) + d.logc0 - -_pdf(d::MatrixDistribution, x::AbstractMatrix{T}) where {T<:Real} = exp(_logpdf(d, x)) - -""" - logpdf(d::MatrixDistribution, AbstractMatrix) - -Compute the logarithm of the probability density at the input matrix `x`. -""" -function logpdf(d::MatrixDistribution, x::AbstractMatrix{T}) where T<:Real - size(x) == size(d) || - throw(DimensionMismatch("Inconsistent array dimensions.")) - _logpdf(d, x) -end - -""" - pdf(d::MatrixDistribution, x::AbstractArray) - -Compute the probability density at the input matrix `x`. -""" -function pdf(d::MatrixDistribution, x::AbstractMatrix{T}) where T<:Real - size(x) == size(d) || - throw(DimensionMismatch("Inconsistent array dimensions.")) - _pdf(d, x) -end - -function _logpdf!(r::AbstractArray, d::MatrixDistribution, X::AbstractArray{M}) where M<:Matrix - for i = 1:length(X) - r[i] = logpdf(d, X[i]) - end - return r -end - -function _pdf!(r::AbstractArray, d::MatrixDistribution, X::AbstractArray{M}) where M<:Matrix - for i = 1:length(X) - r[i] = pdf(d, X[i]) - end - return r -end - -function logpdf!(r::AbstractArray, d::MatrixDistribution, X::AbstractArray{M}) where M<:Matrix - length(X) == length(r) || - throw(DimensionMismatch("Inconsistent array dimensions.")) - _logpdf!(r, d, X) -end - -function pdf!(r::AbstractArray, d::MatrixDistribution, X::AbstractArray{M}) where M<:Matrix - length(X) == length(r) || - throw(DimensionMismatch("Inconsistent array dimensions.")) - _pdf!(r, d, X) -end - -function logpdf(d::MatrixDistribution, X::AbstractArray{<:AbstractMatrix{<:Real}}) - map(Base.Fix1(logpdf, d), X) -end - -function pdf(d::MatrixDistribution, X::AbstractArray{<:AbstractMatrix{<:Real}}) - map(Base.Fix1(pdf, d), X) -end - -""" - _logpdf(d::MatrixDistribution, x::AbstractArray) - -Evaluate logarithm of pdf value for a given sample `x`. This function need not perform dimension checking. -""" -_logpdf(d::MatrixDistribution, x::AbstractArray) - -""" - loglikelihood(d::MatrixDistribution, x::AbstractArray) - -The log-likelihood of distribution `d` with respect to all samples contained in array `x`. - -Here, `x` can be a matrix of size `size(d)`, a three-dimensional array with `size(d, 1)` -rows and `size(d, 2)` columns, or an array of matrices of size `size(d)`. -""" -loglikelihood(d::MatrixDistribution, X::AbstractMatrix{<:Real}) = logpdf(d, X) -function loglikelihood(d::MatrixDistribution, X::AbstractArray{<:Real,3}) - (size(X, 1), size(X, 2)) == size(d) || throw(DimensionMismatch("Inconsistent array dimensions.")) - return sum(i -> _logpdf(d, view(X, :, :, i)), axes(X, 3)) -end -function loglikelihood(d::MatrixDistribution, X::AbstractArray{<:AbstractMatrix{<:Real}}) - return sum(x -> logpdf(d, x), X) -end +# TODO: Remove or restrict - this causes many ambiguity errors... +_logpdf(d::MatrixDistribution, X::AbstractMatrix{<:Real}) = logkernel(d, X) + d.logc0 # for testing is_univariate(d::MatrixDistribution) = size(d) == (1, 1) @@ -240,7 +88,6 @@ check_univariate(d::MatrixDistribution) = is_univariate(d) || throw(ArgumentErro ##### Specific distributions ##### for fname in ["wishart.jl", "inversewishart.jl", "matrixnormal.jl", - "matrixreshaped.jl", "matrixtdist.jl", "matrixbeta.jl", - "matrixfdist.jl", "lkj.jl"] + "matrixtdist.jl", "matrixbeta.jl", "matrixfdist.jl", "lkj.jl"] include(joinpath("matrix", fname)) end diff --git a/src/mixtures/mixturemodel.jl b/src/mixtures/mixturemodel.jl index 7ced3ff1f..06c91c75b 100644 --- a/src/mixtures/mixturemodel.jl +++ b/src/mixtures/mixturemodel.jl @@ -468,15 +468,18 @@ function MixtureSampler(d::MixtureModel{VF,VS}) where {VF,VS} MixtureSampler{VF,VS,eltype(csamplers)}(csamplers, psampler) end +Base.length(s::MixtureSampler) = length(first(s.csamplers)) + rand(rng::AbstractRNG, s::MixtureSampler{Univariate}) = rand(rng, s.csamplers[rand(rng, s.psampler)]) rand(rng::AbstractRNG, d::MixtureModel{Univariate}) = rand(rng, component(d, rand(rng, d.prior))) # multivariate mixture sampler for a vector -_rand!(rng::AbstractRNG, s::MixtureSampler{Multivariate}, x::AbstractVector) = - _rand!(rng, s.csamplers[rand(rng, s.psampler)], x) -_rand!(rng::AbstractRNG, s::MixtureModel{Multivariate}, x::AbstractVector) = - _rand!(rng, sampler(s), x) +_rand!(rng::AbstractRNG, s::MixtureSampler{Multivariate}, x::AbstractVector{<:Real}) = + @inbounds rand!(rng, s.csamplers[rand(rng, s.psampler)], x) +# if only a single sample is requested, no alias table is created +_rand!(rng::AbstractRNG, d::MixtureModel{Multivariate}, x::AbstractVector{<:Real}) = + @inbounds rand!(rng, component(d, rand(rng, d.prior)), x) sampler(d::MixtureModel) = MixtureSampler(d) diff --git a/src/multivariate/multinomial.jl b/src/multivariate/multinomial.jl index fb55e77f6..939cae1a1 100644 --- a/src/multivariate/multinomial.jl +++ b/src/multivariate/multinomial.jl @@ -51,9 +51,12 @@ params(d::Multinomial) = (d.n, d.p) ### Conversions convert(::Type{Multinomial{T, TV}}, d::Multinomial) where {T<:Real, TV<:AbstractVector{T}} = Multinomial(d.n, TV(d.p)) +convert(::Type{Multinomial{T, TV}}, d::Multinomial{T, TV}) where {T<:Real, TV<:AbstractVector{T}} = d convert(::Type{Multinomial{T, TV}}, n, p::AbstractVector) where {T<:Real, TV<:AbstractVector} = Multinomial(n, TV(p)) convert(::Type{Multinomial{T}}, d::Multinomial) where {T<:Real} = Multinomial(d.n, T.(d.p)) +convert(::Type{Multinomial{T}}, d::Multinomial{T}) where {T<:Real} = d convert(::Type{Multinomial{T}}, n, p::AbstractVector) where {T<:Real} = Multinomial(n, T.(p)) + # Statistics mean(d::Multinomial) = d.n .* d.p @@ -161,9 +164,8 @@ end # Sampling -_rand!(d::Multinomial, x::AbstractVector{T}) where T<:Real = - multinom_rand!(ntrials(d), probs(d), x) -_rand!(rng::AbstractRNG, d::Multinomial, x::AbstractVector{T}) where T<:Real = +# if only a single sample is requested, no alias table is created +_rand!(rng::AbstractRNG, d::Multinomial, x::AbstractVector{<:Real}) = multinom_rand!(rng, ntrials(d), probs(d), x) sampler(d::Multinomial) = MultinomialSampler(ntrials(d), probs(d)) diff --git a/src/multivariate/mvnormal.jl b/src/multivariate/mvnormal.jl index c72f0e82c..220d3b30a 100644 --- a/src/multivariate/mvnormal.jl +++ b/src/multivariate/mvnormal.jl @@ -104,14 +104,14 @@ end function kldivergence(p::AbstractMvNormal, q::AbstractMvNormal) # This is the generic implementation for AbstractMvNormal, you might need to specialize for your type - length(p) == length(q) || + length(p) == length(q) || throw(DimensionMismatch("Distributions p and q have different dimensions $(length(p)) and $(length(q))")) # logdetcov is used separately from _cov for any potential optimization done there return (tr(_cov(q) \ _cov(p)) + sqmahal(q, mean(p)) - length(p) + logdetcov(q) - logdetcov(p)) / 2 end # This is a workaround to take advantage of the PDMats objects for MvNormal and avoid copies as Matrix -# TODO: Remove this once `cov(::MvNormal)` returns the PDMats object +# TODO: Remove this once `cov(::MvNormal)` returns the PDMats object _cov(d::AbstractMvNormal) = cov(d) """ @@ -142,7 +142,7 @@ sqmahal(d::AbstractMvNormal, x::AbstractMatrix) = sqmahal!(Vector{promote_type(p _logpdf(d::AbstractMvNormal, x::AbstractVector) = mvnormal_c0(d) - sqmahal(d, x)/2 -function _logpdf!(r::AbstractArray, d::AbstractMvNormal, x::AbstractMatrix) +function _logpdf!(r::AbstractArray{<:Real}, d::AbstractMvNormal, x::AbstractMatrix{<:Real}) sqmahal!(r, d, x) c0 = mvnormal_c0(d) for i = 1:size(x, 2) @@ -151,8 +151,6 @@ function _logpdf!(r::AbstractArray, d::AbstractMvNormal, x::AbstractMatrix) r end -_pdf!(r::AbstractArray, d::AbstractMvNormal, x::AbstractMatrix) = exp!(_logpdf!(r, d, x)) - ########################################################### # # MvNormal diff --git a/src/multivariate/mvtdist.jl b/src/multivariate/mvtdist.jl index edc9cec64..bdd1899a5 100644 --- a/src/multivariate/mvtdist.jl +++ b/src/multivariate/mvtdist.jl @@ -146,8 +146,6 @@ function _logpdf!(r::AbstractArray, d::AbstractMvTDist, x::AbstractMatrix) return r end -_pdf!(r::AbstractArray, d::AbstractMvTDist, x::AbstractMatrix{T}) where {T<:Real} = exp!(_logpdf!(r, d, x)) - function gradlogpdf(d::GenericMvTDist, x::AbstractVector{<:Real}) z = x - d.μ prz = invscale(d)*z diff --git a/src/multivariates.jl b/src/multivariates.jl index 0d2f214c9..1a087f1ba 100644 --- a/src/multivariates.jl +++ b/src/multivariates.jl @@ -16,71 +16,12 @@ size(d::MultivariateDistribution) ## sampling -""" - rand!([rng::AbstractRNG,] d::MultivariateDistribution, x::AbstractArray) - -Draw samples and output them to a pre-allocated array x. Here, x can be either -a vector of length `dim(d)` or a matrix with `dim(d)` rows. -""" -rand!(rng::AbstractRNG, d::MultivariateDistribution, x::AbstractArray) - -# multivariate with pre-allocated array -function _rand!(rng::AbstractRNG, s::Sampleable{Multivariate}, m::AbstractMatrix) - @boundscheck size(m, 1) == length(s) || - throw(DimensionMismatch("Output size inconsistent with sample length.")) - smp = sampler(s) - for i in Base.OneTo(size(m,2)) - _rand!(rng, smp, view(m,:,i)) - end - return m -end - -# single multivariate with pre-allocated vector -function rand!(rng::AbstractRNG, s::Sampleable{Multivariate}, - v::AbstractVector{<:Real}) - @boundscheck length(v) == length(s) || - throw(DimensionMismatch("Output size inconsistent with sample length.")) - _rand!(rng, s, v) -end - -# multiple multivariates with pre-allocated array of maybe pre-allocated vectors -rand!(rng::AbstractRNG, s::Sampleable{Multivariate}, - X::AbstractArray{<:AbstractVector}) = - @inbounds rand!(rng, s, X, - !all([isassigned(X,i) for i in eachindex(X)]) || - !all(length.(X) .== length(s))) - -function rand!(rng::AbstractRNG, s::Sampleable{Multivariate}, - X::AbstractArray{V}, allocate::Bool) where V <: AbstractVector - smp = sampler(s) - if allocate - for i in eachindex(X) - X[i] = _rand!(rng, smp, V(undef, size(s))) - end - else - for x in X - rand!(rng, smp, x) - end - end - return X -end - -# multiple multivariate, must allocate matrix or array of vectors -rand(s::Sampleable{Multivariate}, n::Int) = rand(GLOBAL_RNG, s, n) +# multiple multivariate, must allocate matrix +# TODO: inconsistency with other `ArrayLikeVariate`s and `rand(s, (n,))` - maybe remove? rand(rng::AbstractRNG, s::Sampleable{Multivariate}, n::Int) = - _rand!(rng, s, Matrix{eltype(s)}(undef, length(s), n)) + @inbounds rand!(rng, sampler(s), Matrix{eltype(s)}(undef, length(s), n)) rand(rng::AbstractRNG, s::Sampleable{Multivariate,Continuous}, n::Int) = - _rand!(rng, s, Matrix{float(eltype(s))}(undef, length(s), n)) -rand(rng::AbstractRNG, s::Sampleable{Multivariate}, dims::Dims) = - rand!(rng, s, Array{Vector{eltype(s)}}(undef, dims), true) -rand(rng::AbstractRNG, s::Sampleable{Multivariate,Continuous}, dims::Dims) = - rand!(rng, s, Array{Vector{float(eltype(s))}}(undef, dims), true) - -# single multivariate, must allocate vector -rand(rng::AbstractRNG, s::Sampleable{Multivariate}) = - _rand!(rng, s, Vector{eltype(s)}(undef, length(s))) -rand(rng::AbstractRNG, s::Sampleable{Multivariate,Continuous}) = - _rand!(rng, s, Vector{float(eltype(s))}(undef, length(s))) + @inbounds rand!(rng, sampler(s), Matrix{float(eltype(s))}(undef, length(s), n)) ## domain @@ -166,110 +107,6 @@ function cor(d::MultivariateDistribution) return R end -# pdf and logpdf - -""" - pdf(d::MultivariateDistribution, x::AbstractArray) - -Return the probability density of distribution `d` evaluated at `x`. - -- If `x` is a vector, it returns the result as a scalar. -- If `x` is a matrix with n columns, it returns a vector `r` of length n, where `r[i]` corresponds -to `x[:,i]` (i.e. treating each column as a sample). - -`pdf!(r, d, x)` will write the results to a pre-allocated array `r`. -""" -pdf(d::MultivariateDistribution, x::AbstractArray) - -""" - logpdf(d::MultivariateDistribution, x::AbstractArray) - -Return the logarithm of probability density evaluated at `x`. - -- If `x` is a vector, it returns the result as a scalar. -- If `x` is a matrix with n columns, it returns a vector `r` of length n, where `r[i]` corresponds to `x[:,i]`. - -`logpdf!(r, d, x)` will write the results to a pre-allocated array `r`. -""" -logpdf(d::MultivariateDistribution, x::AbstractArray) - -_pdf(d::MultivariateDistribution, X::AbstractVector) = exp(_logpdf(d, X)) - -function logpdf(d::MultivariateDistribution, X::AbstractVector) - length(X) == length(d) || - throw(DimensionMismatch("Inconsistent array dimensions.")) - _logpdf(d, X) -end - -function pdf(d::MultivariateDistribution, X::AbstractVector) - length(X) == length(d) || - throw(DimensionMismatch("Inconsistent array dimensions.")) - _pdf(d, X) -end - -function _logpdf!(r::AbstractArray, d::MultivariateDistribution, X::AbstractMatrix) - for i in 1 : size(X,2) - @inbounds r[i] = logpdf(d, view(X,:,i)) - end - return r -end - -function _pdf!(r::AbstractArray, d::MultivariateDistribution, X::AbstractMatrix) - for i in 1 : size(X,2) - @inbounds r[i] = pdf(d, view(X,:,i)) - end - return r -end - -function logpdf!(r::AbstractArray, d::MultivariateDistribution, X::AbstractMatrix) - size(X) == (length(d), length(r)) || - throw(DimensionMismatch("Inconsistent array dimensions.")) - _logpdf!(r, d, X) -end - -function pdf!(r::AbstractArray, d::MultivariateDistribution, X::AbstractMatrix) - size(X) == (length(d), length(r)) || - throw(DimensionMismatch("Inconsistent array dimensions.")) - _pdf!(r, d, X) -end - -function logpdf(d::MultivariateDistribution, X::AbstractMatrix) - size(X, 1) == length(d) || - throw(DimensionMismatch("Inconsistent array dimensions.")) - map(i -> _logpdf(d, view(X, :, i)), axes(X, 2)) -end - -function pdf(d::MultivariateDistribution, X::AbstractMatrix) - size(X, 1) == length(d) || - throw(DimensionMismatch("Inconsistent array dimensions.")) - map(i -> _pdf(d, view(X, :, i)), axes(X, 2)) -end - -""" - _logpdf{T<:Real}(d::MultivariateDistribution, x::AbstractArray) - -Evaluate logarithm of pdf value for a given vector `x`. This function need not perform dimension checking. -Generally, one does not need to implement `pdf` (or `_pdf`) as fallback methods are provided in `src/multivariates.jl`. -""" -_logpdf(d::MultivariateDistribution, x::AbstractArray) - -""" - loglikelihood(d::MultivariateDistribution, x::AbstractArray) - -The log-likelihood of distribution `d` with respect to all samples contained in array `x`. - -Here, `x` can be a vector of length `dim(d)`, a matrix with `dim(d)` rows, or an array of -vectors of length `dim(d)`. -""" -loglikelihood(d::MultivariateDistribution, X::AbstractVector{<:Real}) = logpdf(d, X) -function loglikelihood(d::MultivariateDistribution, X::AbstractMatrix{<:Real}) - size(X, 1) == length(d) || throw(DimensionMismatch("Inconsistent array dimensions.")) - return sum(i -> _logpdf(d, view(X, :, i)), 1:size(X, 2)) -end -function loglikelihood(d::MultivariateDistribution, X::AbstractArray{<:AbstractVector}) - return sum(x -> logpdf(d, x), X) -end - ##### Specific distributions ##### for fname in ["dirichlet.jl", diff --git a/src/reshaped.jl b/src/reshaped.jl new file mode 100644 index 000000000..b447b7eef --- /dev/null +++ b/src/reshaped.jl @@ -0,0 +1,163 @@ +""" + ReshapedDistribution(d::Distribution{<:ArrayLikeVariate}, dims::Dims{N}) + +Distribution of the `N`-dimensional random variable `reshape(X, dims)` where `X` is a random +variable wth distribution `d`. + +It is recommended to not use `reshape` instead of the constructor of `ReshapedDistribution` +directly since `reshape` can return more optimized distributions for specific types of `d` +and number of dimensions `N`. +""" +struct ReshapedDistribution{N,S<:ValueSupport,D<:Distribution{<:ArrayLikeVariate,S}} <: Distribution{ArrayLikeVariate{N},S} + dist::D + dims::Dims{N} + + function ReshapedDistribution(dist::Distribution{<:ArrayLikeVariate,S}, dims::Dims{N}) where {N,S<:ValueSupport} + _reshape_check_dims(dist, dims) + return new{N,S,typeof(dist)}(dist, dims) + end +end + +function _reshape_check_dims(dist::Distribution{<:ArrayLikeVariate}, dims::Dims) + (all(d > 0 for d in dims) && length(dist) == prod(dims)) || + throw(ArgumentError("dimensions $(dims) do not match size of source distribution $(size(dist))")) +end + +Base.size(d::ReshapedDistribution) = d.dims +Base.eltype(::Type{ReshapedDistribution{<:Any,<:ValueSupport,D}}) where {D} = eltype(D) + +partype(d::ReshapedDistribution) = partype(d.dist) +params(d::ReshapedDistribution) = (d.dist, d.dims) + +function insupport(d::ReshapedDistribution{N}, x::AbstractArray{<:Real,N}) where {N} + return size(d) == size(x) && insupport(d.dist, reshape(x, size(d.dist))) +end + +mean(d::ReshapedDistribution) = reshape(mean(d.dist), size(d)) +var(d::ReshapedDistribution) = reshape(var(d.dist), size(d)) +cov(d::ReshapedDistribution) = reshape(cov(d.dist), length(d), length(d)) +function cov(d::ReshapedDistribution{2}, ::Val{false}) + n, p = size(d) + return reshape(cov(d), n, p, n, p) +end + +mode(d::ReshapedDistribution) = reshape(mode(d.dist), size(d)) + +# TODO: remove? +rank(d::ReshapedDistribution{2}) = minimum(size(d)) + +# logpdf evaluation +# have to fix method ambiguity due to default fallback for `MatrixDistribution`... +_logpdf(d::ReshapedDistribution{N}, x::AbstractArray{<:Real,N}) where {N} = __logpdf(d, x) +_logpdf(d::ReshapedDistribution{2}, x::AbstractMatrix{<:Real}) = __logpdf(d, x) +function __logpdf(d::ReshapedDistribution{N}, x::AbstractArray{<:Real,N}) where {N} + dist = d.dist + return @inbounds logpdf(dist, reshape(x, size(dist))) +end + +# loglikelihood +# useful if the original distribution defined more optimized methods +@inline function loglikelihood( + d::ReshapedDistribution{N}, + x::AbstractArray{<:Real,N}, +) where {N} + @boundscheck begin + size(x) == size(d) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + dist = d.dist + return @inbounds loglikelihood(dist, reshape(x, size(dist))) +end +@inline function loglikelihood( + d::ReshapedDistribution{N}, + x::AbstractArray{<:Real,M}, +) where {N,M} + @boundscheck begin + M > N || + throw(DimensionMismatch( + "number of dimensions of `x` ($M) must be greater than number of dimensions of `d` ($N)" + )) + ntuple(i -> size(x, i), Val(N)) == size(d) || + throw(DimensionMismatch("inconsistent array dimensions")) + end + dist = d.dist + trailingsize = ntuple(i -> size(x, N + i), Val(M - N)) + return @inbounds loglikelihood(dist, reshape(x, size(dist)..., trailingsize...)) +end + +# sampling +function _rand!( + rng::AbstractRNG, + d::ReshapedDistribution{N}, + x::AbstractArray{<:Real,N} +) where {N} + dist = d.dist + @inbounds rand!(rng, dist, reshape(x, size(dist))) + return x +end + +""" + reshape(d::Distribution{<:ArrayLikeVariate}, dims::Int...) + reshape(d::Distribution{<:ArrayLikeVariate}, dims::Dims) + +Return a [`Distribution`](@ref) of `reshape(X, dims)` where `X` is a random variable with +distribution `d`. + +The default implementation returns a [`ReshapedDistribution`](@ref). However, it can return +more optimized distributions for specific types of distributions and numbers of dimensions. +Therefore it is recommended to use `reshape` instead of the constructor of +`ReshapedDistribution`. + +# Implementation + +Since `reshape(d, dims::Int...)` calls `reshape(d, dims::Dims)`, one should implement +`reshape(d, ::Dims)` for desired distributions `d`. + +See also: [`vec`](@ref) +""" +function Base.reshape(dist::Distribution{<:ArrayLikeVariate}, dims::Dims) + return ReshapedDistribution(dist, dims) +end +function Base.reshape(dist::Distribution{<:ArrayLikeVariate}, dims1::Int, dims::Int...) + return reshape(dist, (dims1, dims...)) +end + +""" + vec(d::Distribution{<:ArrayLikeVariate}) + +Return a [`MultivariateDistribution`](@ref) of `vec(X)` where `X` is a random variable with +distribution `d`. + +The default implementation returns a [`ReshapedDistribution`](@ref). However, it can return +more optimized distributions for specific types of distributions and numbers of dimensions. +Therefore it is recommended to use `vec` instead of the constructor of +`ReshapedDistribution`. + +# Implementation + +Since `vec(d)` is defined as `reshape(d, length(d))` one should implement +`reshape(d, ::Tuple{Int})` rather than `vec`. + +See also: [`reshape`](@ref) +""" +Base.vec(dist::Distribution{<:ArrayLikeVariate}) = reshape(dist, length(dist)) + +# avoid unnecessary wrappers +function Base.reshape( + dist::ReshapedDistribution{<:Any,<:ValueSupport,<:MultivariateDistribution}, + dims::Tuple{Int}, +) + _reshape_check_dims(dist, dims) + return dist.dist +end + +function Base.reshape(dist::MultivariateDistribution, dims::Tuple{Int}) + _reshape_check_dims(dist, dims) + return dist +end + +# specialization for flattened `MatrixNormal` +function Base.reshape(dist::MatrixNormal, dims::Tuple{Int}) + _reshape_check_dims(dist, dims) + return MvNormal(vec(dist.M), kron(dist.V, dist.U)) +end diff --git a/src/samplers/multinomial.jl b/src/samplers/multinomial.jl index b45c2972a..e8464a1b4 100644 --- a/src/samplers/multinomial.jl +++ b/src/samplers/multinomial.jl @@ -1,46 +1,5 @@ - -function multinom_rand!(n::Int, p::AbstractVector{Float64}, - x::AbstractVector{T}) where T<:Real - k = length(p) - length(x) == k || throw(DimensionMismatch("Invalid argument dimension.")) - - rp = 1.0 # remaining total probability - i = 0 - km1 = k - 1 - - while i < km1 && n > 0 - i += 1 - @inbounds pi = p[i] - if pi < rp - xi = rand(Binomial(n, pi / rp)) - @inbounds x[i] = xi - n -= xi - rp -= pi - else - # In this case, we don't even have to sample - # from Binomial. Just assign remaining counts - # to xi. - - @inbounds x[i] = n - n = 0 - # rp = 0.0 (no need for this, as rp is no longer needed) - end - end - - if i == km1 - @inbounds x[k] = n - else # n must have been zero - z = zero(T) - for j = i+1 : k - @inbounds x[j] = z - end - end - - return x -end - function multinom_rand!(rng::AbstractRNG, n::Int, p::AbstractVector{Float64}, - x::AbstractVector{T}) where T<:Real + x::AbstractVector{<:Real}) k = length(p) length(x) == k || throw(DimensionMismatch("Invalid argument dimension.")) @@ -70,7 +29,7 @@ function multinom_rand!(rng::AbstractRNG, n::Int, p::AbstractVector{Float64}, if i == km1 @inbounds x[k] = n else # n must have been zero - z = zero(T) + z = zero(eltype(x)) for j = i+1 : k @inbounds x[j] = z end @@ -85,20 +44,19 @@ struct MultinomialSampler{T<:Real} <: Sampleable{Multivariate,Discrete} alias::AliasTable end -MultinomialSampler(n::Int, prob::Vector{T}) where T<:Real = - MultinomialSampler{T}(n, prob, AliasTable(prob)) +function MultinomialSampler(n::Int, prob::Vector{<:Real}) + return MultinomialSampler(n, prob, AliasTable(prob)) +end -_rand!(s::MultinomialSampler, x::AbstractVector{T}) where T<:Real = - _rand!(GLOBAL_RNG, s, x) function _rand!(rng::AbstractRNG, s::MultinomialSampler, - x::AbstractVector{T}) where T<:Real + x::AbstractVector{<:Real}) n = s.n k = length(s) if n^2 > k multinom_rand!(rng, n, s.prob, x) else # Use an alias table - fill!(x, zero(T)) + fill!(x, zero(eltype(x))) a = s.alias for i = 1:n x[rand(rng, a)] += 1 diff --git a/src/samplers/vonmisesfisher.jl b/src/samplers/vonmisesfisher.jl index 29ce3daf1..fd3eb2df0 100644 --- a/src/samplers/vonmisesfisher.jl +++ b/src/samplers/vonmisesfisher.jl @@ -1,6 +1,5 @@ # Sampler for von Mises-Fisher - -struct VonMisesFisherSampler +struct VonMisesFisherSampler <: Sampleable{Multivariate,Continuous} p::Int # the dimension κ::Float64 b::Float64 @@ -18,6 +17,8 @@ function VonMisesFisherSampler(μ::Vector{Float64}, κ::Float64) VonMisesFisherSampler(p, κ, b, x0, c, v) end +Base.length(s::VonMisesFisherSampler) = length(s.v) + @inline function _vmf_rot!(v::AbstractVector, x::AbstractVector) # rotate scale = 2.0 * (v' * x) @@ -45,15 +46,6 @@ function _rand!(rng::AbstractRNG, spl::VonMisesFisherSampler, x::AbstractVector) return _vmf_rot!(spl.v, x) end - -function _rand!(rng::AbstractRNG, spl::VonMisesFisherSampler, x::AbstractMatrix) - @inbounds for j in axes(x, 2) - _rand!(rng, spl, view(x,:,j)) - end - return x -end - - ### Core computation _vmf_bval(p::Int, κ::Real) = (p - 1) / (2.0κ + sqrt(4 * abs2(κ) + abs2(p - 1))) diff --git a/src/univariate/continuous/gumbel.jl b/src/univariate/continuous/gumbel.jl index 6be60d088..0dc8315c1 100644 --- a/src/univariate/continuous/gumbel.jl +++ b/src/univariate/continuous/gumbel.jl @@ -59,7 +59,7 @@ partype(::Gumbel{T}) where {T} = T function Base.rand(rng::Random.AbstractRNG, d::Gumbel) return d.μ - d.θ * log(randexp(rng, float(eltype(d)))) end -function Random.rand!(rng::Random.AbstractRNG, d::Gumbel, x::AbstractArray) +function _rand!(rng::Random.AbstractRNG, d::Gumbel, x::AbstractArray{<:Real}) randexp!(rng, x) x .= d.μ .- d.θ .* log.(x) return x diff --git a/src/univariate/continuous/uniform.jl b/src/univariate/continuous/uniform.jl index fc04a9546..f18eafd9c 100644 --- a/src/univariate/continuous/uniform.jl +++ b/src/univariate/continuous/uniform.jl @@ -114,7 +114,7 @@ end rand(rng::AbstractRNG, d::Uniform) = d.a + (d.b - d.a) * rand(rng) -rand!(rng::AbstractRNG, d::Uniform, A::AbstractArray) = +_rand!(rng::AbstractRNG, d::Uniform, A::AbstractArray{<:Real}) = A .= quantile.(d, rand!(rng, A)) diff --git a/src/univariates.jl b/src/univariates.jl index 52073863b..56cd1b06b 100644 --- a/src/univariates.jl +++ b/src/univariates.jl @@ -133,20 +133,14 @@ end ## sampling -# multiple univariate, must allocate array -rand(rng::AbstractRNG, s::Sampleable{Univariate}, dims::Dims) = - rand!(rng, s, Array{eltype(s)}(undef, dims)) -rand(rng::AbstractRNG, s::Sampleable{Univariate,Continuous}, dims::Dims) = - rand!(rng, s, Array{float(eltype(s))}(undef, dims)) - # multiple univariate with pre-allocated array # we use a function barrier since for some distributions `sampler(s)` is not type-stable: # https://github.com/JuliaStats/Distributions.jl/pull/1281 -function rand!(rng::AbstractRNG, s::Sampleable{Univariate}, A::AbstractArray) - return _rand_loops!(rng, sampler(s), A) +function rand!(rng::AbstractRNG, s::Sampleable{Univariate}, A::AbstractArray{<:Real}) + return _rand!(rng, sampler(s), A) end -function _rand_loops!(rng::AbstractRNG, sampler::Sampleable{Univariate}, A::AbstractArray) +function _rand!(rng::AbstractRNG, sampler::Sampleable{Univariate}, A::AbstractArray{<:Real}) for i in eachindex(A) @inbounds A[i] = rand(rng, sampler) end @@ -160,13 +154,6 @@ Generate a scalar sample from `d`. The general fallback is `quantile(d, rand())` """ rand(rng::AbstractRNG, d::UnivariateDistribution) = quantile(d, rand(rng)) -""" - rand!(rng::AbstractRNG, ::UnivariateDistribution, ::AbstractArray) - -Sample a univariate distribution and store the results in the provided array. -""" -rand!(rng::AbstractRNG, ::UnivariateDistribution, ::AbstractArray) - ## statistics """ @@ -305,6 +292,9 @@ See also: [`logpdf`](@ref). """ pdf(d::UnivariateDistribution, x::Real) = exp(logpdf(d, x)) +# extract value from array of zero dimension +_pdf(d::UnivariateDistribution, x::AbstractArray{<:Real,0}) = pdf(d, first(x)) + """ logpdf(d::UnivariateDistribution, x::Real) @@ -314,6 +304,12 @@ See also: [`pdf`](@ref). """ logpdf(d::UnivariateDistribution, x::Real) +# extract value from array of zero dimension +_logpdf(d::UnivariateDistribution, x::AbstractArray{<:Real,0}) = logpdf(d, first(x)) + +# loglikelihood for `Real` +Base.@propagate_inbounds loglikelihood(d::UnivariateDistribution, x::Real) = logpdf(d, x) + """ cdf(d::UnivariateDistribution, x::Real) @@ -467,17 +463,6 @@ function _pdf!(r::AbstractArray, d::DiscreteUnivariateDistribution, X::UnitRange return r end -## loglikelihood -""" - loglikelihood(d::UnivariateDistribution, x::Union{Real,AbstractArray}) - -The log-likelihood of distribution `d` with respect to all samples contained in `x`. - -Here `x` can be a single scalar sample or an array of samples. -""" -loglikelihood(d::UnivariateDistribution, X::AbstractArray) = sum(x -> logpdf(d, x), X) -loglikelihood(d::UnivariateDistribution, x::Real) = logpdf(d, x) - ### special definitions for distributions with integer-valued support function cdf_int(d::DiscreteUnivariateDistribution, x::Real) diff --git a/test/matrixreshaped.jl b/test/matrixreshaped.jl index 3f3169857..5ed2ec746 100644 --- a/test/matrixreshaped.jl +++ b/test/matrixreshaped.jl @@ -1,29 +1,24 @@ +# TODO: Remove when `MatrixReshaped` is removed using Distributions, Test, Random, LinearAlgebra -using Distributions: MatrixReshaped rng = MersenneTwister(123456) -σ = rand(rng, 16, 16) -μ = rand(rng, 16) -d1 = MvNormal(μ, σ * σ') -x1 = rand(rng, d1) +@testset "matrixreshaped.jl" begin -sizes = [(4, 4), (8, 2), (2, 8), (1, 16), (16, 1), (4,)] -ranks = [4, 2, 2, 1, 1, 4] +function test_matrixreshaped(rng, d1, sizes) + x1 = rand(rng, d1) + d1s = [@test_deprecated(MatrixReshaped(d1, s...)) for s in sizes] -d1s = [MatrixReshaped(d1, s...) for s in sizes] - - -@testset "MatrixReshaped MvNormal tests" begin +@testset "MatrixReshaped $(nameof(typeof(d1))) tests" begin @testset "MatrixReshaped constructor" begin for d in d1s @test d isa MatrixReshaped end end @testset "MatrixReshaped constructor errors" begin - @test_throws ArgumentError MatrixReshaped(d1, 4, 3) - @test_throws ArgumentError MatrixReshaped(d1, 3) - @test_throws ArgumentError MatrixReshaped(d1, -4, -4) + @test_throws ArgumentError MatrixReshaped(d1, length(d1), 2) + @test_throws ArgumentError MatrixReshaped(d1, length(d1)) + @test_throws ArgumentError MatrixReshaped(d1, -length(d1), -1) end @testset "MatrixReshaped size" begin for (d, s) in zip(d1s[1:end-1], sizes[1:end-1]) @@ -32,12 +27,12 @@ d1s = [MatrixReshaped(d1, s...) for s in sizes] end @testset "MatrixReshaped length" begin for d in d1s - @test length(d) == length(μ) + @test length(d) == length(d1) end end @testset "MatrixReshaped rank" begin - for (d, r) in zip(d1s, ranks) - @test rank(d) == r + for (d, s) in zip(d1s, sizes) + @test rank(d) == minimum(s) end end @testset "MatrixReshaped insupport" begin @@ -49,7 +44,7 @@ d1s = [MatrixReshaped(d1, s...) for s in sizes] end @testset "MatrixReshaped mean" begin for (d, s) in zip(d1s[1:end-1], sizes[1:end-1]) - @test mean(d) == reshape(μ, s) + @test mean(d) == reshape(mean(d1), s) end end @testset "MatrixReshaped mode" begin @@ -59,8 +54,8 @@ d1s = [MatrixReshaped(d1, s...) for s in sizes] end @testset "MatrixReshaped covariance" begin for (d, (n, p)) in zip(d1s[1:end-1], sizes[1:end-1]) - @test cov(d) == σ * σ' - @test cov(d, Val(false)) == reshape(σ * σ', n, p, n, p) + @test cov(d) == cov(d1) + @test cov(d, Val(false)) == reshape(cov(d1), n, p, n, p) end end @testset "MatrixReshaped variance" begin @@ -70,100 +65,17 @@ d1s = [MatrixReshaped(d1, s...) for s in sizes] end @testset "MatrixReshaped params" begin for (d, s) in zip(d1s[1:end-1], sizes[1:end-1]) - @test params(d) == (d1, s...) + @test params(d) == (d1, s) end end @testset "MatrixReshaped partype" begin for d in d1s - @test partype(d) == Float64 - end - end - @testset "MatrixReshaped logpdf" begin - for (d, s) in zip(d1s[1:end-1], sizes[1:end-1]) - x = reshape(x1, s) - @test logpdf(d, x) == logpdf(d1, x1) - end - end - @testset "MatrixReshaped rand" begin - for d in d1s - x = rand(rng, d) - @test insupport(d, x) - @test insupport(d1, vec(x)) - @test logpdf(d, x) == logpdf(d1, vec(x)) - end - end - @testset "MatrixReshaped vec" begin - for d in d1s - @test vec(d) == d1 + @test partype(d) === partype(d1) end end -end - -α = rand(rng, 36) -d1 = Dirichlet(α) -x1 = rand(rng, d1) - -sizes = [(6, 6), (4, 9), (9, 4), (3, 12), (12, 3), (1, 36), (36, 1), (6,)] -ranks = [6, 4, 4, 3, 3, 1, 1, 6] - -d1s = [MatrixReshaped(d1, s...) for s in sizes] - -@testset "MatrixReshaped Dirichlet tests" begin - @testset "MatrixReshaped constructor" begin + @testset "MatrixReshaped eltype" begin for d in d1s - @test d isa MatrixReshaped - end - end - @testset "MatrixReshaped constructor errors" begin - @test_throws ArgumentError MatrixReshaped(d1, 4, 3) - @test_throws ArgumentError MatrixReshaped(d1, 3) - end - @testset "MatrixReshaped size" begin - for (d, s) in zip(d1s[1:end-1], sizes[1:end-1]) - @test size(d) == s - end - end - @testset "MatrixReshaped length" begin - for d in d1s - @test length(d) == length(α) - end - end - @testset "MatrixReshaped rank" begin - for (d, r) in zip(d1s, ranks) - @test rank(d) == r - end - end - @testset "MatrixReshaped insupport" begin - for (i, d) in enumerate(d1s[1:end-1]) - for (j, s) in enumerate(sizes[1:end-1]) - @test (i == j) ⊻ !insupport(d, reshape(x1, s)) - end - end - end - @testset "MatrixReshaped mean" begin - for (d, s) in zip(d1s[1:end-1], sizes[1:end-1]) - @test mean(d) == reshape(mean(d1), s) - end - end - @testset "MatrixReshaped covariance" begin - for (d, (n, p)) in zip(d1s[1:end-1], sizes[1:end-1]) - @test cov(d) == cov(d1) - @test cov(d, Val(false)) == reshape(cov(d1), n, p, n, p) - end - end - @testset "MatrixReshaped variance" begin - for (d, s) in zip(d1s[1:end-1], sizes[1:end-1]) - @test var(d) == reshape(var(d1), s) - end - end - @testset "MatrixReshaped params" begin - for (d, s) in zip(d1s[1:end-1], sizes[1:end-1]) - @test params(d) == (d1, s...) - end - end - @testset "MatrixReshaped partype" begin - for d in d1s - @test partype(d) == Float64 + @test eltype(d) === eltype(d1) end end @testset "MatrixReshaped logpdf" begin @@ -182,7 +94,22 @@ d1s = [MatrixReshaped(d1, s...) for s in sizes] end @testset "MatrixReshaped vec" begin for d in d1s - @test vec(d) == d1 + @test vec(d) === d1 end end end +end + + # MvNormal + σ = rand(rng, 16, 16) + μ = rand(rng, 16) + d1 = MvNormal(μ, σ * σ') + sizes = [(4, 4), (8, 2), (2, 8), (1, 16), (16, 1), (4,)] + test_matrixreshaped(rng, d1, sizes) + + # Dirichlet + α = rand(rng, 36) .+ 1 # mode is only defined if all alpha > 1 + d1 = Dirichlet(α) + sizes = [(6, 6), (4, 9), (9, 4), (3, 12), (12, 3), (1, 36), (36, 1), (6,)] + test_matrixreshaped(rng, d1, sizes) +end diff --git a/test/reshaped.jl b/test/reshaped.jl new file mode 100644 index 000000000..54bf9be8d --- /dev/null +++ b/test/reshaped.jl @@ -0,0 +1,151 @@ +@testset "reshaped.jl" begin + using Distributions + using Distributions: ReshapedDistribution + using Test, Random, LinearAlgebra + + rng = MersenneTwister(1234) + + function test_reshaped(rng, d1, sizes) + x1 = rand(rng, d1) + x3 = similar(x1, size(x1)..., 3) + rand!(rng, d1, x3) + d1s = map(s -> reshape(d1, s...), sizes) + + # check types + for (d, s) in zip(d1s, sizes) + @test d isa ReshapedDistribution{length(s)} + end + + # incorrect dimensions + @test_throws ArgumentError reshape(d1, length(d1), 2) + @test_throws ArgumentError reshape(d1, length(d1), 1, 2) + @test_throws ArgumentError reshape(d1, -length(d1), -1) + + # size + for (d, s) in zip(d1s, sizes) + @test size(d) == s + end + + # length + for d in d1s + @test length(d) == length(d1) + end + + # rank definition for matrix distributions + for (d, s) in zip(d1s, sizes) + if length(s) == 2 + @test rank(d) == minimum(s) + end + end + + # support + for d in d1s, s in sizes + @test (size(d) == s) ⊻ (length(s) != length(size(d)) || !insupport(d, reshape(x1, s))) + end + + # mean + for (d, s) in zip(d1s, sizes) + @test mean(d) == reshape(mean(d1), s) + end + + # mode + for (d, s) in zip(d1s, sizes) + @test mode(d) == reshape(mode(d1), s) + end + + # covariance + for (d, s) in zip(d1s, sizes) + @test cov(d) == cov(d1) + if length(s) == 2 + @test cov(d, Val(false)) == reshape(cov(d1), s..., s...) + end + end + + # variance + for (d, s) in zip(d1s, sizes) + @test var(d) == reshape(var(d1), s) + end + + # params + for (d, s) in zip(d1s, sizes) + @test params(d) == (d1, s) + end + + # partype + for d in d1s + @test partype(d) === partype(d1) + end + + # eltype + for d in d1s + @test eltype(d) === eltype(d1) + end + + # logpdf + for (d, s) in zip(d1s, sizes) + @test logpdf(d, reshape(x1, s)) == logpdf(d1, x1) + @test logpdf(d, reshape(x3, s..., 3)) == logpdf(d1, x3) + end + + # loglikelihood + for (d, s) in zip(d1s, sizes) + @test loglikelihood(d, reshape(x1, s)) == loglikelihood(d1, x1) + @test loglikelihood(d, reshape(x3, s..., 3)) == loglikelihood(d1, x3) + end + + # rand + for d in d1s + x = rand(rng, d) + @test insupport(d, x) + @test insupport(d1, vec(x)) + @test logpdf(d, x) == logpdf(d1, vec(x)) + end + + # reshape + for d in d1s + @test reshape(d, size(d1)...) === d1 + @test reshape(d, size(d1)) === d1 + if d1 isa MultivariateDistribution + @test vec(d) === d1 + end + end + if d1 isa MultivariateDistribution + @test reshape(d1, size(d1)...) === d1 + @test reshape(d1, size(d1)) === d1 + @test vec(d1) === d1 + end + end + + @testset "reshape MvNormal" begin + σ = rand(rng, 16, 16) + μ = rand(rng, 16) + d1 = MvNormal(μ, σ * σ') + sizes = [(4, 4), (8, 2), (2, 8), (1, 16), (16, 1), (4, 2, 2), (2, 4, 2), (2, 2, 2, 2)] + test_reshaped(rng, d1, sizes) + end + + @testset "reshape Dirichlet" begin + α = rand(rng, 36) .+ 1 # mode is only defined if all alpha > 1 + d1 = Dirichlet(α) + sizes = [ + (6, 6), (4, 9), (9, 4), (3, 12), (12, 3), (1, 36), (36, 1), (6, 3, 2), + (3, 2, 6), (2, 3, 3, 2), + ] + test_reshaped(rng, d1, sizes) + end + + @testset "special cases" begin + # MatrixNormal + rand_posdef_mat(X) = X * X' + I + U = rand_posdef_mat(rand(5, 5)) + V = rand_posdef_mat(rand(4, 4)) + M = randn(5, 4) + d = MatrixNormal(M, U, V) + + for v in (vec(d), reshape(d, length(d)), reshape(d, (length(d),))) + @test v isa MvNormal + @test mean(v) == vec(M) + @test cov(v) == kron(V, U) + end + end +end diff --git a/test/runtests.jl b/test/runtests.jl index 6b7833bb5..728d05443 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -66,6 +66,7 @@ const tests = [ "rician", "functionals", "density_interface", + "reshaped", ] printstyled("Running tests:\n", color=:blue) From b4ce282f50084bdd0772b7d856caf574f7cc4da4 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Sat, 27 Nov 2021 09:35:14 +0100 Subject: [PATCH 50/58] Remove some utilities and use external functions (#1425) * Remove some utilities and use external functions * Update src/multivariate/mvnormalcanon.jl --- src/multivariate/dirichlet.jl | 6 ++-- src/multivariate/dirichletmultinomial.jl | 8 ++--- src/multivariate/multinomial.jl | 8 ++--- src/multivariate/mvlognormal.jl | 7 ++-- src/multivariate/mvnormal.jl | 17 ++++++---- src/multivariate/mvnormalcanon.jl | 14 +++++--- src/univariate/discrete/categorical.jl | 6 ++-- .../discrete/discretenonparametric.jl | 2 +- src/utils.jl | 32 ------------------- test/locationscale.jl | 2 +- 10 files changed, 42 insertions(+), 60 deletions(-) diff --git a/src/multivariate/dirichlet.jl b/src/multivariate/dirichlet.jl index afac39e5d..7025a7c0a 100644 --- a/src/multivariate/dirichlet.jl +++ b/src/multivariate/dirichlet.jl @@ -156,14 +156,14 @@ function _rand!(rng::AbstractRNG, for (i, αi) in zip(eachindex(x), d.alpha) @inbounds x[i] = rand(rng, Gamma(αi)) end - multiply!(x, inv(sum(x))) # this returns x + lmul!(inv(sum(x)), x) # this returns x end function _rand!(rng::AbstractRNG, d::Dirichlet{T,<:FillArrays.AbstractFill{T}}, x::AbstractVector{<:Real}) where {T<:Real} rand!(rng, Gamma(FillArrays.getindex_value(d.alpha)), x) - multiply!(x, inv(sum(x))) # this returns x + lmul!(inv(sum(x)), x) # this returns x end ####################################### @@ -232,7 +232,7 @@ function _dirichlet_mle_init2(μ::Vector{Float64}, γ::Vector{Float64}) end α0 /= K - multiply!(μ, α0) + lmul!(α0, μ) end function dirichlet_mle_init(P::AbstractMatrix{Float64}) diff --git a/src/multivariate/dirichletmultinomial.jl b/src/multivariate/dirichletmultinomial.jl index f8a9e400d..4dc0d3fbb 100644 --- a/src/multivariate/dirichletmultinomial.jl +++ b/src/multivariate/dirichletmultinomial.jl @@ -34,12 +34,12 @@ function var(d::DirichletMultinomial{T}) where T <: Real end v end -function cov(d::DirichletMultinomial{T}) where T <: Real +function cov(d::DirichletMultinomial{<:Real}) v = var(d) c = d.α * d.α' - multiply!(c, -d.n * (d.n + d.α0) / (d.α0^2 * (1 + d.α0))) - for i in 1:length(d) - @inbounds c[i, i] = v[i] + lmul!(-d.n * (d.n + d.α0) / (d.α0^2 * (1 + d.α0)), c) + for (i, vi) in zip(diagind(c), v) + @inbounds c[i] = vi end c end diff --git a/src/multivariate/multinomial.jl b/src/multivariate/multinomial.jl index 939cae1a1..b746d6f77 100644 --- a/src/multivariate/multinomial.jl +++ b/src/multivariate/multinomial.jl @@ -232,12 +232,12 @@ end fit_mle(::Type{<:Multinomial}, ss::MultinomialStats) = Multinomial(ss.n, ss.scnts * inv(ss.tw * ss.n)) -function fit_mle(::Type{<:Multinomial}, x::Matrix{T}) where T<:Real +function fit_mle(::Type{<:Multinomial}, x::Matrix{<:Real}) ss = suffstats(Multinomial, x) - Multinomial(ss.n, multiply!(ss.scnts, inv(ss.tw * ss.n))) + Multinomial(ss.n, lmul!(inv(ss.tw * ss.n), ss.scnts)) end -function fit_mle(::Type{<:Multinomial}, x::Matrix{T}, w::Array{Float64}) where T<:Real +function fit_mle(::Type{<:Multinomial}, x::Matrix{<:Real}, w::Array{Float64}) ss = suffstats(Multinomial, x, w) - Multinomial(ss.n, multiply!(ss.scnts, inv(ss.tw * ss.n))) + Multinomial(ss.n, lmul!(inv(ss.tw * ss.n), ss.scnts)) end diff --git a/src/multivariate/mvlognormal.jl b/src/multivariate/mvlognormal.jl index 25cddeae8..4ca573c7f 100644 --- a/src/multivariate/mvlognormal.jl +++ b/src/multivariate/mvlognormal.jl @@ -230,8 +230,11 @@ var(d::MvLogNormal) = diag(cov(d)) entropy(d::MvLogNormal) = length(d)*(1+log2π)/2 + logdetcov(d.normal)/2 + sum(mean(d.normal)) #See https://en.wikipedia.org/wiki/Log-normal_distribution -_rand!(rng::AbstractRNG, d::MvLogNormal, x::AbstractVecOrMat{<:Real}) = - exp!(_rand!(rng, d.normal, x)) +function _rand!(rng::AbstractRNG, d::MvLogNormal, x::AbstractVecOrMat{<:Real}) + _rand!(rng, d.normal, x) + map!(exp, x, x) + return x +end _logpdf(d::MvLogNormal, x::AbstractVecOrMat{T}) where {T<:Real} = insupport(d, x) ? (_logpdf(d.normal, log.(x)) - sum(log.(x))) : -Inf _pdf(d::MvLogNormal, x::AbstractVecOrMat{T}) where {T<:Real} = insupport(d,x) ? _pdf(d.normal, log.(x))/prod(x) : 0.0 diff --git a/src/multivariate/mvnormal.jl b/src/multivariate/mvnormal.jl index 220d3b30a..b19c1f801 100644 --- a/src/multivariate/mvnormal.jl +++ b/src/multivariate/mvnormal.jl @@ -270,15 +270,20 @@ gradlogpdf(d::MvNormal, x::AbstractVector{<:Real}) = -(d.Σ \ (x .- d.μ)) # Sampling (for GenericMvNormal) -_rand!(rng::AbstractRNG, d::MvNormal, x::VecOrMat) = - add!(unwhiten!(d.Σ, randn!(rng, x)), d.μ) +function _rand!(rng::AbstractRNG, d::MvNormal, x::VecOrMat) + unwhiten!(d.Σ, randn!(rng, x)) + x .+= d.μ + return x +end # Workaround: randn! only works for Array, but not generally for AbstractArray function _rand!(rng::AbstractRNG, d::MvNormal, x::AbstractVector) for i in eachindex(x) @inbounds x[i] = randn(rng, eltype(x)) end - add!(unwhiten!(d.Σ, x), d.μ) + unwhiten!(d.Σ, x) + x .+= d.μ + return x end ### Affine transformations @@ -342,7 +347,7 @@ fit_mle(g::MvNormalKnownCov{C}, ss::MvNormalKnownCovStats{C}) where {C<:Abstract function fit_mle(g::MvNormalKnownCov, x::AbstractMatrix{Float64}) d = length(g) size(x,1) == d || throw(DimensionMismatch("Invalid argument dimensions.")) - μ = multiply!(vec(sum(x,dims=2)), inv(size(x,2))) + μ = lmul!(inv(size(x,2)), vec(sum(x,dims=2))) MvNormal(μ, g.Σ) end @@ -448,7 +453,7 @@ function fit_mle(D::Type{DiagNormal}, x::AbstractMatrix{Float64}) @inbounds va[i] += abs2(x[i,j] - mu[i]) end end - multiply!(va, inv(n)) + lmul!(inv(n), va) MvNormal(mu, PDiagMat(va)) end @@ -467,7 +472,7 @@ function fit_mle(D::Type{DiagNormal}, x::AbstractMatrix{Float64}, w::AbstractVec @inbounds va[i] += abs2(x[i,j] - mu[i]) * wj end end - multiply!(va, inv_sw) + lmul!(inv_sw, va) MvNormal(mu, PDiagMat(va)) end diff --git a/src/multivariate/mvnormalcanon.jl b/src/multivariate/mvnormalcanon.jl index 3dd2430b4..1e656657c 100644 --- a/src/multivariate/mvnormalcanon.jl +++ b/src/multivariate/mvnormalcanon.jl @@ -174,7 +174,13 @@ if isdefined(PDMats, :PDSparseMat) unwhiten_winv!(J::PDSparseMat, x::AbstractVecOrMat) = x[:] = J.chol.PtL' \ x end -_rand!(rng::AbstractRNG, d::MvNormalCanon, x::AbstractVector) = - add!(unwhiten_winv!(d.J, randn!(rng,x)), d.μ) -_rand!(rng::AbstractRNG, d::MvNormalCanon, x::AbstractMatrix) = - add!(unwhiten_winv!(d.J, randn!(rng,x)), d.μ) +function _rand!(rng::AbstractRNG, d::MvNormalCanon, x::AbstractVector) + unwhiten_winv!(d.J, randn!(rng, x)) + x .+= d.μ + return x +end +function _rand!(rng::AbstractRNG, d::MvNormalCanon, x::AbstractMatrix) + unwhiten_winv!(d.J, randn!(rng, x)) + x .+= d.μ + return x +end diff --git a/src/univariate/discrete/categorical.jl b/src/univariate/discrete/categorical.jl index 851881172..66384f94e 100644 --- a/src/univariate/discrete/categorical.jl +++ b/src/univariate/discrete/categorical.jl @@ -153,15 +153,15 @@ suffstats(::Type{<:Categorical}, data::CategoricalData, w::AbstractArray{Float64 # Model fitting function fit_mle(::Type{<:Categorical}, ss::CategoricalStats) - Categorical(pnormalize!(ss.h)) + Categorical(normalize!(ss.h, 1)) end function fit_mle(::Type{<:Categorical}, k::Integer, x::AbstractArray{T}) where T<:Integer - Categorical(pnormalize!(add_categorical_counts!(zeros(k), x)), check_args=false) + Categorical(normalize!(add_categorical_counts!(zeros(k), x), 1), check_args=false) end function fit_mle(::Type{<:Categorical}, k::Integer, x::AbstractArray{T}, w::AbstractArray{Float64}) where T<:Integer - Categorical(pnormalize!(add_categorical_counts!(zeros(k), x, w)), check_args=false) + Categorical(normalize!(add_categorical_counts!(zeros(k), x, w), 1), check_args=false) end fit_mle(::Type{<:Categorical}, data::CategoricalData) = fit_mle(Categorical, data...) diff --git a/src/univariate/discrete/discretenonparametric.jl b/src/univariate/discrete/discretenonparametric.jl index e54a0789f..987f351a4 100644 --- a/src/univariate/discrete/discretenonparametric.jl +++ b/src/univariate/discrete/discretenonparametric.jl @@ -307,4 +307,4 @@ end fit_mle(::Type{<:DiscreteNonParametric}, ss::DiscreteNonParametricStats{T,W,Ts,Ws}) where {T,W,Ts,Ws} = - DiscreteNonParametric{T,W,Ts,Ws}(ss.support, pnormalize!(copy(ss.freq)), check_args=false) + DiscreteNonParametric{T,W,Ts,Ws}(ss.support, normalize!(copy(ss.freq), 1), check_args=false) diff --git a/src/utils.jl b/src/utils.jl index a1e7b78b3..490e875fa 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -16,33 +16,12 @@ isunitvec(v::AbstractVector) = (norm(v) - 1.0) < 1.0e-12 isprobvec(p::AbstractVector{<:Real}) = all(x -> x ≥ zero(x), p) && isapprox(sum(p), one(eltype(p))) -pnormalize!(v::AbstractVector{<:Real}) = (v ./= sum(v); v) - -add!(x::AbstractArray, y::AbstractVector) = broadcast!(+, x, x, y) -add!(x::AbstractArray, y::Zeros) = x - -multiply!(x::AbstractArray, c::Number) = (x .*= c; x) - -exp!(x::AbstractArray) = (x .= exp.(x); x) - # get a type wide enough to represent all a distributions's parameters # (if the distribution is parametric) # if the distribution is not parametric, we need this to be a float so that # inplace pdf calculations, etc. allocate storage correctly @inline partype(::Distribution) = Float64 -# for checking the input range of quantile functions -# comparison with NaN is always false, so no explicit check is required -macro checkquantile(p,ex) - p, ex = esc(p), esc(ex) - :(zero($p) <= $p <= one($p) ? $ex : NaN) -end - -macro checkinvlogcdf(lp,ex) - lp, ex = esc(lp), esc(ex) - :($lp <= zero($lp) ? $ex : NaN) -end - # because X == X' keeps failing due to floating point nonsense function isApproxSymmmetric(a::AbstractMatrix{Float64}) tmp = true @@ -54,17 +33,6 @@ function isApproxSymmmetric(a::AbstractMatrix{Float64}) return tmp end -# because isposdef keeps giving the wrong answer for samples -# from Wishart and InverseWisharts -hasCholesky(a::Matrix{Float64}) = isa(trycholesky(a), Cholesky) - -function trycholesky(a::Matrix{Float64}) - try cholesky(a) - catch e - return e - end -end - """ ispossemdef(A, k) -> Bool Test whether a matrix is positive semi-definite with specified rank `k` by diff --git a/test/locationscale.jl b/test/locationscale.jl index a2c7dcebe..68d9c4c6b 100644 --- a/test/locationscale.jl +++ b/test/locationscale.jl @@ -167,7 +167,7 @@ end end test_location_scale_normal(rng, ForwardDiff.Dual(0.3), 0.2, 0.1, 0.2) - probs = Distributions.pnormalize!(rand(10)) + probs = normalize!(rand(10), 1) for _rng in (missing, rng) test_location_scale_discretenonparametric(_rng, 1//3, 1//2, 1:10, probs) test_location_scale_discretenonparametric(_rng, -1//4, 1//3, (-10):(-1), probs) From 7813b836eb51973875ee0a61f730d62b110c6a2a Mon Sep 17 00:00:00 2001 From: Lukas Arnroth Date: Sun, 28 Nov 2021 00:10:19 +0100 Subject: [PATCH 51/58] Add skewed exponential power distribution (#1435) * first commit * rand number generation seems ok * should not be on master * everything checks out * mistakenly modified skewnormal * fixed comments * updated tests and types * Two additional suggestions Co-authored-by: David Widmann --- docs/src/univariate.md | 7 + src/Distributions.jl | 1 + .../continuous/skewedexponentialpower.jl | 122 ++++++++++++++++++ src/univariates.jl | 3 +- test/runtests.jl | 1 + test/skewedexponentialpower.jl | 106 +++++++++++++++ 6 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/univariate/continuous/skewedexponentialpower.jl create mode 100644 test/skewedexponentialpower.jl diff --git a/docs/src/univariate.md b/docs/src/univariate.md index f9f1922a7..08e4b0180 100644 --- a/docs/src/univariate.md +++ b/docs/src/univariate.md @@ -418,6 +418,13 @@ Semicircle plotdensity((-1, 1), Semicircle, (1,)) # hide ``` +```@docs +SkewedExponentialPower +``` +```@example plotdensity +plotdensity((-8, 5), SkewedExponentialPower, (0, 1, 0.7, 0.7)) # hide +``` + ```@docs StudentizedRange SymTriangularDist diff --git a/src/Distributions.jl b/src/Distributions.jl index 798fbe526..3cf0a2812 100644 --- a/src/Distributions.jl +++ b/src/Distributions.jl @@ -144,6 +144,7 @@ export NormalInverseGaussian, Pareto, PGeneralizedGaussian, + SkewedExponentialPower, Product, Poisson, PoissonBinomial, diff --git a/src/univariate/continuous/skewedexponentialpower.jl b/src/univariate/continuous/skewedexponentialpower.jl new file mode 100644 index 000000000..dcbaa3b56 --- /dev/null +++ b/src/univariate/continuous/skewedexponentialpower.jl @@ -0,0 +1,122 @@ +""" + SkewedExponentialPower(μ, σ, p, α) + +The *Skewed exponential power distribution*, with location `μ`, scale `σ`, shape `p`, and skewness `α` +has the probability density function [1] +```math +f(x; \\mu, \\sigma, p, \\alpha) = +\\begin{cases} +\\frac{1}{\\sigma 2p^{1/p}\\Gamma(1+1/p)} \\exp \\left\\{ - \\frac{1}{2p}\\Big| \\frac{x-\\mu}{\\alpha \\sigma} \\Big|^p \\right\\}, & \\text{if } x \\leq \\mu \\\\ +\\frac{1}{\\sigma 2p^{1/p}\\Gamma(1+1/p)} \\exp \\left\\{ - \\frac{1}{2p}\\Big| \\frac{x-\\mu}{(1-\\alpha) \\sigma} \\Big|^p \\right\\}, & \\text{if } x > \\mu +\\end{cases}. +``` +The Skewed exponential power distribution (SEPD) incorporates the Laplace (``p=1, \\alpha=0.5``), +normal (``p=2, \\alpha=0.5``), uniform (``p\\rightarrow \\infty, \\alpha=0.5``), asymmetric Laplace (``p=1``), skew normal (``p=2``), +and exponential power distribution (``\\alpha = 0.5``) as special cases. + +[1] Zhy, D. and V. Zinde-Walsh (2009). Properties and estimation of asymmetric exponential power distribution. _Journal of econometrics_, 148(1):86-96, 2009. + +```julia +SkewedExponentialPower() # SEPD with shape 2, scale 1, location 0, and skewness 0.5 (the standard normal distribution) +SkewedExponentialPower(μ, σ, p, α) # SEPD with location μ, scale σ, shape p, and skewness α +SkewedExponentialPower(μ, σ, p) # SEPD with location μ, scale σ, shape p, and skewness 0.5 (the exponential power distribution) +SkewedExponentialPower(μ, σ) # SEPD with location μ, scale σ, shape 2, and skewness 0.5 (the normal distribution) +SkewedExponentialPower(μ) # SEPD with location μ, scale 1, shape 2, and skewness 0.5 (the normal distribution) + +params(d) # Get the parameters, i.e. (μ, σ, p, α) +shape(d) # Get the shape parameter, i.e. p +location(d) # Get the location parameter, i.e. μ +scale(d) # Get the scale parameter, i.e. σ +``` +""" +struct SkewedExponentialPower{T <: Real} <: ContinuousUnivariateDistribution + μ::T + σ::T + p::T + α::T + SkewedExponentialPower{T}(μ::T, σ::T, p::T, α::T) where {T} = new{T}(μ, σ, p, α) +end + +function SkewedExponentialPower(µ::T, σ::T, p::T, α::T; check_args=true) where {T <: Real} + if check_args + @check_args(SkewedExponentialPower, σ > zero(σ)) + @check_args(SkewedExponentialPower, p > zero(p)) + @check_args(SkewedExponentialPower, zero(α) < α < one(α)) + end + return SkewedExponentialPower{T}(µ, σ, p, α) +end + +function SkewedExponentialPower(μ::Real=0, σ::Real=1, p::Real=2, α::Real=1//2; kwargs...) + return SkewedExponentialPower(promote(μ, σ, p, α)...; kwargs...) +end + +@distr_support SkewedExponentialPower -Inf Inf + +### Conversions +convert(::Type{SkewedExponentialPower{T}}, μ::S, σ::S, p::S, α::S) where {T <: Real, S <: Real} = SkewedExponentialPower(T(μ), T(σ), T(p), T(α)) +convert(::Type{SkewedExponentialPower{T}}, d::SkewedExponentialPower{S}) where {T <: Real, S <: Real} = SkewedExponentialPower(T(d.μ), T(d.σ), T(d.p), T(d.α), check_args=false) +convert(::Type{SkewedExponentialPower{T}}, d::SkewedExponentialPower{T}) where {T<:Real} = d + +### Parameters +@inline partype(d::SkewedExponentialPower{T}) where {T<:Real} = T + +params(d::SkewedExponentialPower) = (d.μ, d.σ, d.p, d.α) +location(d::SkewedExponentialPower) = d.μ +shape(d::SkewedExponentialPower) = d.p +scale(d::SkewedExponentialPower) = d.σ + +### Statistics + +#Calculates the kth central moment of the SEPD +function m_k(d::SkewedExponentialPower, k::Integer) + _, σ, p, α = params(d) + inv_p = inv(p) + return k * (logtwo + inv_p * log(p) + log(σ)) + loggamma((1 + k) * inv_p) - + loggamma(inv_p) + log(abs((-1)^k * α^(1 + k) + (1 - α)^(1 + k))) +end + +# needed for odd moments on log-scale +sgn(d::SkewedExponentialPower) = d.α > 1//2 ? -1 : 1 + +mean(d::SkewedExponentialPower) = d.α == 1//2 ? float(d.μ) : sgn(d)*exp(m_k(d, 1)) + d.μ +mode(d::SkewedExponentialPower) = mean(d) +var(d::SkewedExponentialPower) = exp(m_k(d, 2)) - exp(2*m_k(d, 1)) +skewness(d::SkewedExponentialPower) = d.α == 1//2 ? float(zero(partype(d))) : sgn(d)*exp(m_k(d, 3)) / (std(d))^3 +kurtosis(d::SkewedExponentialPower) = exp(m_k(d, 4))/var(d)^2 - 3 + +function logpdf(d::SkewedExponentialPower, x::Real) + μ, σ, p, α = params(d) + a = x < μ ? α : 1 - α + inv_p = inv(p) + return -(logtwo + log(σ) + inv_p * log(p) + loggamma(1 + inv_p) + inv_p * (abs(μ - x) / (2 * σ * a))^p) +end + +function cdf(d::SkewedExponentialPower, x::Real) + μ, σ, p, α = params(d) + inv_p = inv(p) + if x <= μ + α * ccdf(Gamma(inv_p), inv_p * (abs((x-μ)/σ) / (2*α))^p) + else + α + (1-α) * cdf(Gamma(inv_p), inv_p * (abs((x-μ)/σ) / (2*(1-α)))^p) + end +end + +function quantile(d::SkewedExponentialPower, p::Real) + μ, σ, _, α = params(d) + inv_p = inv(d.p) + if p <= α + μ - 2*α*σ * (d.p * quantile(Gamma(inv_p), 1-p/α))^(inv_p) + else + μ + 2*(1-α)*σ * (d.p * quantile(Gamma(inv_p), 1-(1-p)/(1-α)))^(inv_p) + end +end + +function rand(rng::AbstractRNG, d::SkewedExponentialPower) + μ, σ, p, α = params(d) + inv_p = inv(d.p) + if rand(rng) < d.α + μ - σ * 2*p^(inv_p) * α * rand(Gamma(inv_p, 1))^(inv_p) + else + μ + σ * 2*p^(inv_p) * (1-α) * rand(Gamma(inv_p, 1))^(inv_p) + end +end diff --git a/src/univariates.jl b/src/univariates.jl index 56cd1b06b..19c03c664 100644 --- a/src/univariates.jl +++ b/src/univariates.jl @@ -697,7 +697,8 @@ const continuous_distributions = [ "uniform", "loguniform", # depends on Uniform "vonmises", - "weibull" + "weibull", + "skewedexponentialpower" ] include(joinpath("univariate", "locationscale.jl")) diff --git a/test/runtests.jl b/test/runtests.jl index 728d05443..02729ac8c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -67,6 +67,7 @@ const tests = [ "functionals", "density_interface", "reshaped", + "skewedexponentialpower", ] printstyled("Running tests:\n", color=:blue) diff --git a/test/skewedexponentialpower.jl b/test/skewedexponentialpower.jl new file mode 100644 index 000000000..e6e40c7d6 --- /dev/null +++ b/test/skewedexponentialpower.jl @@ -0,0 +1,106 @@ +using Test +using Distributions + +@testset "SkewedExponentialPower" begin + @testset "α = 0.5" begin + @test_throws ArgumentError SkewedExponentialPower(0, 0, 0, 0) + d1 = SkewedExponentialPower(0, 1, 1, 0.5f0) + @test @inferred partype(d1) == Float32 + d2 = SkewedExponentialPower(0, 1, 1, 0.5) + @test @inferred partype(d2) == Float64 + @test @inferred params(d2) == (0., 1., 1., 0.5) + @test @inferred cdf(d2, Inf) == 1 + @test @inferred cdf(d2, -Inf) == 0 + @test @inferred quantile(d2, 1) == Inf + @test @inferred quantile(d2, 0) == -Inf + + # Comparison to laplace + d = SkewedExponentialPower(0, 1, 1, 0.5) + dl = Laplace(0, 1) + @test @inferred mean(d) ≈ mean(dl) + @test @inferred var(d) ≈ var(dl) + @test @inferred skewness(d) ≈ skewness(dl) + @test @inferred kurtosis(d) ≈ kurtosis(dl) + @test @inferred pdf(d, 0.5) ≈ pdf(dl, 0.5) + @test @inferred cdf(d, 0.5) ≈ cdf(dl, 0.5) + @test @inferred quantile(d, 0.5) ≈ quantile(dl, 0.5) + + # comparison to exponential power distribution (PGeneralizedGaussian), + # where the variance is reparametrized as σₚ = p^(1/p)σ to ensure equal pdfs + p = 1.2 + d = SkewedExponentialPower(0, 1, p, 0.5) + de = PGeneralizedGaussian(0, p^(1/p), p) + @test @inferred mean(d) ≈ mean(de) + @test @inferred var(d) ≈ var(de) + @test @inferred skewness(d) ≈ skewness(de) + @test @inferred kurtosis(d) ≈ kurtosis(de) + @test @inferred pdf(d, 0.5) ≈ pdf(de, 0.5) + @test @inferred cdf(d, 0.5) ≈ cdf(de, 0.5) + + # This is infinite for the PGeneralizedGaussian implementation + d = SkewedExponentialPower(0, 1, 0.01, 0.5) + @test @inferred isfinite(var(d)) + + # Comparison to normal + d = SkewedExponentialPower(0, 1, 2, 0.5) + dn = Normal(0, 1) + @test @inferred mean(d) ≈ mean(dn) + @test @inferred var(d) ≈ var(dn) + @test @inferred skewness(d) ≈ skewness(dn) + @test @inferred isapprox(kurtosis(d), kurtosis(dn), atol = 1/10^10) + @test @inferred pdf(d, 0.5) ≈ pdf(dn, 0.5) + @test @inferred cdf(d, 0.5) ≈ cdf(dn, 0.5) + @test @inferred quantile(d, 0.5) ≈ quantile(dn, 0.5) + end + @testset "α != 0.5" begin + # Format is [x, pdf, cdf] from the asymmetric + # exponential power function in R from package + # VaRES. Values are set to μ = 0, σ = 1, p = 0.5, α = 0.7 + test = [ + -10.0000000 0.004770878 0.02119061; + -9.2631579 0.005831228 0.02508110; + -8.5263158 0.007185598 0.02985598; + -7.7894737 0.008936705 0.03576749; + -7.0526316 0.011232802 0.04315901; + -6.3157895 0.014293449 0.05250781; + -5.5789474 0.018454000 0.06449163; + -4.8421053 0.024246394 0.08010122; + -4.1052632 0.032555467 0.10083623; + -3.3684211 0.044947194 0.12906978; + -2.6315789 0.064438597 0.16879238; + -1.8947368 0.097617334 0.22732053; + -1.1578947 0.162209719 0.32007314; + -0.4210526 0.333932302 0.49013645; + 0.3157895 0.234346966 0.82757893; + 1.0526316 0.070717323 0.92258764; + 1.7894737 0.031620241 0.95773428; + 2.5263158 0.016507946 0.97472202; + 3.2631579 0.009427166 0.98399211; + 4.0000000 0.005718906 0.98942757; + ] + + d = SkewedExponentialPower(0, 1, 0.5, 0.7) + for i ∈ 1:size(test, 1) + @test @inferred isapprox(pdf(d, test[i, 1]), test[i, 2], rtol=1e-3) + @test @inferred isapprox(cdf(d, test[i, 1]), test[i, 3], rtol=1e-3) + end + + # relationship between sepd(μ, σ, p, α) and + # sepd(μ, σ, p, 1-α) + d1 = SkewedExponentialPower(0, 1, 0.1, 0.7) + d2 = SkewedExponentialPower(0, 1, 0.1, 0.3) + @inferred -mean(d1) ≈ mean(d2) + @inferred var(d1) ≈ var(d2) + @inferred -skewness(d1) ≈ skewness(d2) + @inferred kurtosis(d1) ≈ kurtosis(d2) + + α, p = rand(2) + d = SkewedExponentialPower(0, 1, p, α) + # moments of the standard SEPD, Equation 18 in Zhy, D. and V. Zinde-Walsh (2009) + moments = [(2*p^(1/p))^k * ((-1)^k*α^(1+k) + (1-α)^(1+k)) * gamma((1+k)/p)/gamma(1/p) for k ∈ 1:4] + + @inferred var(d) ≈ moments[2] - moments[1]^2 + @inferred skewness(d) ≈ moments[3] / (√(moments[2] - moments[1]^2))^3 + @inferred kurtosis(d) ≈ (moments[4] / ((moments[2] - moments[1]^2))^2 - 3) + end +end From e94d6704386d1f753b28011067af03990303b14c Mon Sep 17 00:00:00 2001 From: David Widmann Date: Sun, 28 Nov 2021 00:11:27 +0100 Subject: [PATCH 52/58] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 701b05f43..95fa6c905 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.33" +version = "0.25.34" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" From d308d9bf5903b643c4dde13011c97ac38f9c0cbf Mon Sep 17 00:00:00 2001 From: David Widmann Date: Wed, 8 Dec 2021 22:10:43 +0100 Subject: [PATCH 53/58] Fix deprecations and test errors on Julia 1.7 (#1448) * Fix deprecations and test errors on Julia 1.7 * Update locationscale.jl --- test/density_interface.jl | 2 +- test/locationscale.jl | 4 ++-- test/matrixreshaped.jl | 6 +++--- test/matrixvariates.jl | 9 +++++---- test/runtests.jl | 1 - 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/density_interface.jl b/test/density_interface.jl index 4a9a00614..a909d8082 100644 --- a/test/density_interface.jl +++ b/test/density_interface.jl @@ -4,7 +4,7 @@ d_uv_continous = Normal(-1.5, 2.3) d_uv_discrete = Poisson(4.7) d_mv = MvNormal([2.3 0.4; 0.4 1.2]) - d_av = Distributions.MatrixReshaped(MvNormal(rand(10)), 2, 5) + d_av = reshape(MvNormal(Diagonal(rand(10))), 2, 5) @testset "Distribution" begin for d in (d_uv_continous, d_uv_discrete, d_mv, d_av) diff --git a/test/locationscale.jl b/test/locationscale.jl index 68d9c4c6b..ff02f7943 100644 --- a/test/locationscale.jl +++ b/test/locationscale.jl @@ -102,8 +102,8 @@ function test_location_scale( @test cdf(d, x) ≈ cdf(dref, x) @test logcdf(d, x) ≈ logcdf(dref, x) - @test ccdf(d, x) ≈ ccdf(dref, x) atol=1e-15 - @test logccdf(d, x) ≈ logccdf(dref, x) atol=1e-15 + @test ccdf(d, x) ≈ ccdf(dref, x) atol=1e-14 + @test logccdf(d, x) ≈ logccdf(dref, x) atol=1e-14 @test quantile(d,0.1) ≈ quantile(dref,0.1) @test quantile(d,0.5) ≈ quantile(dref,0.5) diff --git a/test/matrixreshaped.jl b/test/matrixreshaped.jl index 5ed2ec746..da92ecd2e 100644 --- a/test/matrixreshaped.jl +++ b/test/matrixreshaped.jl @@ -16,9 +16,9 @@ function test_matrixreshaped(rng, d1, sizes) end end @testset "MatrixReshaped constructor errors" begin - @test_throws ArgumentError MatrixReshaped(d1, length(d1), 2) - @test_throws ArgumentError MatrixReshaped(d1, length(d1)) - @test_throws ArgumentError MatrixReshaped(d1, -length(d1), -1) + @test_deprecated(@test_throws ArgumentError MatrixReshaped(d1, length(d1), 2)) + @test_deprecated(@test_throws ArgumentError MatrixReshaped(d1, length(d1))) + @test_deprecated(@test_throws ArgumentError MatrixReshaped(d1, -length(d1), -1)) end @testset "MatrixReshaped size" begin for (d, s) in zip(d1s[1:end-1], sizes[1:end-1]) diff --git a/test/matrixvariates.jl b/test/matrixvariates.jl index 23acb5db7..dae5f6452 100644 --- a/test/matrixvariates.jl +++ b/test/matrixvariates.jl @@ -52,13 +52,14 @@ test_draw(d::MatrixDistribution) = test_draw(d, rand(d)) # Check that sample quantities are close to population quantities # -------------------------------------------------- -function test_draws(d::MatrixDistribution, draws::AbstractArray) - @test isapprox(mean(draws), mean(d), atol = 0.1) - @test isapprox(cov(hcat(vec.(draws)...)'), cov(d) , atol = 0.1) +function test_draws(d::MatrixDistribution, draws::AbstractArray{<:AbstractMatrix}) + @test mean(draws) ≈ mean(d) rtol = 0.01 + draws_matrix = mapreduce(vec, hcat, draws) + @test cov(draws_matrix; dims=2) ≈ cov(d) rtol = 0.1 nothing end -function test_draws(d::LKJ, draws::AbstractArray) +function test_draws(d::LKJ, draws::AbstractArray{<:AbstractMatrix}) @test isapprox(mean(draws), mean(d), atol = 0.1) @test isapprox(var(draws), var(d) , atol = 0.1) nothing diff --git a/test/runtests.jl b/test/runtests.jl index 02729ac8c..18bcff72d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -79,7 +79,6 @@ include("testutils.jl") for t in tests @testset "Test $t" begin - Random.seed!(345679) include("$t.jl") end end From 16ee99b954205d5b185cf730be618491ebbeaa2c Mon Sep 17 00:00:00 2001 From: David Widmann Date: Thu, 9 Dec 2021 13:19:14 +0100 Subject: [PATCH 54/58] Simplify and document `convolve` (#1452) --- Project.toml | 2 +- docs/make.jl | 1 + docs/src/convolution.md | 11 ++++ src/convolution.jl | 63 ++++++++------------- test/convolution.jl | 122 +++++++++++----------------------------- 5 files changed, 68 insertions(+), 131 deletions(-) create mode 100644 docs/src/convolution.md diff --git a/Project.toml b/Project.toml index 95fa6c905..06510bb0b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.34" +version = "0.25.35" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/docs/make.jl b/docs/make.jl index b451eecd2..6b33de4ae 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -16,6 +16,7 @@ makedocs( "reshape.md", "cholesky.md", "mixture.md", + "convolution.md", "fit.md", "extends.md", "density_interface.md", diff --git a/docs/src/convolution.md b/docs/src/convolution.md new file mode 100644 index 000000000..352235675 --- /dev/null +++ b/docs/src/convolution.md @@ -0,0 +1,11 @@ +# Convolutions + +A [convolution of two probability distributions](https://en.wikipedia.org/wiki/List_of_convolutions_of_probability_distributions) +is the probability distribution of the sum of two independent random variables that are +distributed according to these distributions. + +The convolution of two distributions can be constructed with [`convolve`](@ref). + +```@docs +convolve +``` diff --git a/src/convolution.jl b/src/convolution.jl index a5c81737c..6c18b70e4 100644 --- a/src/convolution.jl +++ b/src/convolution.jl @@ -1,25 +1,27 @@ """ - convolve(d1::T, d2::T) where T<:Distribution -> Distribution - -Convolve two distributions of the same type to yield the distribution corresponding to the -sum of independent random variables drawn from the underlying distributions. - -The function is only defined in the cases where the convolution has a closed form as -defined here https://en.wikipedia.org/wiki/List_of_convolutions_of_probability_distributions - -* `Bernoulli` -* `Binomial` -* `NegativeBinomial` -* `Geometric` -* `Poisson` -* `Normal` -* `Cauchy` -* `Chisq` -* `Exponential` -* `Gamma` -* `MultivariateNormal` + convolve(d1::Distribution, d2::Distribution) + +Convolve two distributions and return the distribution corresponding to the sum of +independent random variables drawn from the underlying distributions. + +Currently, the function is only defined in cases where the convolution has a closed form. +More precisely, the function is defined if the distributions of `d1` and `d2` are the same +and one of +* [`Bernoulli`](@ref) +* [`Binomial`](@ref) +* [`NegativeBinomial`](@ref) +* [`Geometric`](@ref) +* [`Poisson`](@ref) +* [`Normal`](@ref) +* [`Cauchy`](@ref) +* [`Chisq`](@ref) +* [`Exponential`](@ref) +* [`Gamma`](@ref) +* [`MvNormal`](@ref) + +External links: [List of convolutions of probability distributions on Wikipedia](https://en.wikipedia.org/wiki/List_of_convolutions_of_probability_distributions) """ -function convolve end +convolve(::Distribution, ::Distribution) # discrete univariate function convolve(d1::Bernoulli, d2::Bernoulli) @@ -61,30 +63,11 @@ function convolve(d1::Gamma, d2::Gamma) end # continuous multivariate -# The first two methods exist for performance reasons to avoid unnecessarily converting -# PDMats to a Matrix -function convolve( - d1::Union{IsoNormal, ZeroMeanIsoNormal, DiagNormal, ZeroMeanDiagNormal}, - d2::Union{IsoNormal, ZeroMeanIsoNormal, DiagNormal, ZeroMeanDiagNormal}, - ) - _check_convolution_shape(d1, d2) - return MvNormal(d1.μ .+ d2.μ, d1.Σ + d2.Σ) -end - -function convolve( - d1::Union{FullNormal, ZeroMeanFullNormal}, - d2::Union{FullNormal, ZeroMeanFullNormal}, - ) - _check_convolution_shape(d1, d2) - return MvNormal(d1.μ .+ d2.μ, d1.Σ.mat + d2.Σ.mat) -end - function convolve(d1::MvNormal, d2::MvNormal) _check_convolution_shape(d1, d2) - return MvNormal(d1.μ .+ d2.μ, Matrix(d1.Σ) + Matrix(d2.Σ)) + return MvNormal(d1.μ + d2.μ, d1.Σ + d2.Σ) end - function _check_convolution_args(p1, p2) p1 ≈ p2 || throw(ArgumentError( "$(p1) !≈ $(p2): distribution parameters must be approximately equal", diff --git a/test/convolution.jl b/test/convolution.jl index bcb5292d8..826e4f82c 100644 --- a/test/convolution.jl +++ b/test/convolution.jl @@ -5,12 +5,10 @@ using LinearAlgebra using Test @testset "discrete univariate" begin - @testset "Bernoulli" begin d1 = Bernoulli(0.1) - d2 = convolve(d1, d1) - - @test isa(d2, Binomial) + d2 = @inferred(convolve(d1, d1)) + @test d2 isa Binomial @test d2.n == 2 @test d2.p == 0.1 @@ -20,30 +18,26 @@ using Test # only works if p1 ≈ p2 d3 = Bernoulli(0.2) @test_throws ArgumentError convolve(d1, d3) - end @testset "Binomial" begin d1 = Binomial(2, 0.1) d2 = Binomial(5, 0.1) - d3 = convolve(d1, d2) - - @test isa(d3, Binomial) + d3 = @inferred(convolve(d1, d2)) + @test d3 isa Binomial @test d3.n == 7 @test d3.p == 0.1 # only works if p1 ≈ p2 d4 = Binomial(2, 0.2) @test_throws ArgumentError convolve(d1, d4) - end @testset "NegativeBinomial" begin d1 = NegativeBinomial(4, 0.1) d2 = NegativeBinomial(1, 0.1) - d3 = convolve(d1, d2) - - isa(d3, NegativeBinomial) + d3 = @inferred(convolve(d1, d2)) + @test d3 isa NegativeBinomial @test d3.r == 5 @test d3.p == 0.1 @@ -51,12 +45,10 @@ using Test @test_throws ArgumentError convolve(d1, d4) end - @testset "Geometric" begin d1 = Geometric(0.2) d2 = convolve(d1, d1) - - @test isa(d2, NegativeBinomial) + @test d2 isa NegativeBinomial @test d2.p == 0.2 # cannot convolve a Geometric with a NegativeBinomial @@ -70,22 +62,18 @@ using Test @testset "Poisson" begin d1 = Poisson(0.1) d2 = Poisson(0.4) - d3 = convolve(d1, d2) - - @test isa(d3, Poisson) + d3 = @inferred(convolve(d1, d2)) + @test d3 isa Poisson @test d3.λ == 0.5 end - end @testset "continuous univariate" begin - @testset "Gaussian" begin d1 = Normal(0.1, 0.2) d2 = Normal(0.25, 1.7) - d3 = convolve(d1, d2) - - @test isa(d3, Normal) + d3 = @inferred(convolve(d1, d2)) + @test d3 isa Normal @test d3.μ == 0.35 @test d3.σ == hypot(0.2, 1.7) end @@ -93,9 +81,8 @@ end @testset "Cauchy" begin d1 = Cauchy(0.2, 0.7) d2 = Cauchy(1.9, 0.8) - d3 = convolve(d1, d2) - - @test isa(d3, Cauchy) + d3 = @inferred(convolve(d1, d2)) + @test d3 isa Cauchy @test d3.μ == 2.1 @test d3.σ == 1.5 end @@ -103,17 +90,15 @@ end @testset "Chisq" begin d1 = Chisq(0.1) d2 = Chisq(0.3) - d3 = convolve(d1, d2) - - @test isa(d3, Chisq) + d3 = @inferred(convolve(d1, d2)) + @test d3 isa Chisq @test d3.ν == 0.4 end @testset "Exponential" begin d1 = Exponential(0.7) - d2 = convolve(d1, d1) - - @test isa(d2, Gamma) + d2 = @inferred(convolve(d1, d1)) + @test d2 isa Gamma @test d2.α == 2 @test d2.θ == 0.7 @@ -128,9 +113,8 @@ end @testset "Gamma" begin d1 = Gamma(0.1, 1.7) d2 = Gamma(0.5, 1.7) - d3 = convolve(d1, d2) - - @test isa(d3, Gamma) + d3 = @inferred(convolve(d1, d2)) + @test d3 isa Gamma @test d3.α == 0.6 @test d3.θ == 1.7 @@ -138,13 +122,10 @@ end d4 = Gamma(1.2, 0.4) @test_throws ArgumentError convolve(d1, d4) end - end @testset "continuous multivariate" begin - @testset "iso-/diag-normal" begin - in1 = MvNormal([1.2, 0.3], 2 * I) in2 = MvNormal([-2.0, 6.9], 0.5 * I) @@ -155,74 +136,35 @@ end dn2 = MvNormal([-3.4, 1.2], Diagonal([3.2, 0.2])) zmdn1 = MvNormal(Diagonal([1.2, 0.3])) - zmdn2 = MvNormal(Diagonal([-0.8, 1.0])) - - dist_list = (in1, in2, zmin1, zmin2, dn1, dn2, zmdn1, zmdn2) - - for (d1, d2) in Iterators.product(dist_list, dist_list) - d3 = convolve(d1, d2) - @test d3 isa Union{IsoNormal,DiagNormal,ZeroMeanIsoNormal,ZeroMeanDiagNormal} - @test d3.μ == d1.μ .+ d2.μ - @test Matrix(d3.Σ) == Matrix(d1.Σ + d2.Σ) # isequal not defined for PDMats - end - - # erroring - in3 = MvNormal([1, 2, 3], 0.2 * I) - @test_throws ArgumentError convolve(in1, in3) - end - - - @testset "full-normal" begin + zmdn2 = MvNormal(Diagonal([0.8, 1.0])) m1 = Symmetric(rand(2,2)) - m1sq = m1^2 - fn1 = MvNormal(ones(2), m1sq.data) + fn1 = MvNormal(ones(2), m1^2) m2 = Symmetric(rand(2,2)) - m2sq = m2^2 - fn2 = MvNormal([2.1, 0.4], m2sq.data) + fn2 = MvNormal([2.1, 0.4], m2^2) m3 = Symmetric(rand(2,2)) - m3sq = m3^2 - zm1 = MvNormal(m3sq.data) + zm1 = MvNormal(m3^2) m4 = Symmetric(rand(2,2)) - m4sq = m4^2 - zm2 = MvNormal(m4sq.data) + zm2 = MvNormal(m4^2) - dist_list = (fn1, fn2, zm1, zm2) + dist_list = (in1, in2, zmin1, zmin2, dn1, dn2, zmdn1, zmdn2, fn1, fn2, zm1, zm2) for (d1, d2) in Iterators.product(dist_list, dist_list) - d3 = convolve(d1, d2) - @test d3 isa Union{FullNormal,ZeroMeanFullNormal} + d3 = @inferred(convolve(d1, d2)) + @test d3 isa MvNormal @test d3.μ == d1.μ .+ d2.μ - @test d3.Σ.mat == d1.Σ.mat + d2.Σ.mat # isequal not defined for PDMats + @test Matrix(d3.Σ) == Matrix(d1.Σ + d2.Σ) # isequal not defined for PDMats end # erroring + in3 = MvNormal([1, 2, 3], 0.2 * I) + @test_throws ArgumentError convolve(in1, in3) + m5 = Symmetric(rand(3, 3)) - m5sq = m5^2 - fn3 = MvNormal(zeros(3), m5sq.data) + fn3 = MvNormal(zeros(3), m5^2) @test_throws ArgumentError convolve(fn1, fn3) end - - @testset "mixed" begin - - in1 = MvNormal([1.2, 0.3], 2 * I) - zmin1 = MvNormal(Zeros(2), 1.9 * I) - dn1 = MvNormal([0.0, 4.7], Diagonal([0.1, 1.8])) - zmdn1 = MvNormal(Diagonal([1.2, 0.3])) - m1 = Symmetric(rand(2, 2)) - m1sq = m1^2 - full = MvNormal(ones(2), m1sq.data) - - dist_list = (in1, zmin1, dn1, zmdn1) - - for (d1, d2) in Iterators.product((full, ), dist_list) - d3 = convolve(d1, d2) - @test isa(d3, MvNormal) - @test d3.μ == d1.μ .+ d2.μ - @test Matrix(d3.Σ) == Matrix(d1.Σ + d2.Σ) # isequal not defined for PDMats - end - end end From 7670bed8b155d351ad518b28dd345aac1feeec36 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Dec 2021 11:41:42 +0100 Subject: [PATCH 55/58] CompatHelper: bump compat for SpecialFunctions to 2, (keep existing compat) (#1434) * CompatHelper: bump compat for SpecialFunctions to 2, (keep existing compat) * Fix test * Update Project.toml * Fix tests Co-authored-by: CompatHelper Julia Co-authored-by: David Widmann Co-authored-by: David Widmann --- Project.toml | 4 ++-- test/dirichletmultinomial.jl | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Project.toml b/Project.toml index 06510bb0b..55e3e8b22 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.35" +version = "0.25.36" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" @@ -25,7 +25,7 @@ DensityInterface = "0.4" FillArrays = "0.9, 0.10, 0.11, 0.12" PDMats = "0.10, 0.11" QuadGK = "2" -SpecialFunctions = "0.8, 0.9, 0.10, 1.0" +SpecialFunctions = "0.8, 0.9, 0.10, 1.0, 2" StatsBase = "0.32, 0.33" StatsFuns = "0.8, 0.9" julia = "1" diff --git a/test/dirichletmultinomial.jl b/test/dirichletmultinomial.jl index 68a5dff15..32b3133ff 100644 --- a/test/dirichletmultinomial.jl +++ b/test/dirichletmultinomial.jl @@ -4,7 +4,6 @@ using Distributions using Test, Random, SpecialFunctions -import SpecialFunctions: factorial Random.seed!(123) rng = MersenneTwister(123) @@ -52,9 +51,9 @@ d = DirichletMultinomial(10, 5) for x in (2 * ones(5), [1, 2, 3, 4, 0], [3.0, 0.0, 3.0, 0.0, 4.0], [0, 0, 0, 0, 10]) @test pdf(d, x) ≈ - factorial(d.n) * gamma(d.α0) / gamma(d.n + d.α0) * prod(gamma.(d.α + x) ./ factorial.(x) ./ gamma.(d.α)) + factorial(d.n) * gamma(d.α0) / gamma(d.n + d.α0) * prod(gamma.(d.α .+ x) ./ (gamma.(x .+ 1) .* gamma.(d.α))) @test logpdf(d, x) ≈ - log(factorial(d.n)) + loggamma(d.α0) - loggamma(d.n + d.α0) + sum(loggamma, d.α + x) - sum(loggamma, d.α) - sum(log.(factorial.(x))) + logfactorial(d.n) + loggamma(d.α0) - loggamma(d.n + d.α0) + sum(loggamma, d.α + x) - sum(loggamma, d.α) - sum(loggamma.(x .+ 1)) end # test Sampling From 8864b314c29925d6cf512965ace6818e600675b5 Mon Sep 17 00:00:00 2001 From: David Widmann Date: Mon, 20 Dec 2021 23:04:26 +0100 Subject: [PATCH 56/58] Add affine transformations of `Normal`, `NormalCanon`, `Laplace`, `Cauchy`, and `Uniform` (#1407) * Add affine transformations of `Normal` * Fix tests * Add affine transformations for `Laplace`, `Cauchy`, and `Uniform` * Simplify implementation * Define affine transformation of `NormalCanon` * Simplify test --- Project.toml | 2 +- src/univariate/continuous/cauchy.jl | 4 + src/univariate/continuous/laplace.jl | 4 + src/univariate/continuous/normal.jl | 5 + src/univariate/continuous/normalcanon.jl | 8 ++ src/univariate/continuous/uniform.jl | 4 + src/univariates.jl | 2 + test/cauchy.jl | 4 + test/laplace.jl | 4 + test/locationscale.jl | 123 ++++++++++------------- test/normal.jl | 4 + test/runtests.jl | 3 + test/testutils.jl | 30 ++++++ test/uniform.jl | 4 + 14 files changed, 132 insertions(+), 69 deletions(-) create mode 100644 test/cauchy.jl create mode 100644 test/laplace.jl create mode 100644 test/uniform.jl diff --git a/Project.toml b/Project.toml index 55e3e8b22..9f09a0376 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Distributions" uuid = "31c24e10-a181-5473-b8eb-7969acd0382f" authors = ["JuliaStats"] -version = "0.25.36" +version = "0.25.37" [deps] ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/src/univariate/continuous/cauchy.jl b/src/univariate/continuous/cauchy.jl index 5afe79d90..525f6f19b 100644 --- a/src/univariate/continuous/cauchy.jl +++ b/src/univariate/continuous/cauchy.jl @@ -101,6 +101,10 @@ end mgf(d::Cauchy{T}, t::Real) where {T<:Real} = t == zero(t) ? one(T) : T(NaN) cf(d::Cauchy, t::Real) = exp(im * (t * d.μ) - d.σ * abs(t)) +#### Affine transformations + +Base.:+(d::Cauchy, c::Real) = Cauchy(d.μ + c, d.σ) +Base.:*(c::Real, d::Cauchy) = Cauchy(c * d.μ, abs(c) * d.σ) #### Fitting diff --git a/src/univariate/continuous/laplace.jl b/src/univariate/continuous/laplace.jl index 5cf1c171d..9a66f7b97 100644 --- a/src/univariate/continuous/laplace.jl +++ b/src/univariate/continuous/laplace.jl @@ -105,6 +105,10 @@ function cf(d::Laplace, t::Real) cis(t * d.μ) / (1+st*st) end +#### Affine transformations + +Base.:+(d::Laplace, c::Real) = Laplace(d.μ + c, d.θ) +Base.:*(c::Real, d::Laplace) = Laplace(c * d.μ, abs(c) * d.θ) #### Sampling diff --git a/src/univariate/continuous/normal.jl b/src/univariate/continuous/normal.jl index fe5feab84..29f23630a 100644 --- a/src/univariate/continuous/normal.jl +++ b/src/univariate/continuous/normal.jl @@ -247,6 +247,11 @@ end mgf(d::Normal, t::Real) = exp(t * d.μ + d.σ^2 / 2 * t^2) cf(d::Normal, t::Real) = exp(im * t * d.μ - d.σ^2 / 2 * t^2) +#### Affine transformations + +Base.:+(d::Normal, c::Real) = Normal(d.μ + c, d.σ) +Base.:*(c::Real, d::Normal) = Normal(c * d.μ, abs(c) * d.σ) + #### Sampling rand(rng::AbstractRNG, d::Normal{T}) where {T} = d.μ + d.σ * randn(rng, float(T)) diff --git a/src/univariate/continuous/normalcanon.jl b/src/univariate/continuous/normalcanon.jl index 10fae8108..a049b5eb6 100644 --- a/src/univariate/continuous/normalcanon.jl +++ b/src/univariate/continuous/normalcanon.jl @@ -80,3 +80,11 @@ invlogccdf(d::NormalCanon, lp::Real) = xval(d, norminvlogccdf(lp)) #### Sampling rand(rng::AbstractRNG, cf::NormalCanon) = cf.μ + randn(rng) / sqrt(cf.λ) + +#### Affine transformations + +function Base.:+(d::NormalCanon, c::Real) + η, λ = params(d) + return NormalCanon(η + c * λ, λ) +end +Base.:*(c::Real, d::NormalCanon) = NormalCanon(d.η / c, d.λ / c^2) diff --git a/src/univariate/continuous/uniform.jl b/src/univariate/continuous/uniform.jl index f18eafd9c..856dbff1c 100644 --- a/src/univariate/continuous/uniform.jl +++ b/src/univariate/continuous/uniform.jl @@ -109,6 +109,10 @@ function cf(d::Uniform, t::Real) cis(v) * (sin(u) / u) end +#### Affine transformations + +Base.:+(d::Uniform, c::Real) = Uniform(d.a + c, d.b + c) +Base.:*(c::Real, d::Uniform) = Uniform(minmax(c * d.a, c * d.b)...) #### Sampling diff --git a/src/univariates.jl b/src/univariates.jl index 19c03c664..a48771212 100644 --- a/src/univariates.jl +++ b/src/univariates.jl @@ -20,6 +20,8 @@ isupperbounded(d::Union{D,Type{D}}) where {D<:UnivariateDistribution} = maximum( hasfinitesupport(d::Union{D,Type{D}}) where {D<:DiscreteUnivariateDistribution} = isbounded(d) hasfinitesupport(d::Union{D,Type{D}}) where {D<:ContinuousUnivariateDistribution} = false +Base.:(==)(r1::RealInterval, r2::RealInterval) = r1.lb == r2.lb && r1.ub == r2.ub + """ params(d::UnivariateDistribution) diff --git a/test/cauchy.jl b/test/cauchy.jl new file mode 100644 index 000000000..2cc7c15f3 --- /dev/null +++ b/test/cauchy.jl @@ -0,0 +1,4 @@ +@testset "cauchy.jl" begin + # affine transformations + test_affine_transformations(Cauchy, randn(), randn()^2) +end diff --git a/test/laplace.jl b/test/laplace.jl new file mode 100644 index 000000000..a551f714a --- /dev/null +++ b/test/laplace.jl @@ -0,0 +1,4 @@ +@testset "laplace.jl" begin + # affine transformations + test_affine_transformations(Laplace, randn(), randn()^2) +end diff --git a/test/locationscale.jl b/test/locationscale.jl index ff02f7943..c96562c81 100644 --- a/test/locationscale.jl +++ b/test/locationscale.jl @@ -27,117 +27,103 @@ function test_location_scale( #### Support / Domain @testset "Support" begin - function test_support(d, dref) - @test minimum(d) == minimum(dref) - @test maximum(d) == maximum(dref) - @test extrema(d) == (minimum(d), maximum(d)) - @test extrema(support(d)) == extrema(d) - if support(d.ρ) isa RealInterval - @test support(d) isa RealInterval - elseif hasfinitesupport(d.ρ) - @test support(d) == d.μ .+ d.σ .* support(d.ρ) - end - end @testset "$k" for (k,dtest) in d_dict - test_support(dtest, dref) + @test minimum(dtest) == minimum(dref) + @test maximum(dtest) == maximum(dref) + @test extrema(dtest) == (minimum(dref), maximum(dref)) + @test extrema(support(dtest)) == extrema(dref) + @test support(dtest) == support(dref) end end #### Promotions and conversions @testset "Promotions and conversions" begin - function test_promotions_and_conversions(d, dref) - @test typeof(d.µ) === typeof(d.σ) - @test location(d) ≈ μ atol=1e-15 - @test scale(d) ≈ σ atol=1e-15 - end @testset "$k" for (k,dtest) in d_dict - test_promotions_and_conversions(dtest, dref) + if dtest isa LocationScale + @test typeof(dtest.μ) === typeof(dtest.σ) + @test location(dtest) ≈ μ atol=1e-15 + @test scale(dtest) ≈ σ atol=1e-15 + end end end #### Statistics @testset "Statistics" begin - function test_statistics(d, dref) - @test mean(d) ≈ mean(dref) - @test median(d) ≈ median(dref) - @test mode(d) ≈ mode(dref) - @test modes(d) ≈ modes(dref) + @testset "$k" for (k,dtest) in d_dict + @test mean(dtest) ≈ mean(dref) + @test median(dtest) ≈ median(dref) + @test mode(dtest) ≈ mode(dref) + @test modes(dtest) ≈ modes(dref) - @test var(d) ≈ var(dref) - @test std(d) ≈ std(dref) + @test var(dtest) ≈ var(dref) + @test std(dtest) ≈ std(dref) - @test skewness(d) ≈ skewness(dref) - @test kurtosis(d) ≈ kurtosis(dref) + @test skewness(dtest) ≈ skewness(dref) + @test kurtosis(dtest) ≈ kurtosis(dref) - @test isplatykurtic(d) == isplatykurtic(dref) - @test isleptokurtic(d) == isleptokurtic(dref) - @test ismesokurtic(d) == ismesokurtic(dref) + @test isplatykurtic(dtest) == isplatykurtic(dref) + @test isleptokurtic(dtest) == isleptokurtic(dref) + @test ismesokurtic(dtest) == ismesokurtic(dref) - @test entropy(d) ≈ entropy(dref) - @test mgf(d,-0.1) ≈ mgf(dref,-0.1) - end - @testset "$k" for (k,dtest) in d_dict - test_statistics(dtest, dref) + @test entropy(dtest) ≈ entropy(dref) + @test mgf(dtest,-0.1) ≈ mgf(dref,-0.1) end end #### Evaluation & Sampling @testset "Evaluation & Sampling" begin - function test_evaluation_and_sampling(rng, d, dref) + @testset "$k" for (k,dtest) in d_dict xs = rand(dref, 5) x = first(xs) - insupport(d, x) == insupport(dref, x) + insupport(dtest, x) == insupport(dref, x) # might return `false` for discrete distributions - insupport(d, -x) == insupport(dref, -x) + insupport(dtest, -x) == insupport(dref, -x) - @test pdf(d, x) ≈ pdf(dref, x) - @test pdf.(d, xs) ≈ pdf.(dref, xs) - @test logpdf(d, x) ≈ logpdf(dref, x) - @test logpdf.(d, xs) ≈ logpdf.(dref, xs) - @test loglikelihood(d, x) ≈ loglikelihood(dref, x) - @test loglikelihood(d, xs) ≈ loglikelihood(dref, xs) + @test pdf(dtest, x) ≈ pdf(dref, x) + @test pdf.(dtest, xs) ≈ pdf.(dref, xs) + @test logpdf(dtest, x) ≈ logpdf(dref, x) + @test logpdf.(dtest, xs) ≈ logpdf.(dref, xs) + @test loglikelihood(dtest, x) ≈ loglikelihood(dref, x) + @test loglikelihood(dtest, xs) ≈ loglikelihood(dref, xs) - @test cdf(d, x) ≈ cdf(dref, x) - @test logcdf(d, x) ≈ logcdf(dref, x) - @test ccdf(d, x) ≈ ccdf(dref, x) atol=1e-14 - @test logccdf(d, x) ≈ logccdf(dref, x) atol=1e-14 + @test cdf(dtest, x) ≈ cdf(dref, x) + @test logcdf(dtest, x) ≈ logcdf(dref, x) + @test ccdf(dtest, x) ≈ ccdf(dref, x) atol=1e-14 + @test logccdf(dtest, x) ≈ logccdf(dref, x) atol=1e-14 - @test quantile(d,0.1) ≈ quantile(dref,0.1) - @test quantile(d,0.5) ≈ quantile(dref,0.5) - @test quantile(d,0.9) ≈ quantile(dref,0.9) + @test quantile(dtest, 0.1) ≈ quantile(dref, 0.1) + @test quantile(dtest, 0.5) ≈ quantile(dref, 0.5) + @test quantile(dtest, 0.9) ≈ quantile(dref, 0.9) - @test cquantile(d,0.1) ≈ cquantile(dref,0.1) - @test cquantile(d,0.5) ≈ cquantile(dref,0.5) - @test cquantile(d,0.9) ≈ cquantile(dref,0.9) + @test cquantile(dtest, 0.1) ≈ cquantile(dref, 0.1) + @test cquantile(dtest, 0.5) ≈ cquantile(dref, 0.5) + @test cquantile(dtest, 0.9) ≈ cquantile(dref, 0.9) - @test invlogcdf(d,log(0.2)) ≈ invlogcdf(dref,log(0.2)) - @test invlogcdf(d,log(0.5)) ≈ invlogcdf(dref,log(0.5)) - @test invlogcdf(d,log(0.8)) ≈ invlogcdf(dref,log(0.8)) + @test invlogcdf(dtest, log(0.2)) ≈ invlogcdf(dref, log(0.2)) + @test invlogcdf(dtest, log(0.5)) ≈ invlogcdf(dref, log(0.5)) + @test invlogcdf(dtest, log(0.8)) ≈ invlogcdf(dref, log(0.8)) - @test invlogccdf(d,log(0.2)) ≈ invlogccdf(dref,log(0.2)) - @test invlogccdf(d,log(0.5)) ≈ invlogccdf(dref,log(0.5)) - @test invlogccdf(d,log(0.8)) ≈ invlogccdf(dref,log(0.8)) + @test invlogccdf(dtest, log(0.2)) ≈ invlogccdf(dref, log(0.2)) + @test invlogccdf(dtest, log(0.5)) ≈ invlogccdf(dref, log(0.5)) + @test invlogccdf(dtest, log(0.8)) ≈ invlogccdf(dref, log(0.8)) - r = Array{float(eltype(d))}(undef, 100000) + r = Array{float(eltype(dtest))}(undef, 100000) if ismissing(rng) - rand!(d,r) + rand!(dtest, r) else - rand!(rng,d,r) + rand!(rng, dtest, r) end @test mean(r) ≈ mean(dref) atol=0.02 @test std(r) ≈ std(dref) atol=0.01 - @test cf(d, -0.1) ≈ cf(dref,-0.1) + @test cf(dtest, -0.1) ≈ cf(dref,-0.1) if dref isa ContinuousDistribution - @test gradlogpdf(d, 0.1) ≈ gradlogpdf(dref, 0.1) + @test gradlogpdf(dtest, 0.1) ≈ gradlogpdf(dref, 0.1) end end - @testset "$k" for (k,dtest) in d_dict - test_evaluation_and_sampling(rng, dtest, dref) - end end end @@ -146,6 +132,7 @@ function test_location_scale_normal( ) ρ = Normal(μD, σD) dref = Normal(μ + σ * μD, σ * σD) + @test dref === μ + σ * ρ return test_location_scale(rng, μ, σ, ρ, dref) end diff --git a/test/normal.jl b/test/normal.jl index 70acf51b5..487dcebba 100644 --- a/test/normal.jl +++ b/test/normal.jl @@ -181,3 +181,7 @@ end @test mean(canonform(Normal(0.25, 0.7))) ≈ 0.25 @test std(canonform(Normal(0.25, 0.7))) ≈ 0.7 end + +# affine transformations +test_affine_transformations(Normal, randn(), randn()^2) +test_affine_transformations(NormalCanon, randn()^2, randn()^2) diff --git a/test/runtests.jl b/test/runtests.jl index 18bcff72d..3506949ce 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,6 +18,9 @@ const tests = [ "truncnormal", "truncated_exponential", "normal", + "laplace", + "cauchy", + "uniform", "lognormal", "mvnormal", "mvlognormal", diff --git a/test/testutils.jl b/test/testutils.jl index 6a8e93bf0..4d773f3d1 100644 --- a/test/testutils.jl +++ b/test/testutils.jl @@ -605,3 +605,33 @@ function pvalue_kolmogorovsmirnoff(x::AbstractVector, d::UnivariateDistribution) # compute asymptotic p-value (see `KSDist`) return ccdf(KSDist(n), dmax) end + +function test_affine_transformations(::Type{T}, params...) where {T<:UnivariateDistribution} + @testset "affine tranformations ($T)" begin + # distribution + d = T(params...) + + # random shift and scale + c = randn() + + # addition + for shift_d in (@inferred(d + c), @inferred(c + d)) + @test shift_d isa T + @test location(shift_d) ≈ location(d) + c + @test scale(shift_d) ≈ scale(d) + end + + # multiplication (negative and positive values) + for s in (-c, c) + for scale_d in (@inferred(s * d), @inferred(d * s), @inferred(d / inv(s))) + @test scale_d isa T + if d isa Uniform + @test location(scale_d) ≈ (s > 0 ? s * minimum(d) : s * maximum(d)) + else + @test location(scale_d) ≈ s * location(d) + end + @test scale(scale_d) ≈ abs(s) * scale(d) + end + end + end +end diff --git a/test/uniform.jl b/test/uniform.jl new file mode 100644 index 000000000..bb728d1c4 --- /dev/null +++ b/test/uniform.jl @@ -0,0 +1,4 @@ +@testset "uniform.jl" begin + # affine transformations + test_affine_transformations(Uniform, rand(), 4 + rand()) +end From 8723fa1fc574197755d68a000fcde1743ad9782b Mon Sep 17 00:00:00 2001 From: Carlos Parada Date: Mon, 20 Dec 2021 17:17:51 -0800 Subject: [PATCH 57/58] Rename LocationScale (#1453) * Rename LocationScale * Revert file names * Update src/Distributions.jl Co-authored-by: David Widmann * Update src/univariate/locationscale.jl Co-authored-by: David Widmann * Update src/univariate/locationscale.jl Co-authored-by: David Widmann * Update src/univariate/locationscale.jl Co-authored-by: David Widmann * Update src/univariate/locationscale.jl Co-authored-by: David Widmann * fix test * Update test/locationscale.jl Co-authored-by: David Widmann * Update src/univariate/locationscale.jl * Fix deprecation * Maybe fix? * Apply suggestions from code review * Type test * extra tests * test_deprecated * Update test/locationscale.jl Co-authored-by: David Widmann * Update test/locationscale.jl Co-authored-by: David Widmann * Update test/locationscale.jl Co-authored-by: David Widmann * Update src/univariate/locationscale.jl Co-authored-by: David Widmann * Update src/univariate/locationscale.jl Co-authored-by: David Widmann * Update src/univariate/locationscale.jl Co-authored-by: David Widmann * Update src/univariate/locationscale.jl Co-authored-by: David Widmann Co-authored-by: David Widmann --- src/matrix/lkj.jl | 2 +- src/matrix/matrixfdist.jl | 2 +- src/matrix/matrixtdist.jl | 2 +- src/univariate/locationscale.jl | 141 +++++++++++++++++--------------- test/locationscale.jl | 12 ++- 5 files changed, 88 insertions(+), 71 deletions(-) diff --git a/src/matrix/lkj.jl b/src/matrix/lkj.jl index 06af0e1f5..3930f2766 100644 --- a/src/matrix/lkj.jl +++ b/src/matrix/lkj.jl @@ -159,7 +159,7 @@ function _marginal(lkj::LKJ) d = lkj.d η = lkj.η α = η + 0.5d - 1 - LocationScale(-1, 2, Beta(α, α)) + AffineDistribution(-1, 2, Beta(α, α)) end # ----------------------------------------------------------------------------- diff --git a/src/matrix/matrixfdist.jl b/src/matrix/matrixfdist.jl index 6086203df..7687cc81a 100644 --- a/src/matrix/matrixfdist.jl +++ b/src/matrix/matrixfdist.jl @@ -157,7 +157,7 @@ function _univariate(d::MatrixFDist) n1, n2, B = params(d) μ = zero(partype(d)) σ = (n1 / n2) * Matrix(B)[1] - return LocationScale(μ, σ, FDist(n1, n2)) + return AffineDistribution(μ, σ, FDist(n1, n2)) end function _rand_params(::Type{MatrixFDist}, elty, n::Int, p::Int) diff --git a/src/matrix/matrixtdist.jl b/src/matrix/matrixtdist.jl index 5e2a0f9a9..d61cf3cc1 100644 --- a/src/matrix/matrixtdist.jl +++ b/src/matrix/matrixtdist.jl @@ -178,7 +178,7 @@ function _univariate(d::MatrixTDist) ν, M, Σ, Ω = params(d) μ = M[1] σ = sqrt( Matrix(Σ)[1] * Matrix(Ω)[1] / ν ) - return LocationScale(μ, σ, TDist(ν)) + return AffineDistribution(μ, σ, TDist(ν)) end _multivariate(d::MatrixTDist) = MvTDist(d) diff --git a/src/univariate/locationscale.jl b/src/univariate/locationscale.jl index ba61c8d90..d2c030d32 100644 --- a/src/univariate/locationscale.jl +++ b/src/univariate/locationscale.jl @@ -1,129 +1,140 @@ """ - LocationScale(μ,σ,ρ) + AffineDistribution(μ, σ, ρ) -A location-scale transformed distribution with location parameter `μ`, -scale parameter `σ`, and given univariate distribution `ρ`. +A shifted and scaled (affinely transformed) version of `ρ`. -If ``Z`` is a random variable with distribution `ρ`, then the distribution of the random -variable +If ``Z`` is a random variable with distribution `ρ`, then `AffineDistribution(μ, σ, ρ)` is +the distribution of the random variable ```math X = μ + σ Z ``` -is the location-scale transformed distribution with location parameter `μ` and scale -parameter `σ`. -If `ρ` is a discrete distribution, the probability mass function of -the transformed distribution is given by +If `ρ` is a discrete univariate distribution, the probability mass function of the +transformed distribution is given by ```math P(X = x) = P\\left(Z = \\frac{x-μ}{σ} \\right). ``` -If `ρ` is a continuous distribution, the probability density function of -the transformed distribution is given by + +If `ρ` is a continuous univariate distribution with probability density function ``f_Z``, +the probability density function of the transformed distribution is given by ```math -f(x) = \\frac{1}{σ} ρ \\! \\left( \\frac{x-μ}{σ} \\right). +f_X(x) = \\frac{1}{|σ|} f_Z\\left( \\frac{x-μ}{σ} \\right). ``` +We recommend against using the `AffineDistribution` constructor directly. Instead, use +`+`, `-`, `*`, and `/`. These are optimized for specific distributions and will fall back +on `AffineDistribution` only when they need to. + +Affine transformations of discrete variables are easily affected by rounding errors. If you +are getting incorrect results, try using exact `Rational` types instead of floats. + ```julia -LocationScale(μ,σ,ρ) # location-scale transformed distribution -params(d) # Get the parameters, i.e. (μ, σ, and the base distribution ρ) -location(d) # Get the location parameter -scale(d) # Get the scale parameter +d = μ + σ * ρ # Create location-scale transformed distribution +params(d) # Get the parameters, i.e. (μ, σ, ρ) ``` - -External links -[Location-Scale family on Wikipedia](https://en.wikipedia.org/wiki/Location%E2%80%93scale_family) """ -struct LocationScale{T<:Real, S<:ValueSupport, D<:UnivariateDistribution{S}} <: UnivariateDistribution{S} +struct AffineDistribution{T<:Real, S<:ValueSupport, D<:UnivariateDistribution{S}} <: UnivariateDistribution{S} μ::T σ::T ρ::D - function LocationScale{T,S,D}(μ::T, σ::T, ρ::D; check_args=true) where {T<:Real, S<:ValueSupport, D<:UnivariateDistribution{S}} - check_args && @check_args(LocationScale, σ > zero(σ)) + function AffineDistribution{T,S,D}(μ::T, σ::T, ρ::D; check_args=true) where {T<:Real, S<:ValueSupport, D<:UnivariateDistribution{S}} + check_args && @check_args(AffineDistribution, σ > zero(σ)) new{T, S, D}(μ, σ, ρ) end end -function LocationScale(μ::T, σ::T, ρ::UnivariateDistribution; check_args=true) where {T<:Real} +function AffineDistribution(μ::T, σ::T, ρ::UnivariateDistribution; check_args::Bool=true) where {T<:Real} _T = promote_type(eltype(ρ), T) D = typeof(ρ) S = value_support(D) - return LocationScale{_T,S,D}(_T(μ), _T(σ), ρ; check_args=check_args) + return AffineDistribution{_T,S,D}(_T(μ), _T(σ), ρ; check_args=check_args) end -LocationScale(μ::Real, σ::Real, ρ::UnivariateDistribution) = LocationScale(promote(μ, σ)..., ρ) +function AffineDistribution(μ::Real, σ::Real, ρ::UnivariateDistribution; check_args::Bool=true) + return AffineDistribution(promote(μ, σ)..., ρ; check_args=check_args) +end # aliases -const ContinuousLocationScale{T<:Real,D<:ContinuousUnivariateDistribution} = LocationScale{T,Continuous,D} -const DiscreteLocationScale{T<:Real,D<:DiscreteUnivariateDistribution} = LocationScale{T,Discrete,D} +const LocationScale{T,S,D} = AffineDistribution{T,S,D} +function LocationScale(μ::Real, σ::Real, ρ::UnivariateDistribution; check_args::Bool=true) + Base.depwarn("`LocationScale` is deprecated, use `AffineDistribution` instead", :LocationScale) + if check_args && σ ≤ 0 # preparation for future PR where I remove σ > 0 check + throw(ArgumentError("σ must be strictly positive.")) + end + return AffineDistribution(μ, σ, ρ; check_args=false) +end + +const ContinuousAffineDistribution{T<:Real,D<:ContinuousUnivariateDistribution} = AffineDistribution{T,Continuous,D} +const DiscreteAffineDistribution{T<:Real,D<:DiscreteUnivariateDistribution} = AffineDistribution{T,Discrete,D} -Base.eltype(::Type{<:LocationScale{T}}) where T = T +Base.eltype(::Type{<:AffineDistribution{T}}) where T = T -minimum(d::LocationScale) = d.μ + d.σ * minimum(d.ρ) -maximum(d::LocationScale) = d.μ + d.σ * maximum(d.ρ) -support(d::LocationScale) = locationscale_support(d.μ, d.σ, support(d.ρ)) -function locationscale_support(μ::Real, σ::Real, support::RealInterval) +minimum(d::AffineDistribution) = d.μ + d.σ * minimum(d.ρ) +maximum(d::AffineDistribution) = d.μ + d.σ * maximum(d.ρ) +support(d::AffineDistribution) = affinedistribution_support(d.μ, d.σ, support(d.ρ)) +function affinedistribution_support(μ::Real, σ::Real, support::RealInterval) return RealInterval(μ + σ * support.lb, μ + σ * support.ub) end -locationscale_support(μ::Real, σ::Real, support) = μ .+ σ .* support +affinedistribution_support(μ::Real, σ::Real, support) = μ .+ σ .* support -LocationScale(μ::Real, σ::Real, d::LocationScale) = LocationScale(μ + d.μ * σ, σ * d.σ, d.ρ) +AffineDistribution(μ::Real, σ::Real, d::AffineDistribution) = AffineDistribution(μ + d.μ * σ, σ * d.σ, d.ρ) #### Conversions -convert(::Type{LocationScale{T}}, μ::Real, σ::Real, ρ::D) where {T<:Real, D<:UnivariateDistribution} = LocationScale(T(μ),T(σ),ρ) -convert(::Type{LocationScale{T}}, d::LocationScale{S}) where {T<:Real, S<:Real} = LocationScale(T(d.μ),T(d.σ),d.ρ, check_args=false) +convert(::Type{AffineDistribution{T}}, μ::Real, σ::Real, ρ::D) where {T<:Real, D<:UnivariateDistribution} = AffineDistribution(T(μ),T(σ),ρ) +convert(::Type{AffineDistribution{T}}, d::AffineDistribution{S}) where {T<:Real, S<:Real} = AffineDistribution(T(d.μ),T(d.σ),d.ρ, check_args=false) #### Parameters -location(d::LocationScale) = d.μ -scale(d::LocationScale) = d.σ -params(d::LocationScale) = (d.μ,d.σ,d.ρ) -partype(::LocationScale{T}) where {T} = T +location(d::AffineDistribution) = d.μ +scale(d::AffineDistribution) = d.σ +params(d::AffineDistribution) = (d.μ,d.σ,d.ρ) +partype(::AffineDistribution{T}) where {T} = T #### Statistics -mean(d::LocationScale) = d.μ + d.σ * mean(d.ρ) -median(d::LocationScale) = d.μ + d.σ * median(d.ρ) -mode(d::LocationScale) = d.μ + d.σ * mode(d.ρ) -modes(d::LocationScale) = d.μ .+ d.σ .* modes(d.ρ) +mean(d::AffineDistribution) = d.μ + d.σ * mean(d.ρ) +median(d::AffineDistribution) = d.μ + d.σ * median(d.ρ) +mode(d::AffineDistribution) = d.μ + d.σ * mode(d.ρ) +modes(d::AffineDistribution) = d.μ .+ d.σ .* modes(d.ρ) -var(d::LocationScale) = d.σ^2 * var(d.ρ) -std(d::LocationScale) = d.σ * std(d.ρ) -skewness(d::LocationScale) = skewness(d.ρ) -kurtosis(d::LocationScale) = kurtosis(d.ρ) +var(d::AffineDistribution) = d.σ^2 * var(d.ρ) +std(d::AffineDistribution) = d.σ * std(d.ρ) +skewness(d::AffineDistribution) = skewness(d.ρ) +kurtosis(d::AffineDistribution) = kurtosis(d.ρ) -isplatykurtic(d::LocationScale) = isplatykurtic(d.ρ) -isleptokurtic(d::LocationScale) = isleptokurtic(d.ρ) -ismesokurtic(d::LocationScale) = ismesokurtic(d.ρ) +isplatykurtic(d::AffineDistribution) = isplatykurtic(d.ρ) +isleptokurtic(d::AffineDistribution) = isleptokurtic(d.ρ) +ismesokurtic(d::AffineDistribution) = ismesokurtic(d.ρ) -entropy(d::ContinuousLocationScale) = entropy(d.ρ) + log(d.σ) -entropy(d::DiscreteLocationScale) = entropy(d.ρ) +entropy(d::ContinuousAffineDistribution) = entropy(d.ρ) + log(d.σ) +entropy(d::DiscreteAffineDistribution) = entropy(d.ρ) -mgf(d::LocationScale,t::Real) = exp(d.μ*t) * mgf(d.ρ,d.σ*t) +mgf(d::AffineDistribution,t::Real) = exp(d.μ*t) * mgf(d.ρ,d.σ*t) #### Evaluation & Sampling -pdf(d::ContinuousLocationScale, x::Real) = pdf(d.ρ,(x-d.μ)/d.σ) / d.σ -pdf(d::DiscreteLocationScale, x::Real) = pdf(d.ρ,(x-d.μ)/d.σ) +pdf(d::ContinuousAffineDistribution, x::Real) = pdf(d.ρ,(x-d.μ)/d.σ) / d.σ +pdf(d::DiscreteAffineDistribution, x::Real) = pdf(d.ρ,(x-d.μ)/d.σ) -logpdf(d::ContinuousLocationScale,x::Real) = logpdf(d.ρ,(x-d.μ)/d.σ) - log(d.σ) -logpdf(d::DiscreteLocationScale, x::Real) = logpdf(d.ρ,(x-d.μ)/d.σ) +logpdf(d::ContinuousAffineDistribution,x::Real) = logpdf(d.ρ,(x-d.μ)/d.σ) - log(d.σ) +logpdf(d::DiscreteAffineDistribution, x::Real) = logpdf(d.ρ,(x-d.μ)/d.σ) for f in (:cdf, :ccdf, :logcdf, :logccdf) - @eval $f(d::LocationScale, x::Real) = $f(d.ρ, (x - d.μ) / d.σ) + @eval $f(d::AffineDistribution, x::Real) = $f(d.ρ, (x - d.μ) / d.σ) end -quantile(d::LocationScale,q::Real) = d.μ + d.σ * quantile(d.ρ,q) +quantile(d::AffineDistribution,q::Real) = d.μ + d.σ * quantile(d.ρ,q) -rand(rng::AbstractRNG, d::LocationScale) = d.μ + d.σ * rand(rng, d.ρ) -cf(d::LocationScale, t::Real) = cf(d.ρ,t*d.σ) * exp(1im*t*d.μ) -gradlogpdf(d::ContinuousLocationScale, x::Real) = gradlogpdf(d.ρ,(x-d.μ)/d.σ) / d.σ +rand(rng::AbstractRNG, d::AffineDistribution) = d.μ + d.σ * rand(rng, d.ρ) +cf(d::AffineDistribution, t::Real) = cf(d.ρ,t*d.σ) * exp(1im*t*d.μ) +gradlogpdf(d::ContinuousAffineDistribution, x::Real) = gradlogpdf(d.ρ,(x-d.μ)/d.σ) / d.σ #### Syntactic sugar for simple transforms of distributions, e.g., d + x, d - x, and so on -Base.:+(d::UnivariateDistribution, x::Real) = LocationScale(x, one(x), d) +Base.:+(d::UnivariateDistribution, x::Real) = AffineDistribution(x, one(x), d) Base.:+(x::Real, d::UnivariateDistribution) = d + x -Base.:*(x::Real, d::UnivariateDistribution) = LocationScale(zero(x), x, d) +Base.:*(x::Real, d::UnivariateDistribution) = AffineDistribution(zero(x), x, d) Base.:*(d::UnivariateDistribution, x::Real) = x * d Base.:-(d::UnivariateDistribution, x::Real) = d + -x Base.:/(d::UnivariateDistribution, x::Real) = inv(x) * d diff --git a/test/locationscale.jl b/test/locationscale.jl index c96562c81..ce05dc04d 100644 --- a/test/locationscale.jl +++ b/test/locationscale.jl @@ -2,11 +2,11 @@ function test_location_scale( rng::Union{AbstractRNG, Missing}, μ::Real, σ::Real, ρ::UnivariateDistribution, dref::UnivariateDistribution, ) - d = LocationScale(μ,σ,ρ) + d = Distributions.AffineDistribution(μ, σ, ρ) @test params(d) == (μ,σ,ρ) @test eltype(d) === eltype(dref) - # Different ways to construct the LocationScale object + # Different ways to construct the AffineDistribution object if dref isa DiscreteDistribution # floating point division introduces numerical errors # Better: multiply with rational numbers @@ -144,7 +144,7 @@ function test_location_scale_discretenonparametric( return test_location_scale(rng, μ, σ, ρ, dref) end -@testset "LocationScale" begin +@testset "AffineDistribution" begin rng = MersenneTwister(123) for _rng in (missing, rng) @@ -160,4 +160,10 @@ end test_location_scale_discretenonparametric(_rng, -1//4, 1//3, (-10):(-1), probs) test_location_scale_discretenonparametric(_rng, 6//5, 3//2, 15:24, probs) end + + @test_logs Distributions.AffineDistribution(1.0, 1, Normal()) + + @test_deprecated ls_norm = LocationScale(1.0, 1, Normal()) + @test ls_norm isa LocationScale{Float64, Continuous, Normal{Float64}} + @test ls_norm isa Distributions.AffineDistribution{Float64, Continuous, Normal{Float64}} end From 93a542d8ce3a818959d604d5268d3ddba2a5af25 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Dec 2021 10:32:15 +0100 Subject: [PATCH 58/58] CompatHelper: bump compat for GR to 0.63 for package docs, (keep existing compat) (#1458) Co-authored-by: CompatHelper Julia --- docs/Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Project.toml b/docs/Project.toml index fe1618933..fab6fae60 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -4,4 +4,4 @@ GR = "28b8d3ca-fb5f-59d9-8090-bfdbd6d07a71" [compat] Documenter = "0.26, 0.27" -GR = "0.61, 0.62" +GR = "0.61, 0.62, 0.63"