Skip to content

Public Documentation ​

Documentation for DistributionsInference's public interface.

DistributionsInference.DistributionsInference Module
julia
DistributionsInference

The inference layer for the EpiAware composable-modelling stack: a PPL-neutral fit protocol (parameter rows, flat dimension, reconstruct hook) and a LogDensityProblems-based log-density engine, with no probabilistic programming language and no chain type in the core package.

The protocol (parameter_rows, reconstruct), default-prior assembly over it (default_prior, with_priors) and the engine (distribution_to_logdensity, logdensity, FitLogDensity) are implemented here. Everything else arrives through an extension: FlexiChains (the dotted-name readback, distribution_params, point_estimate, distribution_draws), DynamicPPL (distribution_to_turing), DynamicPPL x FlexiChains (the VarName-keyed dispatch of distribution_params/point_estimate/distribution_draws), Bijectors (to_constrained, to_unconstrained and the logdensity_to_objective / objective_to_distribution pair built on them), ComposedDistributions (the fit protocol over a composed tree's own codec), Optim (the minimise step of optimise_distribution) and Mooncake (gradient rules for xlogy/xlog1py). The ModifiedDistributions extension is parked until that package registers in General (#17).

julia
using DistributionsInference
source

Contents ​

Index ​

Public API ​

DistributionsInference.FitLogDensity Type
julia
struct FitLogDensity{D, T, L, FP, ES, CF}

A PPL-neutral log-density over a fit-protocol object's estimated parameters.

FitLogDensity carries everything needed to evaluate the (unnormalised) log-posterior of a fittable object over its estimated flat parameter vector, with no PPL dependency: the template obj, the observed data, a loglik reducer scoring data against the object reconstructed at a draw, and the estimated rows' priors flattened once at construction. Build it with distribution_to_logdensity; evaluate it on a flat vector with logdensity. It also implements the LogDensityProblems interface directly, so it is sampleable by any LogDensityProblems consumer.

Fields

  • obj: the template fittable object (the structure reconstruct rebuilds).

  • data: the observed records scored by loglik.

  • loglik: a reducer (obj, data) -> Real (default sums logpdf(obj, record)).

  • flat_priors: the estimated rows' priors, in parameter_rows order, collected once at construction. An entry is nothing for an estimated row scored instead through extra_logprior (an object-dependent prior; see parameter_rows), which then contributes no per-row term. A tree mixing several prior families makes this vector abstractly typed, costing one dynamic dispatch per row in logdensity.

  • extra_state: extra_prior_state(obj), collected once at construction and threaded into every extra_logprior call (#28).

  • concrete_fields: _concrete_field_candidates(obj), the concrete-field- under-AD guard's structural state, collected once at construction and empty for a properly generic object.

See also


Fields

  • obj::Any

  • data::Any

  • loglik::Any

  • flat_priors::Any

  • extra_state::Any

  • concrete_fields::Any

source
DistributionsInference.default_prior Function
julia
default_prior(
    row
) -> Union{Distributions.Uniform{Float64}, Distributions.Normal, Distributions.Truncated{Distributions.Normal{T}, Distributions.Continuous, T1, _A, Nothing} where {T<:Real, T1<:Real, _A<:Union{Nothing, T1}}}

Pick a default prior for one parameter_rows row, brms-style.

default_prior(row) is with_priors's per-row default for rows the caller does not override. row is a parameter_rows-shaped NamedTuple (; name, value, prior, support); the prior family follows the parameter's own name (the last dotted segment of name, e.g. :shape from Symbol("onset.shape")), not the row's support:

  • a [0, 1]-support row (a simplex/probability parameter) -> Uniform(0, 1).

  • a scale/shape/rate-type name (:sigma, :scale, :shape, :rate, ...) -> truncated(Normal(value, scale); lower = 0), positive by construction.

  • a location name (:mu, :location, a bound) -> Normal(value, scale), unconstrained even under a positive-support row.

  • otherwise, falls back to support: non-negative -> truncated(Normal(value, scale); lower = 0), else Normal(value, scale).

The spread scale is max(abs(value), 1), a weakly-informative width that scales with the parameter's magnitude.

Arguments

Examples

julia
using DistributionsInference

row = (name = Symbol("onset.shape"), value = 2.0, prior = nothing,
    support = (0.0, Inf))
DistributionsInference.default_prior(row)

See also

  • with_priors: assembles a full row set from this default.
source
DistributionsInference.distribution_draws Function
julia
distribution_draws(obj, chain; kwargs...) -> Vector

Read every draw of a dotted-name FlexiChain back onto a fitted object.

distribution_draws(obj, chain) is the vectorised form of point_estimate: where point_estimate reduces the chain to one reconstructed object, distribution_draws keeps every draw, returning a vector of reconstructed objects (one per selected iteration) — e.g. for a per-draw posterior-predictive summary.

This has no method until FlexiChains is loaded; the read lives in the DistributionsInferenceFlexiChainsExt extension. A VarName-keyed chain (one sampled from distribution_to_turing) is read by the DistributionsInferenceDynamicPPLFlexiChainsExt extension, which needs DynamicPPL loaded as well.

Arguments

  • obj: the fittable object the chain's parameters were sampled for.

  • chain: the FlexiChain to read every draw from, keyed by the estimated rows' dotted names.

Keyword Arguments

  • draws: a subset of iterations to keep (a range / index vector, or a predicate over the iteration index); nothing keeps every iteration.

Examples

julia
using DistributionsInference, Distributions
using FlexiChains: FlexiChain, Parameter

struct DrawsLeaf
    shape::Float64
    scale::Float64
end

function DistributionsInference.parameter_rows(d::DrawsLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end
function DistributionsInference.reconstruct(d::DrawsLeaf, x::AbstractVector)
    return DrawsLeaf(x[1], d.scale)
end

leaf = DrawsLeaf(2.0, 1.0)
draws = [2.1, 2.4, 2.0, 2.6]
chain = FlexiChain{Symbol}(
    4, 1, Dict(Parameter(:shape) => reshape(draws, 4, 1)))
length(distribution_draws(leaf, chain))

Not layered on distribution_params

Unlike point_estimate, this does not call distribution_params once per draw, which would re-fetch and re-validate every estimated row's column on each call. It materialises each column once instead, so the two are independent implementations of the same per-draw extraction.

See also

source
DistributionsInference.distribution_params Function
julia
distribution_params(obj, chain; kwargs...) -> NamedTuple

Read a dotted-name FlexiChain's parameter values, keyed by name.

distribution_params(obj, chain) is the params-first readback primitive: the estimated parameter values read from chain, keyed by each estimated_rows(obj) row's dotted name, before any object is rebuilt — a single draw's values, or each row's draws reduced by summary over the draws selection (default: the mean over every draw). point_estimate is a thin layer on top: it collapses this result to a flat vector and calls reconstruct.

This has no method until FlexiChains is loaded; the read lives in the DistributionsInferenceFlexiChainsExt extension. A VarName-keyed chain (one sampled from distribution_to_turing) is read by the DistributionsInferenceDynamicPPLFlexiChainsExt extension, which needs DynamicPPL loaded as well.

Arguments

  • obj: the fittable object the chain's parameters were sampled for.

  • chain: the FlexiChain to read parameter values from, keyed by the estimated rows' dotted names.

Keyword Arguments

  • summary: the reduction AbstractVector -> scalar applied to each row's draws (default mean); ignored when draw is given.

  • draw: a single iteration index to read, overriding summary/draws.

  • draws: a subset of iterations to reduce over (a range / index vector, or a predicate over the iteration index); nothing uses every iteration.

Two estimated rows sharing a dotted name is refused with an ArgumentError naming the duplicate: a NamedTuple cannot key two entries by the same name, and a repeated name means a protocol bug in obj's parameter_rows.

Examples

julia
using DistributionsInference, Distributions
using FlexiChains: FlexiChain, Parameter

struct ParamsLeaf
    shape::Float64
    scale::Float64
end

function DistributionsInference.parameter_rows(d::ParamsLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

leaf = ParamsLeaf(2.0, 1.0)
draws = [2.1, 2.4, 2.0, 2.6]
chain = FlexiChain{Symbol}(
    4, 1, Dict(Parameter(:shape) => reshape(draws, 4, 1)))
distribution_params(leaf, chain)

See also

  • point_estimate: rebuilds obj from this primitive's result.

  • distribution_draws: the vectorised, every-draw form (its own optimised implementation, not layered on this — see its docstring).

source
DistributionsInference.distribution_to_logdensity Function
julia
distribution_to_logdensity(
    obj,
    data;
    loglik
) -> Union{DistributionsInference.FitLogDensity{_A, _B, typeof(DistributionsInference._default_loglik), _C, Nothing, Vector{Tuple{Int64, Symbol, DataType}}} where {_A, _B, _C}, DistributionsInference.FitLogDensity{_A, _B, typeof(DistributionsInference._default_loglik), _C, Vector{Tuple{Tuple, Symbol, ComposedDistributions.Pool}}, Vector{Tuple{Int64, Symbol, DataType}}} where {_A, _B, _C}}

Assemble a FitLogDensity from a fittable object and data.

distribution_to_logdensity(obj, data; loglik) packages the template obj and the observed data into the PPL-neutral log-density spec, reading the priors off obj's parameter_rows (the estimation boundary). The result evaluates the (unnormalised) log-posterior over the estimated flat parameter vector via logdensity, on the constrained scale: each prior is scored directly against its row's value with no Jacobian correction. An object with no estimated rows estimates nothing: the flat vector is empty and logdensity is just the data likelihood. Sampling on the unconstrained scale (the transform and its log-Jacobian) is a Bijectors extension concern.

A conditionally available exact likelihood is a loglik the caller writes and passes in, not a helper this package adds (#44). Choose between the exact and approximate branch with an explicit predicate or by dispatch, never by catching an exception from the exact path, which would hide a genuine bug in the exact branch. Where the exact form does not apply, refuse loudly with a named structural reason, the convention to_constrained and distribution_to_turing follow for a row kind they do not support.

Arguments

  • obj: the template fittable object, carrying its parameter_rows.

  • data: the observed records.

Keyword Arguments

  • loglik: a reducer (obj, data) -> Real scoring data against the reconstructed object (default: sum of logpdf(obj, record)).

Examples

julia
using DistributionsInference, Distributions

struct ToyLeaf
    shape::Float64
    scale::Float64
end

Distributions.logpdf(d::ToyLeaf, y::Real) = logpdf(Gamma(d.shape, d.scale), y)

function DistributionsInference.parameter_rows(d::ToyLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

function DistributionsInference.reconstruct(d::ToyLeaf, x::AbstractVector)
    return ToyLeaf(x[1], d.scale)
end

leaf = ToyLeaf(2.0, 1.0)
data = [1.5, 2.0, 3.2]
prob = distribution_to_logdensity(leaf, data)
DistributionsInference.flat_dimension(leaf)

A conditionally exact likelihood, chosen by predicate and passed straight in as loglik:

julia
using DistributionsInference, Distributions

struct ToyLeaf2
    shape::Float64
    scale::Float64
end

Distributions.logpdf(d::ToyLeaf2, y::Real) = logpdf(Gamma(d.shape, d.scale), y)

function DistributionsInference.parameter_rows(d::ToyLeaf2)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

function DistributionsInference.reconstruct(d::ToyLeaf2, x::AbstractVector)
    return ToyLeaf2(x[1], d.scale)
end

has_exact_form(::ToyLeaf2) = false   # a structural property of the object

function chosen_loglik(obj, data)
    if has_exact_form(obj)
        return sum(y -> logpdf(obj, y), data)  # closed form
    else
        error("no exact likelihood for ToyLeaf2: <named structural reason>")
    end
end

# Decide by an explicit predicate and refuse loudly when it does not apply,
# never by catching an exception from the exact path.
leaf2 = ToyLeaf2(2.0, 1.0)
data2 = [1.5, 2.0, 3.2]
prob2 = distribution_to_logdensity(leaf2, data2; loglik = chosen_loglik)
DistributionsInference.flat_dimension(leaf2)

See also

source
DistributionsInference.distribution_to_turing Function

A DynamicPPL model over a fittable object's estimated parameters.

distribution_to_turing(obj, data) returns a DynamicPPL/Turing model whose free parameters are the estimated parameters of obj (one ~ site per estimated_rows(obj) row, the same flat parameters distribution_to_logdensity exposes), so a fitted posterior is sampleable with sample(distribution_to_turing(obj, data), NUTS(), ...). It is a light wrapper on the distribution_to_logdensity codec: each estimated row is a named ~ site sampled from its own prior, and the data likelihood plus extra_logprior are added with DynamicPPL.@addlogprob! from the codec's reconstruct(obj, θ) scored by loglik. The model's total log-density equals logdensity(distribution_to_logdensity(obj, data), x) at the corresponding constrained x by construction.

The ~ sites are named to match the point_estimate/distribution_draws contract exactly: an estimated row's dotted name (e.g. Symbol("onset.shape")) becomes the VarName <prefix>.onset.shape, so a chain from sample(distribution_to_turing(obj, data), ...; chain_type = FlexiChains.VNChain) reads back through point_estimate/distribution_draws unchanged (that VarName-keyed dispatch lives in the DistributionsInferenceDynamicPPLFlexiChainsExt extension, so it needs FlexiChains loaded too; distribution_to_turing itself does not).

An estimated row with no fixed prior (prior === nothing, scored instead through extra_logprior — an object-dependent prior, e.g. a hierarchical population term; see parameter_rows) has no ~ site to sample it from and is rejected with an ArgumentError. Sample such an object with distribution_to_logdensity + LogDensityProblemsAD (the LogDensityProblems extension) instead.

A gradient-based sampler (e.g. NUTS) evaluates reconstruct at a ForwardDiff.Dual-valued flat vector, so each estimated field of obj's type must be generically typed. A field concretely typed Float64 errors under NUTS; a gradient-free sampler such as AdvancedMH has no such constraint.

This method is available only when DynamicPPL is loaded.

Arguments

  • obj: the template fittable object, carrying its parameter_rows.

  • data: the observed records scored by loglik.

Keyword Arguments

  • prefix: the outer submodel variable name the sites are namespaced under (default :d), matching the readback prefix.

  • loglik: a reducer (obj, data) -> Real scoring data against the reconstructed object (default: sum of logpdf(obj, record)), the same default distribution_to_logdensity uses.

Examples

julia
using DistributionsInference, Distributions, DynamicPPL, Turing, Random
using FlexiChains: FlexiChains, VNChain

struct TuringGammaLeaf{S <: Real}
    shape::S
    scale::Float64
end

Distributions.logpdf(d::TuringGammaLeaf, y::Real) = logpdf(Gamma(d.shape, d.scale), y)

function DistributionsInference.parameter_rows(d::TuringGammaLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

function DistributionsInference.reconstruct(d::TuringGammaLeaf, x::AbstractVector)
    return TuringGammaLeaf(x[1], d.scale)
end

leaf = TuringGammaLeaf(2.0, 1.0)
data = [1.5, 2.0, 3.2]

Random.seed!(1)
chain = sample(distribution_to_turing(leaf, data), NUTS(), 200;
    chain_type = VNChain, progress = false)
fitted = point_estimate(leaf, chain)
fitted.scale  # the fixed parameter, untouched

See also

source
DistributionsInference.estimated_rows Function
julia
estimated_rows(obj) -> Vector

The estimated rows of a fittable object: those with a non-nothing prior.

estimated_rows(obj) filters parameter_rows(obj) to the rows whose prior field is set, in the same order. These are the free parameters the engine estimates; a fixed row (prior === nothing) is excluded. An object with no estimated rows has flat_dimension zero.

Arguments

  • obj: the fittable object.

Examples

julia
using DistributionsInference, Distributions

rows = [(name = :shape, value = 2.0, prior = LogNormal(0.0, 0.2),
        support = (0.0, Inf)),
    (name = :scale, value = 1.0, prior = nothing, support = (0.0, Inf))]
DistributionsInference.estimated_rows(rows)

See also

source
DistributionsInference.extra_logprior Function
julia
extra_logprior(obj, reconstructed, x, state) -> Any

Additional log-prior mass that depends on the reconstructed object.

extra_logprior(obj, reconstructed, x, state) is the neutral extension point for a prior term that cannot be scored per-row against x alone — a hierarchical population term is the motivating case, where a pooled member's log-density depends on the (reconstructed) population hyperparameters, not just on its own flat coordinate. The default returns 0.0: most fittable objects need no such term, since an ordinary per-parameter prior is already scored from parameter_rows(obj)'s prior column in the engine's logdensity. A type with an object-dependent prior overrides this with its own method and gives the corresponding row(s) prior = nothing (see parameter_rows).

Arguments

  • obj: the template fittable object.

  • reconstructed: obj rebuilt at x (i.e. reconstruct(obj, x)), the object the extra term is scored against.

  • x: the estimated flat parameter vector reconstructed was built from.

  • state: extra_prior_state(obj), computed once at construction.

Examples

julia
using DistributionsInference, Distributions

struct PooledPair
    a::Float64
    b::Float64
    mu::Float64
end

function DistributionsInference.parameter_rows(p::PooledPair)
    return [(name = :mu, value = p.mu, prior = Normal(0.0, 1.0),
            support = (-Inf, Inf)),
        (name = :a, value = p.a, prior = nothing, support = (-Inf, Inf)),
        (name = :b, value = p.b, prior = nothing, support = (-Inf, Inf))]
end

function DistributionsInference.reconstruct(p::PooledPair, x::AbstractVector)
    return PooledPair(p.a, p.b, x[1])
end

# a and b share the population Normal(mu, 1): an object-dependent prior,
# scored here rather than per row.
function DistributionsInference.extra_logprior(p::PooledPair, r, x, ::Any)
    return logpdf(Normal(r.mu, 1.0), r.a) + logpdf(Normal(r.mu, 1.0), r.b)
end

DistributionsInference.extra_logprior(
    PooledPair(0.2, -0.1, 0.0), PooledPair(0.2, -0.1, 0.5), [0.5], nothing)

See also

source
DistributionsInference.extra_prior_state Function
julia
extra_prior_state(
    obj
) -> Vector{Tuple{Tuple, Symbol, ComposedDistributions.Pool}}

Structure-dependent state extra_logprior needs, computed once.

extra_prior_state(obj) runs once, when distribution_to_logdensity assembles a FitLogDensity, and the result is threaded into every extra_logprior call for that obj. The default returns nothing, which suits most fittable objects since extra_logprior's default is a constant 0.0.

Override this alongside extra_logprior when computing the extra term needs a walk of obj's own structure (e.g. finding which rows carry an object-dependent prior). That walk depends only on obj, never on the flat vector, so paying it here turns a per-evaluation cost into a one-off.

Arguments

  • obj: the template fittable object.

Examples

julia
using DistributionsInference

struct ToyLeaf
    shape::Float64
    scale::Float64
end

DistributionsInference.extra_prior_state(::ToyLeaf) === nothing

See also

source
DistributionsInference.flat_dimension Function
julia
flat_dimension(obj) -> Any

The estimated parameter dimension of a fittable object.

flat_dimension(obj) is the number of scalar estimated parameters: the count of parameter_rows(obj) rows whose prior is not nothing. An object with no estimated rows has flat dimension 0. It is the length of the flat vector reconstruct consumes and the engine's logdensity evaluates on.

Arguments

  • obj: the fittable object.

Examples

julia
using DistributionsInference, Distributions

rows = [(name = :shape, value = 2.0, prior = LogNormal(0.0, 0.2),
        support = (0.0, Inf)),
    (name = :scale, value = 1.0, prior = nothing, support = (0.0, Inf))]
DistributionsInference.flat_dimension(rows)

See also

source
DistributionsInference.logdensity Function
julia
logdensity(
    prob::DistributionsInference.FitLogDensity,
    x::AbstractVector
) -> Any

Evaluate a FitLogDensity on its estimated flat parameter vector.

logdensity(prob, x) is the (unnormalised) log-posterior at the estimated flat vector x (in parameter_rows(prob.obj) row order restricted to the estimated rows), on the constrained scale: each prior in x is scored directly, with no Jacobian correction (an unconstrained-scale transform is a Bijectors extension concern). The value is the sum of the priors' log-densities at x, plus extra_logprior (an object-dependent prior term; 0.0 unless prob.obj overrides it), plus the data log-likelihood of the object reconstructed there via reconstruct. x is flat_dimension(prob.obj) long — empty when prob.obj estimates nothing, where logdensity is just the data likelihood.

Arguments

Examples

julia
using DistributionsInference, Distributions

struct FitLeaf
    shape::Float64
    scale::Float64
end

Distributions.logpdf(d::FitLeaf, y::Real) = logpdf(Gamma(d.shape, d.scale), y)

function DistributionsInference.parameter_rows(d::FitLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

function DistributionsInference.reconstruct(d::FitLeaf, x::AbstractVector)
    return FitLeaf(x[1], d.scale)
end

leaf = FitLeaf(2.0, 1.0)
data = [1.5, 2.0, 3.2]
prob = distribution_to_logdensity(leaf, data)
DistributionsInference.logdensity(prob, [2.5])

See also

source
DistributionsInference.logdensity_to_objective Function
julia
logdensity_to_objective(
    prob
) -> DistributionsInferenceBijectorsExt.var"#11#12"{DistributionsInference.FitLogDensity{D, T, L, FP, ES, CF}} where {D, T, L, FP, ES, CF}

The negative unconstrained log-posterior, as a plain callable for an external optimiser.

logdensity_to_objective(prob) returns f(z) -> Real: the negative of to_constrained's unconstrained-scale log-target logdensity(prob, x) + logjac at (x, logjac) = to_constrained(prob, z). Because distribution_to_logdensity's objective is already a plain (unnormalised) log-density, minimising f with any standard optimisation package finds a maximum a posteriori point directly. logdensity always scores an estimated row's own prior (that is what makes a row estimated; see parameter_rows), so a genuine maximum likelihood point needs a prior whose curvature is negligible next to the data likelihood rather than a loglik swap alone. The optimiser stays external (Optim.jl, Optimization.jl, or any package that accepts a plain callable and an initial vector); DistributionsInference ships no estimator method itself.

The optimiser's minimiser z_hat becomes a fitted object through objective_to_distribution(prob, z_hat). Reach for this function when the optimiser call is yours to write; optimise_distribution runs the whole round trip instead.

This has no method until Bijectors is loaded; the implementation lives in the DistributionsInferenceBijectorsExt extension.

Arguments

Examples

julia
using DistributionsInference, Distributions, Bijectors

struct OptimLeaf
    shape::Float64
    scale::Float64
end

Distributions.logpdf(d::OptimLeaf, y::Real) = logpdf(Gamma(d.shape, d.scale), y)

function DistributionsInference.parameter_rows(d::OptimLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

function DistributionsInference.reconstruct(d::OptimLeaf, x::AbstractVector)
    return OptimLeaf(x[1], d.scale)
end

leaf = OptimLeaf(2.0, 1.0)
data = [1.5, 2.0, 3.2, 2.8, 1.9]
prob = distribution_to_logdensity(leaf, data)
f = logdensity_to_objective(prob)
# `f` is a plain callable, ready for any external optimiser.
f([0.0])

See also

source
DistributionsInference.minimise Function
julia
minimise(objective, init, optimiser; kwargs...) -> Any

Minimise a plain callable from a starting point, with a given optimiser.

minimise(objective, init, optimiser) returns the minimising vector. It is the one optimiser-package-specific step of optimise_distribution, kept apart so that supporting another optimisation package is a method on this function and nothing else. Keyword arguments are forwarded to the optimiser package's own entry point.

The Optim.jl method (any Optim.AbstractOptimizer, e.g. LBFGS()) lives in the DistributionsInferenceOptimExt extension and has no method until Optim is loaded. Its options keyword takes an Optim.Options, and its autodiff keyword an ADTypes backend.

Set autodiff for any gradient-based optimiser. Optim differentiates by central finite differences unless told otherwise, which spends 2n objective evaluations on every gradient and loses accuracy; logdensity_to_objective's objective differentiates directly, so an AD backend gets the same point for a fraction of the work. The backend's package has to be loaded for ADTypes to dispatch on it.

julia
using DistributionsInference, Bijectors, Optim, ADTypes, ForwardDiff

optimise_distribution(leaf, data, LBFGS(); autodiff = AutoForwardDiff())

Arguments

  • objective: a callable z -> Real to minimise, e.g. the result of logdensity_to_objective.

  • init: the starting vector.

  • optimiser: the optimiser, whose type selects the method.

Examples

julia
using DistributionsInference, Optim

DistributionsInference.minimise(z -> sum(abs2, z .- 2.0), [0.0, 0.0], LBFGS())

See also

source
DistributionsInference.objective_to_distribution Function
julia
objective_to_distribution(prob, z) -> Any

Rebuild a fitted distribution from an optimiser's minimiser.

objective_to_distribution(prob, z) closes the round trip logdensity_to_objective opens: it maps the unconstrained point z an optimiser returned back to the constrained scale with to_constrained and rebuilds a concrete object there via reconstruct. What comes back is the same kind of object prob was assembled from, not a parameter vector.

This has no method until Bijectors is loaded; the implementation lives in the DistributionsInferenceBijectorsExt extension.

Arguments

Examples

julia
using DistributionsInference, Distributions, Bijectors

struct MinimiserLeaf
    shape::Float64
    scale::Float64
end

function Distributions.logpdf(d::MinimiserLeaf, y::Real)
    return logpdf(Gamma(d.shape, d.scale), y)
end

function DistributionsInference.parameter_rows(d::MinimiserLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

function DistributionsInference.reconstruct(d::MinimiserLeaf, x::AbstractVector)
    return MinimiserLeaf(x[1], d.scale)
end

leaf = MinimiserLeaf(2.0, 1.0)
prob = distribution_to_logdensity(leaf, [1.5, 2.0, 3.2])
objective_to_distribution(prob, [0.5]).shape

See also

source
DistributionsInference.optimise_distribution Function
julia
optimise_distribution(
    obj,
    data,
    optimiser;
    loglik,
    init,
    kwargs...
) -> Any

Fit a distribution to data with an optimiser, in one call.

optimise_distribution(obj, data, optimiser) runs the whole point fit and returns a fitted distribution of the same kind as obj: it assembles distribution_to_logdensity(obj, data), starts the optimiser at obj's own parameter values mapped through to_unconstrained, minimises logdensity_to_objective with optimiser via minimise, and maps the minimiser back with objective_to_distribution. A FitLogDensity already to hand is fitted directly with optimise_distribution(prob, optimiser).

The point found is a maximum a posteriori one on the unconstrained scale, since logdensity always scores an estimated row's own prior (that is what makes a row estimated; see parameter_rows) and the objective carries the transform's log-Jacobian. A maximum likelihood point needs a prior whose curvature is negligible next to the data likelihood. An object estimating nothing has nothing to optimise, and comes back as it went in.

The optimiser package stays a caller's choice: optimiser is passed to minimise, whose Optim.jl method lives in the DistributionsInferenceOptimExt extension. The transform needs Bijectors loaded as well.

Arguments

  • obj: the template fittable object, carrying its parameter_rows.

  • data: the observed records.

  • optimiser: the optimiser to minimise with, e.g. Optim.LBFGS().

Keyword Arguments

  • loglik: a reducer (obj, data) -> Real scoring data against the reconstructed object (default: sum of logpdf(obj, record)).

  • init: the starting point on the unconstrained scale (default: obj's own estimated values through to_unconstrained). A template value on the boundary of its prior's support has no unconstrained image, and is rejected with the offending row named rather than passed to the optimiser.

  • other keywords are forwarded to minimise, and from there to the optimiser package. With Optim, options takes an Optim.Options and autodiff an ADTypes backend; see minimise for why a gradient-based fit wants the latter set.

Examples

julia
using DistributionsInference, Distributions, Bijectors, Optim

struct FitLeaf{T <: Real}
    shape::T
    scale::Float64
end

Distributions.logpdf(d::FitLeaf, y::Real) = logpdf(Gamma(d.shape, d.scale), y)

function DistributionsInference.parameter_rows(d::FitLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.5), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

function DistributionsInference.reconstruct(d::FitLeaf, x::AbstractVector)
    return FitLeaf(x[1], d.scale)
end

data = [1.5, 2.0, 3.2, 2.8, 1.9, 4.1, 2.5, 3.0]
fitted = optimise_distribution(FitLeaf(2.0, 1.0), data, LBFGS())
fitted.shape

See also

source
DistributionsInference.parameter_rows Function
julia
parameter_rows(obj) -> Any

The scalar parameter rows of a fittable object.

parameter_rows(obj) returns an iterable of rows, one per scalar parameter of obj, each a NamedTuple with fields:

  • name: a Symbol identifying the parameter (a dotted path for a nested parameter, e.g. Symbol("onset.shape")).

  • value: the parameter's current value.

  • prior: the attached prior (a UnivariateDistribution) if the parameter is estimated, or nothing if it is fixed at value.

  • support: the (lower, upper) bounds of the parameter's admissible domain.

A parameter estimated under an object-dependent prior (e.g. a hierarchical population term whose log-density depends on other reconstructed parameters, not on value alone) also carries prior = nothing at the row level — the same as a fixed parameter — and is scored instead through extra_logprior. This keeps the row schema to exactly these four fields for every parameter kind. A type with such rows must give its own estimated_rows/flat_dimension methods (the generic defaults below treat prior === nothing as fixed and would otherwise drop it from the flat vector).

Every fittable object implements this with its own method; estimated_rows, flat_dimension and the engine's distribution_to_logdensity are built on it. A bare AbstractVector of already-built rows is its own parameter_rows, so a literal row list can stand in for a fittable object without a wrapping type.

Arguments

  • obj: the fittable object.

Examples

julia
using DistributionsInference, Distributions

rows = [(name = :shape, value = 2.0, prior = LogNormal(0.0, 0.2),
        support = (0.0, Inf)),
    (name = :scale, value = 1.0, prior = nothing, support = (0.0, Inf))]
parameter_rows(rows) === rows

See also

source
DistributionsInference.point_estimate Function
julia
point_estimate(obj, chain; kwargs...) -> Any

Read a dotted-name FlexiChain back onto a fitted object.

point_estimate(obj, chain) reduces chain to a flat estimated parameter vector and rebuilds a concrete object via reconstruct: a point summary by default (summary applied to each estimated row's draws, default mean), a single iteration (draw), or a summary restricted to a subset of iterations (draws).

This has no method until FlexiChains is loaded; the read lives in the DistributionsInferenceFlexiChainsExt extension. A VarName-keyed chain (one sampled from distribution_to_turing) is read by the DistributionsInferenceDynamicPPLFlexiChainsExt extension, which needs DynamicPPL loaded as well.

Arguments

  • obj: the fittable object the chain's parameters were sampled for.

  • chain: the FlexiChain to read parameter values from, keyed by the estimated rows' dotted names.

Keyword Arguments

  • summary: the reduction AbstractVector -> scalar applied to each row's draws (default mean); ignored when draw is given.

  • draw: a single iteration index to read, overriding summary/draws.

  • draws: a subset of iterations to reduce over (a range / index vector, or a predicate over the iteration index); nothing uses every iteration.

Examples

julia
using DistributionsInference, Distributions
using FlexiChains: FlexiChain, Parameter

struct ReadbackLeaf
    shape::Float64
    scale::Float64
end

function DistributionsInference.parameter_rows(d::ReadbackLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end
function DistributionsInference.reconstruct(d::ReadbackLeaf, x::AbstractVector)
    return ReadbackLeaf(x[1], d.scale)
end

leaf = ReadbackLeaf(2.0, 1.0)
draws = [2.1, 2.4, 2.0, 2.6]
chain = FlexiChain{Symbol}(
    4, 1, Dict(Parameter(:shape) => reshape(draws, 4, 1)))
point_estimate(leaf, chain).shape

See also

source
DistributionsInference.reconstruct Function
julia
reconstruct(obj, x::AbstractVector) -> Distributions.Gamma

Reconstruct a concrete fittable object from an estimated flat parameter vector.

reconstruct(obj, x) returns a new object of the same kind as obj with each estimated parameter (an estimated_rows(obj) row, in parameter_rows order) taken from x, and every fixed parameter held at its value in obj. x is flat_dimension(obj) long — empty when obj estimates nothing, in which case reconstruct(obj, x) == obj.

This is the companion hook every fittable object implements with its own method, alongside parameter_rows. The engine's logdensity calls it once per evaluation to score prob.data against the object collapsed at x.

An estimated field's type must stay generic (e.g. shape::S, not shape::Float64): a gradient-based sampler threads a tracer number (a ForwardDiff.Dual, a ReverseDiff.TrackedReal, ...) through x, and a concrete field rejects it with an opaque MethodError from inside obj's own constructor. Both call sites (logdensity and the DynamicPPL extension's turing model) guard this ahead of time with a named ArgumentError instead. A Union-typed field (e.g. Union{Float64, Missing}) is a false negative of that guard: isconcretetype is false for a Union, so it reads as already generic even though it cannot hold a tracer either.

Arguments

  • obj: the fittable object whose structure is rebuilt.

  • x: an estimated flat vector of length flat_dimension(obj).

Examples

julia
using DistributionsInference, Distributions

struct DemoLeaf
    shape::Float64
    scale::Float64
end

function DistributionsInference.parameter_rows(d::DemoLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

function DistributionsInference.reconstruct(d::DemoLeaf, x::AbstractVector)
    return DemoLeaf(x[1], d.scale)
end

DistributionsInference.reconstruct(DemoLeaf(2.0, 1.0), [3.5])

See also

source
DistributionsInference.to_constrained Function
julia
to_constrained(prob, z) -> Tuple{Any, Any}

Map an unconstrained vector to the constrained scale and its log-Jacobian.

to_constrained(prob, z) returns (x, logjac): the constrained estimated flat parameters x corresponding to the unconstrained vector z, and the log-determinant Jacobian of that (inverse) transform. The transform is built per row from FitLogDensity's stored flat_priors (each estimated row's Bijectors.bijector(prior) — a positive-support prior pushes through an exp-type link, a simplex-valued prior through a logit/stick-breaking-type link, and so on). The unconstrained log-density a sampler works with is logdensity(prob, x) + logjac.

An estimated row with no per-row prior (prior === nothing, scored instead through extra_logprior — an object-dependent prior, e.g. a hierarchical population term; see parameter_rows) has no distribution to build a bijector from, so it is rejected with a clear ArgumentError, mirroring distribution_to_turing's rejection of the same row kind. A type needing an unconstrained transform for such a row supplies its own to_constrained method.

This has no method until Bijectors is loaded; the prior-driven transform lives in the DistributionsInferenceBijectorsExt extension.

Arguments

Examples

julia
using DistributionsInference, Distributions, Bijectors

struct ConstrainedLeaf
    shape::Float64
    scale::Float64
end

Distributions.logpdf(d::ConstrainedLeaf, y::Real) = logpdf(Gamma(d.shape, d.scale), y)

function DistributionsInference.parameter_rows(d::ConstrainedLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

function DistributionsInference.reconstruct(d::ConstrainedLeaf, x::AbstractVector)
    return ConstrainedLeaf(x[1], d.scale)
end

leaf = ConstrainedLeaf(2.0, 1.0)
data = [1.5, 2.0, 3.2]
prob = distribution_to_logdensity(leaf, data)
# An unconstrained draw maps to the constrained (positive) shape plus a
# log-Jacobian.
x, logjac = DistributionsInference.to_constrained(prob, [0.0])
x

See also

source
DistributionsInference.to_unconstrained Function
julia
to_unconstrained(prob, x) -> Any

Map constrained estimated parameters to the unconstrained scale.

to_unconstrained(prob, x) is the forward direction of to_constrained: the unconstrained vector z whose constrained image is x, built per row from FitLogDensity's stored flat_priors (each estimated row's Bijectors.bijector(prior)). No log-Jacobian comes back, since the value of the transform is what a caller wants here — most often a starting point for an optimiser at the parameter values the template already carries, rather than an arbitrary zero.

An estimated row with no per-row prior (prior === nothing, scored instead through extra_logprior) has no distribution to build a bijector from and is rejected with an ArgumentError, exactly as in to_constrained.

This has no method until Bijectors is loaded; the prior-driven transform lives in the DistributionsInferenceBijectorsExt extension.

Arguments

Examples

julia
using DistributionsInference, Distributions, Bijectors

struct UnconstrainedLeaf
    shape::Float64
    scale::Float64
end

function DistributionsInference.parameter_rows(d::UnconstrainedLeaf)
    return [(name = :shape, value = d.shape,
            prior = LogNormal(log(2.0), 0.2), support = (0.0, Inf)),
        (name = :scale, value = d.scale, prior = nothing,
            support = (0.0, Inf))]
end

leaf = UnconstrainedLeaf(2.0, 1.0)
prob = distribution_to_logdensity(leaf, [1.5, 2.0, 3.2])
# The positive shape maps onto the whole real line.
DistributionsInference.to_unconstrained(prob, [2.0])

See also

source
DistributionsInference.with_priors Function
julia
with_priors(obj; priors, default) -> Any

Assemble a fully-specified row set from a fittable object, brms-style.

with_priors(obj; priors, default) reads parameter_rows(obj) and returns the same row shape with every row's prior field filled: a priors override for that row's dotted name, if given, else the row's own attached prior if it is already set, else default(row) (support-derived, default_prior unless a different default is given). The result is directly usable as obj's replacement row set — e.g. feeding reconstruct/estimated_rows or distribution_to_logdensity through the bare-row-vector identity (parameter_rows(rows) === rows) — so with_priors(obj) alone is the estimate-everything path for any fit-protocol object.

Arguments

  • obj: the fittable object (or a bare row vector).

Keyword Arguments

  • priors: per-parameter overrides, an AbstractDict{Symbol} keyed by a row's dotted name (default: empty).

  • default: a function row -> prior for rows not overridden and not already carrying a prior (default: default_prior).

Examples

julia
using DistributionsInference, Distributions

rows = [(name = :shape, value = 2.0, prior = nothing, support = (0.0, Inf)),
    (name = :scale, value = 1.0, prior = nothing, support = (0.0, Inf))]
priored = with_priors(rows)
priored[1].prior

See also

source