Skip to content

Commit 16793e0

Browse files
committed
Finish docs
1 parent dab7704 commit 16793e0

File tree

5 files changed

+175
-41
lines changed

5 files changed

+175
-41
lines changed

docs/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[deps]
2+
AbstractPPL = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf"
23
Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697"
34
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
45
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"

docs/src/api.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ decondition
7878

7979
## Fixing and unfixing
8080

81-
We can also _fix_ a collection of variables in a [`Model`](@ref) to certain using [`fix`](@ref).
81+
We can also _fix_ a collection of variables in a [`Model`](@ref) to certain using [`DynamicPPL.fix`](@ref).
8282

8383
This might seem quite similar to the aforementioned [`condition`](@ref) and its siblings,
8484
but they are indeed different operations:
@@ -89,19 +89,19 @@ but they are indeed different operations:
8989
- `fix`ed variables are considered to be _constant_, and are thus not included
9090
in any log-probability computations.
9191

92-
The differences are more clearly spelled out in the docstring of [`fix`](@ref) below.
92+
The differences are more clearly spelled out in the docstring of [`DynamicPPL.fix`](@ref) below.
9393

9494
```@docs
95-
fix
95+
DynamicPPL.fix
9696
DynamicPPL.fixed
9797
```
9898

99-
The difference between [`fix`](@ref) and [`condition`](@ref) is described in the docstring of [`fix`](@ref) above.
99+
The difference between [`DynamicPPL.fix`](@ref) and [`DynamicPPL.condition`](@ref) is described in the docstring of [`DynamicPPL.fix`](@ref) above.
100100

101-
Similarly, we can [`unfix`](@ref) variables, i.e. return them to their original meaning:
101+
Similarly, we can revert this with [`DynamicPPL.unfix`](@ref), i.e. return the variables to their original meaning:
102102

103103
```@docs
104-
unfix
104+
DynamicPPL.unfix
105105
```
106106

107107
## Predicting

docs/src/internals/submodel_condition.md

Lines changed: 146 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,9 @@ keys(vi)
3535
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.
3636
We will return to the 'manual prefixing' case later.
3737

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)
43-
return tilde_assume(context.context, right, prefix(context, vn), vi)
44-
end
45-
```
46-
47-
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.
4841

4942
## ConditionContext
5043

@@ -205,29 +198,158 @@ DynamicPPL.hasconditioned_nested(inner_ctx_with_outer_cond, @varname(a.x))
205198
DynamicPPL.hasconditioned_nested(inner_ctx_with_inner_cond, @varname(a.x))
206199
```
207200

208-
Essentially, our job is threefold:
201+
This allows us to finally specify our task as follows:
209202

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.
211204

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.
216210

217211
## How do we do it?
218212

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:
253+
254+
```@example
255+
big_ctx = PrefixContext{:a}(
256+
ConditionContext(
257+
Dict(@varname(b.y) => 1.0),
258+
PrefixContext{:b}(ConditionContext(Dict(@varname(x) => 1.0))),
259+
),
260+
)
261+
```
262+
263+
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
285+
new_vn = AbstractPPL.prefix(vn, VarName{Prefix}())
286+
# Then pass to the child context
287+
return myprefix(childcontext(ctx), new_vn)
288+
end
289+
290+
myprefix(big_ctx, @varname(x))
291+
```
292+
293+
This implementation clearly is not correct, because it applies the _inner_ `PrefixContext` before the outer one.
294+
295+
The right way to implement `myprefix` is to, essentially, reverse the order of two lines above:
296+
297+
```@example
298+
function myprefix(ctx::DynamicPPL.PrefixContext{Prefix}, vn::VarName) where {Prefix}
299+
# Pass to the child context first
300+
new_vn = myprefix(childcontext(ctx), vn)
301+
# Then apply this context's prefix
302+
return AbstractPPL.prefix(new_vn, VarName{Prefix}())
303+
end
304+
305+
myprefix(big_ctx, @varname(x))
306+
```
307+
308+
This is a much better result!
309+
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.
220315

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
223317

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:
227319

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`.
320+
```@example
321+
@model function inner_manual()
322+
x ~ Normal()
323+
return y ~ Normal()
324+
end
325+
326+
@model function outer_manual()
327+
return _unused ~ to_submodel(prefix(inner_manual(), :a), false)
328+
end
329+
```
330+
331+
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+
```
229350

230-
## FixedContext
351+
## Loose ends 2: FixedContext
231352

232353
Finally, note that all of the above also applies to the interaction between `PrefixContext` and `FixedContext`, except that the functions have different names.
233354
(`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.

src/context_implementations.jl

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,15 @@ function tilde_assume(rng::Random.AbstractRNG, ::LikelihoodContext, sampler, rig
8585
end
8686

8787
function tilde_assume(context::PrefixContext, right, vn, vi)
88-
# The slightly tricky thing about PrefixContext is that they are applied
89-
# from the outside in, so `PrefixContext{:a}(PrefixContext{:b}(ctx))` means
90-
# that variables get prefixed like `a.b.x`.
91-
# This motivates the implementation shown here, where the function
92-
# `prefix_and_strip_contexts` is responsible for not only adding the
93-
# prefixes, but also removing the `PrefixContext`s from the context stack
94-
# so that they don't get applied twice when recursing.
95-
# TODO(penelopeysm): It would be nice to switch this round, but it's a very
96-
# tricky task. Essentially it forces us to use a foldr inside
97-
# `prefix_and_strip_contexts`, rather than a foldl which is what most of
98-
# DynamicPPL uses.
88+
# Note that we can't use something like this here:
89+
# new_vn = prefix(context, vn)
90+
# return tilde_assume(childcontext(context), right, new_vn, vi)
91+
# This is because `prefix` applies _all_ prefixes in a given context to a
92+
# variable name. Thus, if we had two levels of nested prefixes e.g.
93+
# `PrefixContext{:a}(PrefixContext{:b}(DefaultContext()))`, then the
94+
# first call would apply the prefix `a.b._`, and the recursive call
95+
# would apply the prefix `b._`, resulting in `b.a.b._`.
96+
# This is why we need a special function, `prefix_and_strip_contexts`.
9997
new_vn, new_context = prefix_and_strip_contexts(context, vn)
10098
return tilde_assume(new_context, right, new_vn, vi)
10199
end

src/contexts.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,19 @@ end
281281
282282
Same as `prefix`, but additionally returns a new context stack that has all the
283283
PrefixContexts removed.
284+
285+
NOTE: This does _not_ modify any variables in any `ConditionContext` and
286+
`FixedContext` that may be present in the context stack. This is because this
287+
function is only used in `tilde_assume`, which is lower in the tilde-pipeline
288+
than `contextual_isassumption` and `contextual_isfixed` (the functions which
289+
actually use the `ConditionContext` and `FixedContext` values). Thus, by this
290+
time, any `ConditionContext`s and `FixedContext`s present have already served
291+
their purpose.
292+
293+
If you call this function, you must therefore be careful to ensure that you _do
294+
not_ need to modify any inner `ConditionContext`s and `FixedContext`s. If you
295+
_do_ need to modify them, then you may need to use
296+
`prefix_cond_and_fixed_variables` instead.
284297
"""
285298
function prefix_and_strip_contexts(ctx::PrefixContext{Prefix}, vn::VarName) where {Prefix}
286299
child_context = childcontext(ctx)

0 commit comments

Comments
 (0)