to_distribution and to_submodel
Both functions embed one probabilistic program in another. to_distribution converts a supported model representation into a distribution over its latent variables. to_submodel instead evaluates a DynamicPPL model and yields its return value while recording its latent variables separately.
Currently, to_distribution supports only Stan through BridgeStan; to_submodel accepts only DynamicPPL models.
Stan example
Load BridgeStan and pass Stan source to to_distribution. The first call for a given program and set of options requires a BridgeStan toolchain; identical calls reuse the cached distribution.
using AbstractPPL
using ADTypes: AutoForwardDiff
using BridgeStan, Distributions, DynamicPPL, LogDensityProblems
using ForwardDiff
const STAN = raw"""
parameters {
real location;
real<lower=0> scale;
simplex[3] weights;
ordered[2] cutpoints;
}
model {
location ~ normal(0, 1);
scale ~ lognormal(0, 1);
weights ~ dirichlet(rep_vector(1, 3));
cutpoints ~ normal(0, 2);
}
"""
@model function demo(stan, y)
params ~ to_distribution(stan)
return y ~ Normal(params[1], params[2])
end
ldf = LogDensityFunction(demo(STAN, 0.4))
u = zeros(LogDensityProblems.dimension(ldf))
prepared = AbstractPPL.prepare(
AutoForwardDiff(), u -> LogDensityProblems.logdensity(ldf, u), u
)
logdensity, gradient = AbstractPPL.value_and_gradient!!(prepared, u)(-8.239370568253582, [0.4, -0.8399999999999999, 0.0, 0.0, -0.25, 0.75])The left-hand side receives Stan's constrained parameters as a flat vector in declaration order. In this example, params[1] is location and params[2] is scale. The data and seed keywords configure model construction; stanc_args and make_args configure the build. BridgeStan supplies no sampler, so InitFromPrior uses DynamicPPL's uniform initializer in unconstrained space.
API
DynamicPPL.to_distribution — Function
to_distribution(model)Convert model to a distribution for use on the right-hand side of ~.
In variables ~ to_distribution(model), variables receives the latent variables represented by the resulting distribution. The concrete representation and density depend on the method for typeof(model). This differs from to_submodel, which assigns a wrapped model's return value to the left-hand side and records its latent variables separately.
DynamicPPL.to_submodel — Function
to_submodel(model::Model[, auto_prefix::Bool])Wrap model for use on the right-hand side of ~.
In value ~ to_submodel(model), model is evaluated, its return value is assigned to value, and its latent variables are recorded separately in the surrounding trace. By default, their names are prefixed with the left-hand side: a latent variable x becomes value.x. This differs from to_distribution, which assigns the represented latent variables themselves to the left-hand side.
Conceptually, to_submodel(model) is a returned_value(model) wrapper: its value is the model's return value, not its latent variables.
Submodel is not a Distribution; it provides this tilde behavior but no standalone logpdf method.
Operations normally associated with left ~ right, such as condition, do not necessarily work with to_submodel.
Keep auto_prefix=true unless the wrapped model has been explicitly prefixed. Disabling automatic prefixing can make latent-variable names collide.
Arguments
model::Model: the model to wrap.auto_prefix::Bool=true: whether to prefix the model's latent variables with the left-hand side of~.
Examples
julia> using DynamicPPL, Distributions
julia> @model function demo1(x)
x ~ Normal()
return 1 + abs(x)
end;
julia> @model function demo2(x, y)
a ~ to_submodel(demo1(x))
return y ~ Uniform(0, a)
end;When sampling from demo2(missing, 0.4), the latent variable x is prefixed with a, the left-hand side of the tilde:
julia> model = demo2(missing, 0.4);
julia> haskey(rand(model), @varname(a.x))
trueThe variable a receives the return value of demo1 and can be used in subsequent lines, as in the definition of y above.
We can verify that the log joint probability of the model accumulated in vi is correct:
julia> accs = setacc!!(OnlyAccsVarInfo(), RawValueAccumulator(false));
julia> _, accs = init!!(model, accs, InitFromPrior(), UnlinkAll());
julia> x = get_raw_values(accs)[@varname(a.x)];
julia> getlogjoint(accs) ≈ logpdf(Normal(), x) + logpdf(Uniform(0, 1 + abs(x)), 0.4)
trueWithout automatic prefixing
If auto_prefix=false, the submodel's latent-variable names are unchanged.
julia> @model function demo1(x)
x ~ Normal()
return 1 + abs(x)
end;
julia> @model function demo2_no_prefix(x, z)
a ~ to_submodel(demo1(x), false)
return z ~ Uniform(-a, 1)
end;
julia> model = demo2_no_prefix(missing, 0.4);
julia> haskey(rand(model), @varname(x)) # here we just use `x` instead of `a.x`
trueHowever, not using prefixing is generally not recommended as it can lead to variable name clashes unless one is careful. For example, if the same submodel is used multiple times in a model, not using prefixing will lead to variable name clashes.
One can manually specify a prefix using prefix(::Model, prefix_varname):
julia> @model function demo2(x, y, z)
a ~ to_submodel(prefix(demo1(x), @varname(sub1)), false)
b ~ to_submodel(prefix(demo1(y), @varname(sub2)), false)
return z ~ Uniform(-a, b)
end;
julia> model = demo2(missing, missing, 0.4);
julia> haskey(rand(model), @varname(sub1.x))
true
julia> haskey(rand(model), @varname(sub2.x))
trueDynamicPPL.prefix — Function
prefix(ctx::AbstractContext, vn::VarName)Apply the prefixes in the context ctx to the variable name vn.
prefix(model::Model, x::VarName)
prefix(model::Model, x::Val{sym})
prefix(model::Model, x::Any)Return model but with all random variables prefixed by x, where x is either:
- a
VarName(e.g.@varname(a)), - a
Val{sym}(e.g.Val(:a)), or - for any other type,
xis converted to a Symbol and then to aVarName. Note that this will introduce runtime overheads so is not recommended unless absolutely necessary.
Examples
julia> using DynamicPPL: prefix
julia> @model demo() = x ~ Dirac(1)
demo (generic function with 2 methods)
julia> rand(prefix(demo(), @varname(my_prefix)))
VarNamedTuple
└─ my_prefix => VarNamedTuple
└─ x => 1
julia> rand(prefix(demo(), Val(:my_prefix)))
VarNamedTuple
└─ my_prefix => VarNamedTuple
└─ x => 1