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