diff --git a/HISTORY.md b/HISTORY.md index 9edac441f..d559e6373 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,10 @@ **Breaking changes** +### Submodel macro + +The `@submodel` macro is fully removed; please use `to_submodel` instead. + ### Accumulators This release overhauls how VarInfo objects track variables such as the log joint probability. The new approach is to use what we call accumulators: Objects that the VarInfo carries on it that may change their state at each `tilde_assume!!` and `tilde_observe!!` call based on the value of the variable in question. They replace both variables that were previously hard-coded in the `VarInfo` object (`logp` and `num_produce`) and some contexts. This brings with it a number of breaking changes: diff --git a/docs/src/api.md b/docs/src/api.md index 32b3d80a6..886d34a2f 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -146,12 +146,6 @@ to_submodel Note that a `[to_submodel](@ref)` is only sampleable; one cannot compute `logpdf` for its realizations. -In the past, one would instead embed sub-models using [`@submodel`](@ref), which has been deprecated since the introduction of [`to_submodel(model)`](@ref) - -```@docs -@submodel -``` - In the context of including models within models, it's also useful to prefix the variables in sub-models to avoid variable names clashing: ```@docs diff --git a/src/DynamicPPL.jl b/src/DynamicPPL.jl index 4bd4f2529..2b4d0e4a6 100644 --- a/src/DynamicPPL.jl +++ b/src/DynamicPPL.jl @@ -128,7 +128,6 @@ export AbstractVarInfo, to_submodel, # Convenience macros @addlogprob!, - @submodel, value_iterator_from_chain, check_model, check_model_and_trace, @@ -172,6 +171,7 @@ abstract type AbstractVarInfo <: AbstractModelTrace end include("utils.jl") include("chains.jl") include("model.jl") +include("submodel.jl") include("sampler.jl") include("varname.jl") include("distribution_wrappers.jl") @@ -186,7 +186,6 @@ include("simple_varinfo.jl") include("context_implementations.jl") include("compiler.jl") include("pointwise_logdensities.jl") -include("submodel_macro.jl") include("transforming.jl") include("logdensityfunction.jl") include("model_utils.jl") diff --git a/src/model.jl b/src/model.jl index f46137ed1..27551bfa2 100644 --- a/src/model.jl +++ b/src/model.jl @@ -1265,243 +1265,3 @@ end function returned(model::Model, values, keys) return returned(model, NamedTuple{keys}(values)) end - -""" - is_rhs_model(x) - -Return `true` if `x` is a model or model wrapper, and `false` otherwise. -""" -is_rhs_model(x) = false - -""" - Distributional - -Abstract type for type indicating that something is "distributional". -""" -abstract type Distributional end - -""" - should_auto_prefix(distributional) - -Return `true` if the `distributional` should use automatic prefixing, and `false` otherwise. -""" -function should_auto_prefix end - -""" - is_rhs_model(x) - -Return `true` if the `distributional` is a model, and `false` otherwise. -""" -function is_rhs_model end - -""" - Sampleable{M} <: Distributional - -A wrapper around a model indicating it is sampleable. -""" -struct Sampleable{M,AutoPrefix} <: Distributional - model::M -end - -should_auto_prefix(::Sampleable{<:Any,AutoPrefix}) where {AutoPrefix} = AutoPrefix -is_rhs_model(x::Sampleable) = is_rhs_model(x.model) - -# TODO: Export this if it end up having a purpose beyond `to_submodel`. -""" - to_sampleable(model[, auto_prefix]) - -Return a wrapper around `model` indicating it is sampleable. - -# Arguments -- `model::Model`: the model to wrap. -- `auto_prefix::Bool`: whether to prefix the variables in the model. Default: `true`. -""" -to_sampleable(model, auto_prefix::Bool=true) = Sampleable{typeof(model),auto_prefix}(model) - -""" - rand_like!!(model_wrap, context, varinfo) - -Returns a tuple with the first element being the realization and the second the updated varinfo. - -# Arguments -- `model_wrap::ReturnedModelWrapper`: the wrapper of the model to use. -- `context::AbstractContext`: the context to use for evaluation. -- `varinfo::AbstractVarInfo`: the varinfo to use for evaluation. - """ -function rand_like!!( - model_wrap::Sampleable, context::AbstractContext, varinfo::AbstractVarInfo -) - return rand_like!!(model_wrap.model, context, varinfo) -end - -""" - ReturnedModelWrapper - -A wrapper around a model indicating it is a model over its return values. - -This should rarely be constructed explicitly; see [`returned(model)`](@ref) instead. -""" -struct ReturnedModelWrapper{M<:Model} - model::M -end - -is_rhs_model(::ReturnedModelWrapper) = true - -function rand_like!!( - model_wrap::ReturnedModelWrapper, context::AbstractContext, varinfo::AbstractVarInfo -) - # Return's the value and the (possibly mutated) varinfo. - return _evaluate!!(model_wrap.model, varinfo, context) -end - -""" - returned(model) - -Return a `model` wrapper indicating that it is a model over its return-values. -""" -returned(model::Model) = ReturnedModelWrapper(model) - -""" - to_submodel(model::Model[, auto_prefix::Bool]) - -Return a model wrapper indicating that it is a sampleable model over the return-values. - -This is mainly meant to be used on the right-hand side of a `~` operator to indicate that -the model can be sampled from but not necessarily evaluated for its log density. - -!!! warning - Note that some other operations that one typically associate with expressions of the form - `left ~ right` such as [`condition`](@ref), will also not work with `to_submodel`. - -!!! warning - To avoid variable names clashing between models, it is recommend leave argument `auto_prefix` equal to `true`. - If one does not use automatic prefixing, then it's recommended to use [`prefix(::Model, input)`](@ref) explicitly. - -# Arguments -- `model::Model`: the model to wrap. -- `auto_prefix::Bool`: whether to automatically prefix the variables in the model using the left-hand - side of the `~` statement. Default: `true`. - -# Examples - -## Simple example -```jldoctest submodel-to_submodel; setup=:(using 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 we sample from the model `demo2(missing, 0.4)` random variable `x` will be sampled: -```jldoctest submodel-to_submodel -julia> vi = VarInfo(demo2(missing, 0.4)); - -julia> @varname(a.x) in keys(vi) -true -``` - -The variable `a` is not tracked. However, it will be assigned the return value of `demo1`, -and can be used in subsequent lines of the model, as shown above. -```jldoctest submodel-to_submodel -julia> @varname(a) in keys(vi) -false -``` - -We can check that the log joint probability of the model accumulated in `vi` is correct: - -```jldoctest submodel-to_submodel -julia> x = vi[@varname(a.x)]; - -julia> getlogjoint(vi) ≈ logpdf(Normal(), x) + logpdf(Uniform(0, 1 + abs(x)), 0.4) -true -``` - -## Without automatic prefixing -As mentioned earlier, by default, the `auto_prefix` argument specifies whether to automatically -prefix the variables in the submodel. If `auto_prefix=false`, then the variables in the submodel -will not be prefixed. -```jldoctest submodel-to_submodel-prefix; setup=:(using Distributions) -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> vi = VarInfo(demo2_no_prefix(missing, 0.4)); - -julia> @varname(x) in keys(vi) # here we just use `x` instead of `a.x` -true -``` -However, not using prefixing is generally not recommended as it can lead to variable name clashes -unless one is careful. For example, if we're re-using the same model twice in a model, not using prefixing -will lead to variable name clashes: However, one can manually prefix using the [`prefix(::Model, input)`](@ref): -```jldoctest submodel-to_submodel-prefix -julia> @model function demo2(x, y, z) - a ~ to_submodel(prefix(demo1(x), :sub1), false) - b ~ to_submodel(prefix(demo1(y), :sub2), false) - return z ~ Uniform(-a, b) - end; - -julia> vi = VarInfo(demo2(missing, missing, 0.4)); - -julia> @varname(sub1.x) in keys(vi) -true - -julia> @varname(sub2.x) in keys(vi) -true -``` - -Variables `a` and `b` are not tracked, but are assigned the return values of the respective -calls to `demo1`: -```jldoctest submodel-to_submodel-prefix -julia> @varname(a) in keys(vi) -false - -julia> @varname(b) in keys(vi) -false -``` - -We can check that the log joint probability of the model accumulated in `vi` is correct: - -```jldoctest submodel-to_submodel-prefix -julia> sub1_x = vi[@varname(sub1.x)]; - -julia> sub2_x = vi[@varname(sub2.x)]; - -julia> logprior = logpdf(Normal(), sub1_x) + logpdf(Normal(), sub2_x); - -julia> loglikelihood = logpdf(Uniform(-1 - abs(sub1_x), 1 + abs(sub2_x)), 0.4); - -julia> getlogjoint(vi) ≈ logprior + loglikelihood -true -``` - -## Usage as likelihood is illegal - -Note that it is illegal to use a `to_submodel` model as a likelihood in another model: - -```jldoctest submodel-to_submodel-illegal; setup=:(using Distributions) -julia> @model inner() = x ~ Normal() -inner (generic function with 2 methods) - -julia> @model illegal_likelihood() = a ~ to_submodel(inner()) -illegal_likelihood (generic function with 2 methods) - -julia> model = illegal_likelihood() | (a = 1.0,); - -julia> model() -ERROR: ArgumentError: `~` with a model on the right-hand side of an observe statement is not supported -[...] -``` -""" -to_submodel(model::Model, auto_prefix::Bool=true) = - to_sampleable(returned(model), auto_prefix) diff --git a/src/submodel.jl b/src/submodel.jl new file mode 100644 index 000000000..94658b6bf --- /dev/null +++ b/src/submodel.jl @@ -0,0 +1,239 @@ +""" + is_rhs_model(x) + +Return `true` if `x` is a model or model wrapper, and `false` otherwise. +""" +is_rhs_model(x) = false + +""" + Distributional + +Abstract type for type indicating that something is "distributional". +""" +abstract type Distributional end + +""" + should_auto_prefix(distributional) + +Return `true` if the `distributional` should use automatic prefixing, and `false` otherwise. +""" +function should_auto_prefix end + +""" + is_rhs_model(x) + +Return `true` if the `distributional` is a model, and `false` otherwise. +""" +function is_rhs_model end + +""" + Sampleable{M} <: Distributional + +A wrapper around a model indicating it is sampleable. +""" +struct Sampleable{M,AutoPrefix} <: Distributional + model::M +end + +should_auto_prefix(::Sampleable{<:Any,AutoPrefix}) where {AutoPrefix} = AutoPrefix +is_rhs_model(x::Sampleable) = is_rhs_model(x.model) + +# TODO: Export this if it end up having a purpose beyond `to_submodel`. +""" + to_sampleable(model[, auto_prefix]) + +Return a wrapper around `model` indicating it is sampleable. + +# Arguments +- `model::Model`: the model to wrap. +- `auto_prefix::Bool`: whether to prefix the variables in the model. Default: `true`. +""" +to_sampleable(model, auto_prefix::Bool=true) = Sampleable{typeof(model),auto_prefix}(model) + +""" + rand_like!!(model_wrap, context, varinfo) + +Returns a tuple with the first element being the realization and the second the updated varinfo. + +# Arguments +- `model_wrap::ReturnedModelWrapper`: the wrapper of the model to use. +- `context::AbstractContext`: the context to use for evaluation. +- `varinfo::AbstractVarInfo`: the varinfo to use for evaluation. + """ +function rand_like!!( + model_wrap::Sampleable, context::AbstractContext, varinfo::AbstractVarInfo +) + return rand_like!!(model_wrap.model, context, varinfo) +end + +""" + ReturnedModelWrapper + +A wrapper around a model indicating it is a model over its return values. + +This should rarely be constructed explicitly; see [`returned(model)`](@ref) instead. +""" +struct ReturnedModelWrapper{M<:Model} + model::M +end + +is_rhs_model(::ReturnedModelWrapper) = true + +function rand_like!!( + model_wrap::ReturnedModelWrapper, context::AbstractContext, varinfo::AbstractVarInfo +) + # Return's the value and the (possibly mutated) varinfo. + return _evaluate!!(model_wrap.model, varinfo, context) +end + +""" + returned(model) + +Return a `model` wrapper indicating that it is a model over its return-values. +""" +returned(model::Model) = ReturnedModelWrapper(model) + +""" + to_submodel(model::Model[, auto_prefix::Bool]) + +Return a model wrapper indicating that it is a sampleable model over the return-values. + +This is mainly meant to be used on the right-hand side of a `~` operator to indicate that +the model can be sampled from but not necessarily evaluated for its log density. + +!!! warning + Note that some other operations that one typically associate with expressions of the form + `left ~ right` such as [`condition`](@ref), will also not work with `to_submodel`. + +!!! warning + To avoid variable names clashing between models, it is recommend leave argument `auto_prefix` equal to `true`. + If one does not use automatic prefixing, then it's recommended to use [`prefix(::Model, input)`](@ref) explicitly. + +# Arguments +- `model::Model`: the model to wrap. +- `auto_prefix::Bool`: whether to automatically prefix the variables in the model using the left-hand + side of the `~` statement. Default: `true`. + +# Examples + +## Simple example +```jldoctest submodel-to_submodel; setup=:(using 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 we sample from the model `demo2(missing, 0.4)` random variable `x` will be sampled: +```jldoctest submodel-to_submodel +julia> vi = VarInfo(demo2(missing, 0.4)); + +julia> @varname(a.x) in keys(vi) +true +``` + +The variable `a` is not tracked. However, it will be assigned the return value of `demo1`, +and can be used in subsequent lines of the model, as shown above. +```jldoctest submodel-to_submodel +julia> @varname(a) in keys(vi) +false +``` + +We can check that the log joint probability of the model accumulated in `vi` is correct: + +```jldoctest submodel-to_submodel +julia> x = vi[@varname(a.x)]; + +julia> getlogjoint(vi) ≈ logpdf(Normal(), x) + logpdf(Uniform(0, 1 + abs(x)), 0.4) +true +``` + +## Without automatic prefixing +As mentioned earlier, by default, the `auto_prefix` argument specifies whether to automatically +prefix the variables in the submodel. If `auto_prefix=false`, then the variables in the submodel +will not be prefixed. +```jldoctest submodel-to_submodel-prefix; setup=:(using Distributions) +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> vi = VarInfo(demo2_no_prefix(missing, 0.4)); + +julia> @varname(x) in keys(vi) # here we just use `x` instead of `a.x` +true +``` +However, not using prefixing is generally not recommended as it can lead to variable name clashes +unless one is careful. For example, if we're re-using the same model twice in a model, not using prefixing +will lead to variable name clashes: However, one can manually prefix using the [`prefix(::Model, input)`](@ref): +```jldoctest submodel-to_submodel-prefix +julia> @model function demo2(x, y, z) + a ~ to_submodel(prefix(demo1(x), :sub1), false) + b ~ to_submodel(prefix(demo1(y), :sub2), false) + return z ~ Uniform(-a, b) + end; + +julia> vi = VarInfo(demo2(missing, missing, 0.4)); + +julia> @varname(sub1.x) in keys(vi) +true + +julia> @varname(sub2.x) in keys(vi) +true +``` + +Variables `a` and `b` are not tracked, but are assigned the return values of the respective +calls to `demo1`: +```jldoctest submodel-to_submodel-prefix +julia> @varname(a) in keys(vi) +false + +julia> @varname(b) in keys(vi) +false +``` + +We can check that the log joint probability of the model accumulated in `vi` is correct: + +```jldoctest submodel-to_submodel-prefix +julia> sub1_x = vi[@varname(sub1.x)]; + +julia> sub2_x = vi[@varname(sub2.x)]; + +julia> logprior = logpdf(Normal(), sub1_x) + logpdf(Normal(), sub2_x); + +julia> loglikelihood = logpdf(Uniform(-1 - abs(sub1_x), 1 + abs(sub2_x)), 0.4); + +julia> getlogjoint(vi) ≈ logprior + loglikelihood +true +``` + +## Usage as likelihood is illegal + +Note that it is illegal to use a `to_submodel` model as a likelihood in another model: + +```jldoctest submodel-to_submodel-illegal; setup=:(using Distributions) +julia> @model inner() = x ~ Normal() +inner (generic function with 2 methods) + +julia> @model illegal_likelihood() = a ~ to_submodel(inner()) +illegal_likelihood (generic function with 2 methods) + +julia> model = illegal_likelihood() | (a = 1.0,); + +julia> model() +ERROR: ArgumentError: `~` with a model on the right-hand side of an observe statement is not supported +[...] +``` +""" +to_submodel(model::Model, auto_prefix::Bool=true) = + to_sampleable(returned(model), auto_prefix) diff --git a/src/submodel_macro.jl b/src/submodel_macro.jl deleted file mode 100644 index 67c3a8c18..000000000 --- a/src/submodel_macro.jl +++ /dev/null @@ -1,290 +0,0 @@ -""" - @submodel model - @submodel ... = model - -Run a Turing `model` nested inside of a Turing model. - -!!! warning - This is deprecated and will be removed in a future release. - Use `left ~ to_submodel(model)` instead (see [`to_submodel`](@ref)). - -# Examples - -```jldoctest submodel; setup=:(using Distributions) -julia> @model function demo1(x) - x ~ Normal() - return 1 + abs(x) - end; - -julia> @model function demo2(x, y) - @submodel a = demo1(x) - return y ~ Uniform(0, a) - end; -``` - -When we sample from the model `demo2(missing, 0.4)` random variable `x` will be sampled: -```jldoctest submodel -julia> vi = VarInfo(demo2(missing, 0.4)); -┌ Warning: `@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax. -│ caller = ip:0x0 -└ @ Core :-1 - -julia> @varname(x) in keys(vi) -true -``` - -Variable `a` is not tracked since it can be computed from the random variable `x` that was -tracked when running `demo1`: -```jldoctest submodel -julia> @varname(a) in keys(vi) -false -``` - -We can check that the log joint probability of the model accumulated in `vi` is correct: - -```jldoctest submodel -julia> x = vi[@varname(x)]; - -julia> getlogjoint(vi) ≈ logpdf(Normal(), x) + logpdf(Uniform(0, 1 + abs(x)), 0.4) -true -``` -""" -macro submodel(expr) - return submodel(:(prefix = false), expr) -end - -""" - @submodel prefix=... model - @submodel prefix=... ... = model - -Run a Turing `model` nested inside of a Turing model and add "`prefix`." as a prefix -to all random variables inside of the `model`. - -Valid expressions for `prefix=...` are: -- `prefix=false`: no prefix is used. -- `prefix=true`: _attempt_ to automatically determine the prefix from the left-hand side - `... = model` by first converting into a `VarName`, and then calling `Symbol` on this. -- `prefix=expression`: results in the prefix `Symbol(expression)`. - -The prefix makes it possible to run the same Turing model multiple times while -keeping track of all random variables correctly. - -!!! warning - This is deprecated and will be removed in a future release. - Use `left ~ to_submodel(model)` instead (see [`to_submodel(model)`](@ref)). - -# Examples -## Example models -```jldoctest submodelprefix; setup=:(using Distributions) -julia> @model function demo1(x) - x ~ Normal() - return 1 + abs(x) - end; - -julia> @model function demo2(x, y, z) - @submodel prefix="sub1" a = demo1(x) - @submodel prefix="sub2" b = demo1(y) - return z ~ Uniform(-a, b) - end; -``` - -When we sample from the model `demo2(missing, missing, 0.4)` random variables `sub1.x` and -`sub2.x` will be sampled: -```jldoctest submodelprefix -julia> vi = VarInfo(demo2(missing, missing, 0.4)); -┌ Warning: `@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax. -│ caller = ip:0x0 -└ @ Core :-1 - -julia> @varname(sub1.x) in keys(vi) -true - -julia> @varname(sub2.x) in keys(vi) -true -``` - -Variables `a` and `b` are not tracked since they can be computed from the random variables `sub1.x` and -`sub2.x` that were tracked when running `demo1`: -```jldoctest submodelprefix -julia> @varname(a) in keys(vi) -false - -julia> @varname(b) in keys(vi) -false -``` - -We can check that the log joint probability of the model accumulated in `vi` is correct: - -```jldoctest submodelprefix -julia> sub1_x = vi[@varname(sub1.x)]; - -julia> sub2_x = vi[@varname(sub2.x)]; - -julia> logprior = logpdf(Normal(), sub1_x) + logpdf(Normal(), sub2_x); - -julia> loglikelihood = logpdf(Uniform(-1 - abs(sub1_x), 1 + abs(sub2_x)), 0.4); - -julia> getlogjoint(vi) ≈ logprior + loglikelihood -true -``` - -## Different ways of setting the prefix -```jldoctest submodel-prefix-alternatives; setup=:(using DynamicPPL, Distributions) -julia> @model inner() = x ~ Normal() -inner (generic function with 2 methods) - -julia> # When `prefix` is unspecified, no prefix is used. - @model submodel_noprefix() = @submodel a = inner() -submodel_noprefix (generic function with 2 methods) - -julia> @varname(x) in keys(VarInfo(submodel_noprefix())) -┌ Warning: `@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax. -│ caller = ip:0x0 -└ @ Core :-1 -true - -julia> # Explicitely don't use any prefix. - @model submodel_prefix_false() = @submodel prefix=false a = inner() -submodel_prefix_false (generic function with 2 methods) - -julia> @varname(x) in keys(VarInfo(submodel_prefix_false())) -┌ Warning: `@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax. -│ caller = ip:0x0 -└ @ Core :-1 -true - -julia> # Automatically determined from `a`. - @model submodel_prefix_true() = @submodel prefix=true a = inner() -submodel_prefix_true (generic function with 2 methods) - -julia> @varname(a.x) in keys(VarInfo(submodel_prefix_true())) -┌ Warning: `@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax. -│ caller = ip:0x0 -└ @ Core :-1 -true - -julia> # Using a static string. - @model submodel_prefix_string() = @submodel prefix="my prefix" a = inner() -submodel_prefix_string (generic function with 2 methods) - -julia> @varname(var"my prefix".x) in keys(VarInfo(submodel_prefix_string())) -┌ Warning: `@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax. -│ caller = ip:0x0 -└ @ Core :-1 -true - -julia> # Using string interpolation. - @model submodel_prefix_interpolation() = @submodel prefix="\$(nameof(inner()))" a = inner() -submodel_prefix_interpolation (generic function with 2 methods) - -julia> @varname(inner.x) in keys(VarInfo(submodel_prefix_interpolation())) -┌ Warning: `@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax. -│ caller = ip:0x0 -└ @ Core :-1 -true - -julia> # Or using some arbitrary expression. - @model submodel_prefix_expr() = @submodel prefix=1 + 2 a = inner() -submodel_prefix_expr (generic function with 2 methods) - -julia> @varname(var"3".x) in keys(VarInfo(submodel_prefix_expr())) -┌ Warning: `@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax. -│ caller = ip:0x0 -└ @ Core :-1 -true - -julia> # (×) Automatic prefixing without a left-hand side expression does not work! - @model submodel_prefix_error() = @submodel prefix=true inner() -ERROR: LoadError: cannot automatically prefix with no left-hand side -[...] -``` - -# Notes -- The choice `prefix=expression` means that the prefixing will incur a runtime cost. - This is also the case for `prefix=true`, depending on whether the expression on the - the right-hand side of `... = model` requires runtime-information or not, e.g. - `x = model` will result in the _static_ prefix `x`, while `x[i] = model` will be - resolved at runtime. -""" -macro submodel(prefix_expr, expr) - return submodel(prefix_expr, expr, esc(:__model__)) -end - -# Automatic prefixing. -function prefix_submodel_context(prefix::Bool, left::Symbol, model) - return prefix ? prefix_submodel_context(left, model) : :($model.context) -end - -function prefix_submodel_context(prefix::Bool, left::Expr, model) - return prefix ? prefix_submodel_context(varname(left), model) : :($model.context) -end - -# Manual prefixing. -prefix_submodel_context(prefix, left, model) = prefix_submodel_context(prefix, model) -function prefix_submodel_context(prefix, model) - # E.g. `prefix="asd[$i]"` or `prefix=asd` with `asd` to be evaluated. - return :($(PrefixContext)($(Val)($(Symbol)($(esc(prefix)))), $model.context)) -end - -function prefix_submodel_context(prefix::Union{AbstractString,Symbol}, model) - # E.g. `prefix="asd"`. - return :($(PrefixContext)($(esc(Meta.quot(Val(Symbol(prefix))))), $model.context)) -end - -function prefix_submodel_context(prefix::Bool, model) - if prefix - error("cannot automatically prefix with no left-hand side") - end - - return :($model.context) -end - -const SUBMODEL_DEPWARN_MSG = "`@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax." - -function submodel(prefix_expr, expr, model=esc(:__model__)) - prefix_left, prefix = getargs_assignment(prefix_expr) - if prefix_left !== :prefix - error("$(prefix_left) is not a valid kwarg") - end - - # The user expects `@submodel ...` to return the - # return-value of the `...`, hence we need to capture - # the return-value and handle it correctly. - @gensym retval - - # `prefix=false` => don't prefix, i.e. do nothing to `ctx`. - # `prefix=true` => automatically determine prefix. - # `prefix=...` => use it. - args_assign = getargs_assignment(expr) - return if args_assign === nothing - ctx = prefix_submodel_context(prefix, model) - quote - # Raise deprecation warning to let user know that we recommend using `left ~ to_submodel(model)`. - $(Base.depwarn)(SUBMODEL_DEPWARN_MSG, Symbol("@submodel")) - - $retval, $(esc(:__varinfo__)) = $(_evaluate!!)( - $(esc(expr)), $(esc(:__varinfo__)), $(ctx) - ) - $retval - end - else - L, R = args_assign - # Now that we have `L` and `R`, we can prefix automagically. - try - ctx = prefix_submodel_context(prefix, L, model) - catch e - error( - "failed to determine prefix from $(L); please specify prefix using the `@submodel prefix=\"your prefix\" ...` syntax", - ) - end - quote - # Raise deprecation warning to let user know that we recommend using `left ~ to_submodel(model)`. - $(Base.depwarn)(SUBMODEL_DEPWARN_MSG, Symbol("@submodel")) - - $retval, $(esc(:__varinfo__)) = $(_evaluate!!)( - $(esc(R)), $(esc(:__varinfo__)), $(ctx) - ) - $(esc(L)) = $retval - end - end -end diff --git a/test/deprecated.jl b/test/deprecated.jl deleted file mode 100644 index 500d3eb7f..000000000 --- a/test/deprecated.jl +++ /dev/null @@ -1,57 +0,0 @@ -@testset "deprecated" begin - @testset "@submodel" begin - @testset "is deprecated" begin - @model inner() = x ~ Normal() - @model outer() = @submodel x = inner() - @test_logs( - ( - :warn, - "`@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax.", - ), - outer()() - ) - - @model outer_with_prefix() = @submodel prefix = "sub" x = inner() - @test_logs( - ( - :warn, - "`@submodel model` and `@submodel prefix=... model` are deprecated; see `to_submodel` for the up-to-date syntax.", - ), - outer_with_prefix()() - ) - end - - @testset "prefixing still works correctly" begin - @model inner() = x ~ Normal() - @model function outer() - a = @submodel inner() - b = @submodel prefix = "sub" inner() - return a, b - end - @test outer()() isa Tuple{Float64,Float64} - vi = VarInfo(outer()) - @test @varname(x) in keys(vi) - @test @varname(sub.x) in keys(vi) - end - - @testset "logp is still accumulated properly" begin - @model inner_assume() = x ~ Normal() - @model inner_observe(x, y) = y ~ Normal(x) - @model function outer(b) - a = @submodel inner_assume() - @submodel inner_observe(a, b) - end - y_val = 1.0 - model = outer(y_val) - @test model() == y_val - - x_val = 1.5 - vi = VarInfo(outer(y_val)) - DynamicPPL.setindex!!(vi, x_val, @varname(x)) - @test logprior(model, vi) ≈ logpdf(Normal(), x_val) - @test loglikelihood(model, vi) ≈ logpdf(Normal(x_val), y_val) - @test logjoint(model, vi) ≈ - logpdf(Normal(), x_val) + logpdf(Normal(x_val), y_val) - end - end -end diff --git a/test/runtests.jl b/test/runtests.jl index 6fabcfe59..c60c06786 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -72,7 +72,6 @@ include("test_util.jl") include("context_implementations.jl") include("threadsafe.jl") include("debug_utils.jl") - include("deprecated.jl") include("submodels.jl") include("bijector.jl") end