Skip to content

Commit 44d75fe

Browse files
committed
Add documentation (partial)
1 parent 71f745e commit 44d75fe

File tree

2 files changed

+234
-1
lines changed

2 files changed

+234
-1
lines changed

docs/make.jl

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ makedocs(;
2424
format=Documenter.HTML(; size_threshold=2^10 * 400),
2525
modules=[DynamicPPL, Base.get_extension(DynamicPPL, :DynamicPPLMCMCChainsExt)],
2626
pages=[
27-
"Home" => "index.md", "API" => "api.md", "Internals" => ["internals/varinfo.md"]
27+
"Home" => "index.md",
28+
"API" => "api.md",
29+
"Internals" => ["internals/varinfo.md", "internals/submodel_condition.md"],
2830
],
2931
checkdocs=:exports,
3032
doctest=false,
+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# How `PrefixContext` and `ConditionContext` interact
2+
3+
```@meta
4+
ShareDefaultModule = true
5+
```
6+
7+
## PrefixContext
8+
9+
`PrefixContext` is a context that, as the name suggests, prefixes all variables inside a model with a given symbol.
10+
Thus, for example:
11+
12+
```@example
13+
using DynamicPPL, Distributions
14+
15+
@model function f()
16+
x ~ Normal()
17+
return y ~ Normal()
18+
end
19+
20+
@model function g()
21+
return a ~ to_submodel(f())
22+
end
23+
```
24+
25+
inside the submodel `f`, the variables `x` and `y` become `a.x` and `a.y` respectively.
26+
This is easiest to observe by running the model:
27+
28+
```@example
29+
vi = VarInfo(g())
30+
keys(vi)
31+
```
32+
33+
!!! note
34+
35+
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+
We will return to the 'manual prefixing' case later.
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)
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.
48+
49+
## ConditionContext
50+
51+
`ConditionContext` is a context which stores values of variables that are to be conditioned on.
52+
These values may be stored as a `Dict` which maps `VarName`s to values, or alternatively as a `NamedTuple`.
53+
The latter only works correctly if all `VarName`s are 'basic', in that they have an identity optic (i.e., something like `a.x` or `a[1]` is forbidden).
54+
Because of this limitation, we will only use `Dict` in this example.
55+
56+
!!! note
57+
58+
If a `ConditionContext` with a `NamedTuple` encounters anything to do with a prefix, its internal `NamedTuple` is converted to a `Dict` anyway, so it is quite reasonable to ignore the `NamedTuple` case in this exposition.
59+
60+
One can inspect the conditioning values with, for example:
61+
62+
```@example
63+
@model function d()
64+
x ~ Normal()
65+
return y ~ Normal()
66+
end
67+
68+
cond_model = d() | (@varname(x) => 1.0)
69+
cond_ctx = cond_model.context
70+
```
71+
72+
There are several internal functions that are used to determine whether a variable is conditioned, and if so, what its value is.
73+
74+
```@example
75+
DynamicPPL.hasconditioned_nested(cond_ctx, @varname(x))
76+
```
77+
78+
```@example
79+
DynamicPPL.getconditioned_nested(cond_ctx, @varname(x))
80+
```
81+
82+
These functions are in turn used by the function `DynamicPPL.contextual_isassumption`, which is largely the same as `hasconditioned_nested`, but also checks whether the value is `missing` (in which case it isn't really conditioned).
83+
84+
```@example
85+
DynamicPPL.contextual_isassumption(cond_ctx, @varname(x))
86+
```
87+
88+
!!! note
89+
90+
Notice that (neglecting `missing` values) the return value of `contextual_isassumption` is the _opposite_ of `hasconditioned_nested`, i.e. for a variable that _is_ conditioned on, `contextual_isassumption` returns `false`.
91+
92+
If a variable `x` is conditioned on, then the effect of this is to set the value of `x` to the given value (while still including its contribution to the log probability density).
93+
Since `x` is no longer a random variable, if we were to evaluate the model, we would find only one key in the `VarInfo`:
94+
95+
```@example
96+
keys(VarInfo(cond_model))
97+
```
98+
99+
## Joint behaviour: desiderata at the model level
100+
101+
When paired together, these two contexts have the potential to cause substantial confusion: `PrefixContext` modifies the variable names that are seen, which may cause them to be out of sync with the values contained inside the `ConditionContext`.
102+
103+
We begin by mentioning some high-level desiderata for their joint behaviour.
104+
Take these models, for example:
105+
106+
```@example
107+
# We define a helper function to unwrap a layer of SamplingContext, to
108+
# avoid cluttering the print statements.
109+
unwrap_sampling_context(ctx::DynamicPPL.SamplingContext) = ctx.context
110+
unwrap_sampling_context(ctx::DynamicPPL.AbstractContext) = ctx
111+
@model function inner()
112+
println("inner context: $(unwrap_sampling_context(__context__))")
113+
x ~ Normal()
114+
return y ~ Normal()
115+
end
116+
117+
@model function outer()
118+
println("outer context: $(unwrap_sampling_context(__context__))")
119+
return a ~ to_submodel(inner())
120+
end
121+
122+
# 'Outer conditioning'
123+
with_outer_cond = outer() | (@varname(a.x) => 1.0)
124+
125+
# 'Inner conditioning'
126+
inner_cond = inner() | (@varname(x) => 1.0)
127+
@model function outer2()
128+
println("outer context: $(unwrap_sampling_context(__context__))")
129+
return a ~ to_submodel(inner_cond)
130+
end
131+
with_inner_cond = outer2()
132+
```
133+
134+
We want that:
135+
136+
1. `keys(VarInfo(outer()))` should return `[a.x, a.y]`;
137+
2. `keys(VarInfo(with_outer_cond))` should return `[a.y]`;
138+
3. `keys(VarInfo(with_inner_cond))` should return `[a.y]`,
139+
140+
**In other words, we can condition submodels either from the outside (point (2)) or from the inside (point (3)), and the variable name we use to specify the conditioning should match the level at which we perform the conditioning.**
141+
142+
This is an incredibly salient point because it means that submodels can be treated as individual, opaque objects, and we can condition them without needing to know what it will be prefixed with, or the context in which that submodel is being used.
143+
For example, this means we can reuse `inner_cond` in another model with a different prefix, and it will _still_ have its inner `x` value be conditioned, despite the prefix differing.
144+
145+
!!! info
146+
147+
In the current version of DynamicPPL, these criteria are all fulfilled. However, this was not the case in the past: in particular, point (3) was not fulfilled, and users had to condition the internal submodel with the prefixes that were used outside. (See [this GitHub issue](https://github.com/TuringLang/DynamicPPL.jl/issues/857) for more information; this issue was the direct motivation for this documentation page.)
148+
149+
## Desiderata at the context level
150+
151+
The above section describes how we expect conditioning and prefixing to behave from a user's perpective.
152+
We now turn to the question of how we implement this in terms of DynamicPPL contexts.
153+
We do not specify the implementation details here, but we will sketch out something resembling an API that will allow us to achieve the target behaviour.
154+
155+
**Point (1)** does not involve any conditioning, only prefixing; it is therefore already satisfied by virtue of the `tilde_assume` method shown above.
156+
157+
**Points (2) and (3)** are more tricky.
158+
As the reader may surmise, the difference between them is the order in which the contexts are stacked.
159+
160+
For the _outer_ conditioning case (point (2)), the `ConditionContext` will contain a `VarName` that is already prefixed.
161+
When we enter the inner submodel, this `ConditionContext` has to be passed down and somehow combined with the `PrefixContext` that is created when we enter the submodel.
162+
We make the claim here that the best way to do this is to nest the `PrefixContext` _inside_ the `ConditionContext`.
163+
This is indeed what happens, as can be demonstrated by running the model.
164+
165+
```@example
166+
with_outer_cond();
167+
nothing;
168+
```
169+
170+
!!! info
171+
172+
The `; nothing` at the end is purely to circumvent a Documenter.jl quirk where stdout is only shown if the return value of the final statement is `nothing`.
173+
If these documentation pages are moved to Quarto, it will be possible to remove this.
174+
175+
For the _inner_ conditioning case (point (3)), the outer model is not run with any special context.
176+
The inner model will itself contain a `ConditionContext` will contain a `VarName` that is not prefixed.
177+
When we run the model, this `ConditionContext` should be then nested _inside_ a `PrefixContext` to form the final evaluation context.
178+
Again, we can run the model to see this in action:
179+
180+
```@example
181+
with_inner_cond();
182+
nothing;
183+
```
184+
185+
Putting all of the information so far together, what it means is that if we have these two inner contexts (taken from above):
186+
187+
```@example
188+
using DynamicPPL: PrefixContext, ConditionContext, DefaultContext
189+
190+
inner_ctx_with_outer_cond = ConditionContext(
191+
Dict(@varname(a.x) => 1.0), PrefixContext{:a}(DefaultContext())
192+
)
193+
inner_ctx_with_inner_cond = PrefixContext{:a}(
194+
ConditionContext(Dict(@varname(x) => 1.0), DefaultContext())
195+
)
196+
```
197+
198+
then we want both of these to be `true` (and thankfully, they are!):
199+
200+
```@example
201+
DynamicPPL.hasconditioned_nested(inner_ctx_with_outer_cond, @varname(a.x))
202+
```
203+
204+
```@example
205+
DynamicPPL.hasconditioned_nested(inner_ctx_with_inner_cond, @varname(a.x))
206+
```
207+
208+
Essentially, our job is threefold:
209+
210+
- Firstly, given the correct arguments, we need to make sure that `hasconditioned_nested` and `getconditioned_nested` behave correctly.
211+
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.
216+
217+
## How do we do it?
218+
219+
`hasconditioned_nested` accomplishes this by doing the following:
220+
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.
223+
224+
We ensure that the context stack is correctly arranged by appropriately setting leaf contexts before evaluating a submodel; see the implementation of `tilde_assume!!` for more details.
225+
226+
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`.
227+
228+
## FixedContext
229+
230+
Finally, note that all of the above also applies to the interaction between `PrefixContext` and `FixedContext`, except that the functions have different names.
231+
(`FixedContext` behaves the same way as `ConditionContext`, except that unlike conditioned variables, fixed variables do not contribute to the log probability density.)

0 commit comments

Comments
 (0)