Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve logfuncdensity behavior and add isdensity #9

Merged
merged 20 commits into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ DocMeta.setdocmeta!(
:DocTestSetup,
quote
using DensityInterface
d = logfuncdensity(x -> x^2)
d = logfuncdensity(x -> -x^2)
log_f = logdensityof(d)
f = densityof(d)
x = 4
end;
recursive=true,
Expand Down
6 changes: 5 additions & 1 deletion docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@

```@docs
hasdensity
ismeasure
basemeasure
isdensity
logdensityof
logdensityof(::Any)
logfuncdensity
funcdensity
densityof
densityof(::Any)
```


## Types

```@docs
DensityInterface.LogFuncDensity
DensityInterface.FuncDensity
```

## Test utility
Expand Down
4 changes: 3 additions & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ end
DensityInterface
```

!!TODO - update text below in regard to measure theory; add basemeasure to docs text !!

This package defines an interface for mathematical/statistical densities and objects associated with a density in Julia. The interface comprises the functions [`hasdensity`](@ref), [`logdensityof`](@ref)/[`densityof`](@ref)[^1] and [`logfuncdensity`](@ref).

The following methods must be provided to make a type (e.g. `SomeDensity`) compatible with the interface:
Expand Down Expand Up @@ -64,5 +66,5 @@ true

[^1]: The function names `logdensityof` and `densityof` were chosen to convey that the target object may either *be* a density or something that can be said to *have* a density. They also have less naming conflict potential than `logdensity` and esp. `density` (the latter already being exported by Plots.jl).

[^2]: The package [`MeasureTheory`](https://github.com/cscherrer/MeasureTheory.jl) provides tools to work with densities and measures that go beyond the density in
[^2]: !!TODO - Change Text!! The package [`MeasureTheory`](https://github.com/cscherrer/MeasureTheory.jl) provides tools to work with densities and measures that go beyond the density in
respect to an implied base measure.
216 changes: 183 additions & 33 deletions src/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,90 @@


"""
hasdensity(d)::Bool
ismeasure(object)::Bool

Return `true` if `d` is compatible with the `DensityInterface` interface.
Return `true` if `object` is a measure.

`hasdensity(d) == true` implies that `d` is either a density itself or has an
associated density, e.g. a probability density function or a Radon–Nikodym
!!ToDo: More text!!

Defaults to `false`. For types that are measures (not densities), define

```julia
@inline ismeasure(::MyMeasureType) = true
```

The above will automatically provide `hasdensity(::MyMeasureType) == true`.

See also [`hasdensity`](@ref) and [`isdensity`](@ref).
"""
function ismeasure end
export ismeasure

@inline ismeasure(object) = false


"""
hasdensity(object)::Bool

Return `true` if `object` is compatible with the `DensityInterface` interface.

`hasdensity(object) == true` implies that `object` is either a density itself or
has an associated density, e.g. a probability density function or a Radon–Nikodym
derivative with respect to an implicit base measure. It also implies that the
value of that density at given points can be calculated via
[`logdensityof`](@ref) and [`densityof`](@ref).

Defaults to `ismeasure(object)`. For types that are densities, not measures,
define

```julia
@inline hasdensity(::MyDensityType) = true
```

See also [`ismeasure`](@ref) and [`isdensity`](@ref).
"""
function hasdensity end
export hasdensity

@inline hasdensity(::Any) = false
@inline hasdensity(object) = ismeasure(object)

function check_hasdensity(d)
hasdensity(d) || throw(ArgumentError("Object of type $(typeof(d)) is not compatible with DensityInterface"))
end


"""
isdensity(object) = hasdensity(object) && !ismeasure(object)

Return `true` if `object` is a density. Defaults to
`hasdensity(object) && !ismeasure(object)`.

!!ToDo: More text!!

`isdensity` should *not* be specialized, specialize [`ismeasure`](@ref) instead.

See also [`hasdensity`](@ref).
"""
function isdensity end
export isdensity

@inline isdensity(object) = hasdensity(object) && !ismeasure(object)


"""
basemeasure(m)

Return the base measure of measure `m`.

`logdensityof(m, x)` and `densityof(m, x)` return the log/non-log
Radon–Nikodym derivative of `m` with respect to `basemeasure(m)` evaluated at `x`.

!!ToDo: More text!!
"""
function basemeasure end
oschulz marked this conversation as resolved.
Show resolved Hide resolved
export basemeasure


"""
logdensityof(d, x)::Real

Expand Down Expand Up @@ -69,6 +132,50 @@ function logdensityof(d)
end


"""
densityof(d, x)::Real

Compute the value of density `d` or its associated density at a given point
`x`.

```jldoctest a
julia> hasdensity(d)
true

julia> densityof(d, x) == exp(logdensityof(d, x))
true
```

`densityof(d, x)` defaults to `exp(logdensityof(d, x))`, but
may be specialized.

See also [`hasdensity`](@ref) and [`logdensityof`](@ref).
"""
densityof(d, x) = exp(logdensityof(d, x))
export densityof

"""
densityof(d)

Return a function that computes the value of density `d` or its associated
density at a given point.

```jldoctest a
julia> f = densityof(d);

julia> f(x) == densityof(d, x)
true
```

`densityof(d)` defaults to `Base.Fix1(densityof, d)`, but may be specialized.
"""
function densityof(d)
check_hasdensity(d)
Base.Fix1(densityof, d)
end



"""
logfuncdensity(log_f)

Expand All @@ -93,17 +200,25 @@ specialized for the return type of `logfuncdensity` as well.
`logfuncdensity` is the inverse of `logdensityof`, so the following must
hold true:

* `logfuncdensity(logdensityof(d))` is equivalent to `d`
* `logdensityof(logfuncdensity(log_f))` is equivalent to `log_f`.
* `logfuncdensity(logdensityof(object))` is equivalent to `object` in respect to `logdensityof`.
However, it may not be equal to `object`, especially if `ismeasure(object) == true`:
`logfuncdensity` always creates a density, never a measure.
* `logdensityof(logfuncdensity(log_f))` is equivalent (often equal or even
identical to) to `log_f`.

See also [`hasdensity`](@ref).
"""
function logfuncdensity end
export logfuncdensity

logfuncdensity(log_f) = LogFuncDensity(log_f)
@inline logfuncdensity(log_f) = LogFuncDensity(log_f)

logfuncdensity(log_f::Base.Fix1{typeof(logdensityof)}) = log_f.x
# For functions stemming from measures create a density, not a measure:
@inline _logfuncdensity_impl(::Val{true}, log_f::Base.Fix1{typeof(logdensityof)}) = LogFuncDensity(log_f)
# For functions stemming from densities recover original density:
@inline _logfuncdensity_impl(::Val{false}, log_f::Base.Fix1{typeof(logdensityof)}) = log_f.x

@inline logfuncdensity(log_f::Base.Fix1{typeof(logdensityof)}) = _logfuncdensity_impl(Val(ismeasure(log_f.x)), log_f)

InverseFunctions.inverse(::typeof(logfuncdensity)) = logdensityof
InverseFunctions.inverse(::typeof(logdensityof)) = logfuncdensity
Expand All @@ -126,6 +241,9 @@ LogFuncDensity
@inline logdensityof(d::LogFuncDensity, x) = d._log_f(x)
@inline logdensityof(d::LogFuncDensity) = d._log_f

@inline densityof(d::LogFuncDensity, x) = exp(d._log_f(x))
@inline densityof(d::LogFuncDensity) = exp ∘ d._log_f

function Base.show(io::IO, d::LogFuncDensity)
print(io, nameof(typeof(d)), "(")
show(io, d._log_f)
Expand All @@ -135,43 +253,75 @@ end


"""
densityof(d, x)::Real
funcdensity(f)
oschulz marked this conversation as resolved.
Show resolved Hide resolved

Compute the value of density `d` or its associated density at a given point
`x`.
Return a `DensityInterface`-compatible density that is defined by a given
non-log density function `f`:

```jldoctest a
julia> hasdensity(d)
```jldoctest
julia> d = funcdensity(f);

julia> hasdensity(d) == true
true

julia> densityof(d, x) == exp(logdensityof(d, x))
julia> densityof(d, x) == f(x)
true
```

`densityof(d, x)` defaults to `exp(logdensityof(d, x))`, but
may be specialized.
`funcdensity(f)` returns an instance of [`DensityInterface.FuncDensity`](@ref)
by default, but may be specialized to return something else depending on the
type of `f`). If so, [`densityof`](@ref) will typically have to be
specialized for the return type of `funcdensity` as well.

See also [`hasdensity`](@ref) and [`logdensityof`](@ref).
"""
densityof(d, x) = exp(logdensityof(d, x))
export densityof
`funcdensity` is the inverse of `densityof`, so the following must
hold true:

* `funcdensity(densityof(object))` is equivalent to `object` in respect to `densityof`.
However, it may not be equal to `object`, especially if `ismeasure(object) == true`:
`funcdensity` always creates a density, never a measure.
* `densityof(funcdensity(f))` is equivalent (often equal or even
identical to) to `f`.

See also [`hasdensity`](@ref).
"""
densityof(d)
function funcdensity end
export funcdensity

Return a function that computes the value of density `d` or its associated
density at a given point.
@inline funcdensity(f) = FuncDensity(f)

```jldoctest a
julia> f = densityof(d);
# For functions stemming from measures, create a density (not a measure):
@inline _funcdensity_impl(::Val{true}, f::Base.Fix1{typeof(densityof)}) = FuncDensity(f)
# For functions stemming from densities, recover original density:
@inline _funcdensity_impl(::Val{false}, f::Base.Fix1{typeof(densityof)}) = f.x

@inline funcdensity(f::Base.Fix1{typeof(densityof)}) = _funcdensity_impl(Val(ismeasure(f.x)), f)

InverseFunctions.inverse(::typeof(funcdensity)) = densityof
InverseFunctions.inverse(::typeof(densityof)) = funcdensity

julia> f(x) == densityof(d, x)
true
```

`densityof(d)` defaults to `Base.Fix1(densityof, d)`, but may be specialized.
"""
function densityof(d)
check_hasdensity(d)
Base.Fix1(densityof, d)
struct DensityInterface.FuncDensity{F}

Wraps a non-log density function `f` to make it compatible with
`DensityInterface` interface. Typically, `FuncDensity(f)` should not be
called directly, [`funcdensity`](@ref) should be used instead.
"""
struct FuncDensity{F}
_f::F
end
FuncDensity

@inline hasdensity(::FuncDensity) = true

@inline logdensityof(d::FuncDensity, x) = log(d._f(x))
@inline logdensityof(d::FuncDensity) = log ∘ d._f

@inline densityof(d::FuncDensity, x) = d._f(x)
@inline densityof(d::FuncDensity) = d._f

function Base.show(io::IO, d::FuncDensity)
print(io, nameof(typeof(d)), "(")
show(io, d._f)
print(io, ")")
end
Loading