You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In this case, where `to_submodel` is called without any other arguments, the prefix to be used is automatically inferred from the name of the variable on the left-hand side of the tilde.
36
36
We will return to the 'manual prefixing' case later.
37
37
38
-
What does it really mean to 'become' a different variable?
39
-
We can see this from [the definition of `tilde_assume`, for example](https://github.com/TuringLang/DynamicPPL.jl/blob/60ee68e2ce28a15c6062c243019e6208d16802a5/src/context_implementations.jl#L87-L89):
40
-
41
-
```
42
-
function tilde_assume(context::PrefixContext, right, vn, vi)
Functionally, this means that even though the _initial_ entry to the tilde-pipeline has `vn` as `x` and `y`, once the `PrefixContext` has been applied, the later functions will see `a.x` and `a.y` instead.
38
+
The phrase 'becoming' a different variable is a little underspecified: it is useful to pinpoint the exact location where the prefixing occurs, which is `tilde_assume`.
39
+
The method responsible for it is `tilde_assume(::PrefixContext, right, vn, vi)`: this attaches the prefix in the context to the `VarName` argument, before recursively calling `tilde_assume` with the new prefixed `VarName`.
40
+
This means that even though a statement `x ~ dist` still enters the tilde pipeline at the top level as `x`, if the model evaluation context contains a `PrefixContext`, any function from `tilde_assume` onwards will see `a.x` instead.
This allows us to finally specify our task as follows:
209
202
210
-
- Firstly, given the correct arguments, we need to make sure that `hasconditioned_nested` and `getconditioned_nested` behave correctly.
203
+
(1) Given the correct arguments, we need to make sure that `hasconditioned_nested` and `getconditioned_nested` behave correctly.
211
204
212
-
- Secondly, we need to make sure that both the correct arguments are supplied. In order to do so:
213
-
214
-
+ We need to make sure that when evaluating a submodel, the context stack is arranged such that prefixes are applied _inside_ the parent model's context, but _outside_ the submodel's own context.
215
-
+ We also need to make sure that the `VarName` passed to it is prefixed correctly. This is, in fact, _not_ handled by `tilde_assume`, because `contextual_isassumption` is much higher in the call stack than `tilde_assume` is. So, we need to explicitly prefix it.
205
+
(2) We need to make sure that both the correct arguments are supplied. In order to do so:
206
+
207
+
- (2a) We need to make sure that when evaluating a submodel, the context stack is arranged such that `PrefixContext` is applied _inside_ the parent model's context, but _outside_ the submodel's own context.
208
+
209
+
- (2b) We also need to make sure that the `VarName` passed to it is prefixed correctly.
216
210
217
211
## How do we do it?
218
212
219
-
`hasconditioned_nested` accomplishes this by doing the following:
213
+
(1) `hasconditioned_nested` and `getconditioned_nested` accomplish this by first 'collapsing' the context stack, i.e. they go through the context stack, remove all `PrefixContext`s, and apply those prefixes to any conditioned variables below it in the stack.
214
+
Once the `PrefixContext`s have been removed, one can then iterate through the context stack and check if any of the `ConditionContext`s contain the variable, or get the value itself.
215
+
For more details the reader is encouraged to read the source code.
216
+
217
+
(2a) We ensure that the context stack is correctly arranged by relying on the behaviour of `make_evaluate_args_and_kwargs`.
218
+
This function is called whenever a model (which itself contains a context) is evaluated with a separate ('external') context, and makes sure to arrange both of these contexts such that _the model's context is nested inside the external context_.
219
+
Thus, as long as prefixing is implemented by applying a `PrefixContext` on the outermost layer of the _inner_ model context, this will be correctly combined with an external context to give the behaviour seen above.
220
+
221
+
(2b) At first glance, it seems like `tilde_assume` can take care of the `VarName` prefixing for us (as described in the first section).
222
+
However, this is not actually the case: `contextual_isassumption`, which is the function that calls `hasconditioned_nested`, is much higher in the call stack than `tilde_assume` is.
223
+
So, we need to explicitly prefix it before passing it to `contextual_isassumption`.
224
+
This is done inside the `@model` macro, or technically, its subsidiary function `isassumption`.
225
+
226
+
## Nested submodels
227
+
228
+
Just in case the above wasn't complicated enough, we need to also be very careful when dealing with nested submodels, which have multiple layers of `PrefixContext`s which may be interspersed with `ConditionContext`s.
229
+
For example, in this series of nested submodels,
230
+
231
+
```@example
232
+
@model function charlie()
233
+
x ~ Normal()
234
+
y ~ Normal()
235
+
return z ~ Normal()
236
+
end
237
+
@model function bravo()
238
+
return b ~ to_submodel(charlie() | (@varname(x) => 1.0))
239
+
end
240
+
@model function alpha()
241
+
return a ~ to_submodel(bravo() | (@varname(b.y) => 1.0))
242
+
end
243
+
```
244
+
245
+
we expect that the only variable to be sampled should be `z` inside `charlie`, or rather, `a.b.z` once it has been through the prefixes.
246
+
247
+
```@example
248
+
keys(VarInfo(alpha()))
249
+
```
250
+
251
+
The general strategy that we adopt is similar to above.
252
+
Following the principle that `PrefixContext` should be nested inside the outer context, but outside the inner submodel's context, we can infer that the correct context inside `charlie` should be:
We need several things to work correctly here: we need the `VarName` prefixing to behave correctly, and then we need to implement `hasconditioned_nested` and `getconditioned_nested` on the resulting prefixed `VarName`.
264
+
It turns out that the prefixing itself is enough to illustrate the most important point in this section, namely, the need to traverse the context stack in a _different direction_ to what most of DynamicPPL does.
265
+
266
+
Let's work with a function called `myprefix(::AbstractContext, ::VarName)` (to avoid confusion with any existing DynamicPPL function).
267
+
We should like `myprefix(big_ctx, @varname(x))` to return `@varname(a.b.x)`.
268
+
Consider the following naive implementation, which mirrors a lot of code in the tilde-pipeline:
269
+
270
+
```@example
271
+
using DynamicPPL: NodeTrait, IsLeaf, IsParent, childcontext, AbstractContext
272
+
using AbstractPPL: AbstractPPL
273
+
274
+
function myprefix(ctx::DynamicPPL.AbstractContext, vn::VarName)
275
+
return myprefix(NodeTrait(ctx), ctx, vn)
276
+
end
277
+
function myprefix(::IsLeaf, ::AbstractContext, vn::VarName)
278
+
return vn
279
+
end
280
+
function myprefix(::IsParent, ctx::AbstractContext, vn::VarName)
281
+
return myprefix(childcontext(ctx), vn)
282
+
end
283
+
function myprefix(ctx::DynamicPPL.PrefixContext{Prefix}, vn::VarName) where {Prefix}
284
+
# The functionality to actually manipulate the VarNames is in AbstractPPL
The implementation of related functions such as `hasconditioned_nested` and `getconditioned_nested`, under the hood, use a similar recursion scheme, so you will find that this is a common pattern when reading the source code of various prefixing-related functions, you will find that this is a common pattern
310
+
When editing this code, it is worth being mindful of this as a potential source of incorrectness.
311
+
312
+
!!! info
313
+
314
+
If you have encountered left and right folds, the above discussion illustrates the difference between them: the wrong implementation of `myprefix` uses a left fold (which collects prefixes in the opposite order from which they are encountered), while the correct implementation uses a right fold.
220
315
221
-
- If the outermost layer is a `ConditionContext`, it checks whether the variable is contained in its values.
222
-
- If the outermost layer is a `PrefixContext`, it goes through the `PrefixContext`'s child context and prefixes any inner conditioned variables, before checking whether the variable is contained.
316
+
## Loose ends 1: Manual prefixing
223
317
224
-
We ensure that the context stack is correctly arranged by relying on the behaviour of `make_evaluate_args_and_kwargs`.
225
-
This function is called whenever a model (which itself contains a context) is evaluated with a separate ('outer') context, and makes sure to arrange it such that the model's context is nested inside the outer context.
226
-
Thus, as long as prefixing is implemented by applying a `PrefixContext` on the outermost layer of the _inner_ model context, this will be correctly combined with an outer context to give the behaviour seen above.
318
+
Sometimes users may want to manually prefix a model, for example:
227
319
228
-
And finally, we ensure that the `VarName` is correctly prefixed by modifying the `@model` macro (or, technically, its subsidiary `isassumption`) to explicitly prefix the variable before passing it to `contextual_isassumption`.
In this case, the `VarName` on the left-hand side of the tilde is not used, and the prefix is instead specified using the `prefix` function.
332
+
333
+
The way to deal with this follows on from the previous discussion.
334
+
Specifically, we said that:
335
+
336
+
> [...] as long as prefixing is implemented by applying a `PrefixContext` on the outermost layer of the _inner_ model context, this will be correctly combined [...]
337
+
338
+
When automatic prefixing is used, this application of `PrefixContext` occurs inside the `tilde_assume!!` method.
339
+
In the manual prefixing case, we need to make sure that `prefix(submodel::Model, ::Symbol)` does the same thing, i.e. it inserts a `PrefixContext` at the outermost layer of `submodel`'s context.
340
+
We can see that this is precisely what happens:
341
+
342
+
```@example
343
+
@model f() = x ~ Normal()
344
+
345
+
model = f()
346
+
prefixed_model = prefix(model, :a)
347
+
348
+
(model.context, prefixed_model.context)
349
+
```
229
350
230
-
## FixedContext
351
+
## Loose ends 2: FixedContext
231
352
232
353
Finally, note that all of the above also applies to the interaction between `PrefixContext` and `FixedContext`, except that the functions have different names.
233
354
(`FixedContext` behaves the same way as `ConditionContext`, except that unlike conditioned variables, fixed variables do not contribute to the log probability density.)
355
+
This generally results in a large amount of code duplication, but the concepts that underlie both contexts are exactly the same.
0 commit comments