Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ on:
pull_request:

concurrency:
group: docs
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}

permissions:
contents: write
Expand Down
781 changes: 317 additions & 464 deletions Manifest.toml

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Bijectors = "76274a88-744f-5084-9051-94815aaf08c4"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
Chairmarks = "0ca39b1e-fe0b-4e98-acfc-b1656634c4de"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DifferentialEquations = "0c46a032-eb83-5123-abaf-570d42b7fbaa"
DelayDiffEq = "bcd4f6db-9728-5f36-b5f7-82caef46ccdb"
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
DynamicHMC = "bbc10e6e-7c05-544b-b16e-64fede858acb"
Expand All @@ -27,7 +27,6 @@ LogExpFunctions = "2ab3a3ac-af41-5b50-aa03-7779005ae688"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Lux = "b2108857-7c20-44ae-9111-449ecde12c47"
MCMCChains = "c7f686f2-ff18-58e9-bc7b-31028e88f75d"
MLDataUtils = "cc2ba9b6-d476-5e6d-8eaf-a92d5412d41d"
MLUtils = "f1d291b0-491e-4a28-83b9-f70985020b54"
MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09"
Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e"
Expand All @@ -36,6 +35,7 @@ NNlib = "872c559c-99b0-510c-b3b7-b6c96a88d5cd"
Optimisers = "3bd65402-5787-11e9-1adc-39752487f4e2"
OptimizationNLopt = "4e6fcdb7-1186-4e1f-a706-475e75c168bb"
OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e"
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
PDMats = "90014a1f-27ba-587c-ab20-58faa44d9150"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
Expand All @@ -51,4 +51,4 @@ StatsPlots = "f3b207a7-027a-5e70-b257-86293d7955fd"
Turing = "fce5fe82-541a-59a6-adf8-730c64b5f9a0"

[compat]
Turing = "0.42"
Turing = "0.43"
3 changes: 1 addition & 2 deletions _quarto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ website:
href: https://turinglang.org/team/
right:
# Current version
- text: "v0.42"
- text: "v0.43"
menu:
- text: Changelog
href: https://turinglang.org/docs/changelog.html
Expand Down Expand Up @@ -231,4 +231,3 @@ dev-transforms-distributions: developers/transforms/distributions
dev-transforms-bijectors: developers/transforms/bijectors
dev-transforms-dynamicppl: developers/transforms/dynamicppl
dev-contexts-submodel-condition: developers/contexts/submodel-condition
dev-models-varinfo-overview: developers/models/varinfo-overview
7 changes: 4 additions & 3 deletions developers/compiler/model-manual/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -30,23 +30,24 @@ However, models can be constructed by hand without the use of a macro. Taking th

```{julia}
using DynamicPPL
using DynamicPPL.VarNamedTuples: NoTemplate

# Create the model function.
function gdemo2(model, varinfo, x)
# Assume s² has an InverseGamma distribution.
s², varinfo = DynamicPPL.tilde_assume!!(
model.context, InverseGamma(2, 3), @varname(s²), varinfo
model.context, InverseGamma(2, 3), @varname(s²), NoTemplate(), varinfo
)

# Assume m has a Normal distribution.
m, varinfo = DynamicPPL.tilde_assume!!(
model.context, Normal(0, sqrt(s²)), @varname(m), varinfo
model.context, Normal(0, sqrt(s²)), @varname(m), NoTemplate(), varinfo
)

# Observe each value of x[i] according to a Normal distribution.
for i in eachindex(x)
_retval, varinfo = DynamicPPL.tilde_observe!!(
model.context, Normal(m, sqrt(s²)), x[i], @varname(x[i]), varinfo
model.context, Normal(m, sqrt(s²)), x[i], @varname(x[i]), x, varinfo
)
end

Expand Down
86 changes: 47 additions & 39 deletions developers/contexts/submodel-condition/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,11 @@ The phrase 'becoming' a different variable is a little underspecified: it is use
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`.
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 after `tilde_assume(::PrefixContext, ...)` will see `a.x` instead.

## ConditionContext
## CondFixContext

`ConditionContext` is a context which stores values of variables that are to be conditioned on.
These values may be stored as a `Dict` which maps `VarName`s to values, or alternatively as a `NamedTuple`.
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).
Because of this limitation, we will only use `Dict` in this example.

::: {.callout-note}
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.
:::
`CondFixContext` is a context which stores values of variables that are to be conditioned or fixed on.
It takes a single type parameter, which is either `DynamicPPL.Condition` or `DynamicPPL.Fix`, which indicates whether the context is for conditioning or fixing.
These values are internally stored as a `VarNamedTuple.`

One can inspect the conditioning values with, for example:

Expand All @@ -64,12 +59,15 @@ One can inspect the conditioning values with, for example:
end

cond_model = d() | (@varname(x) => 1.0)
cond_ctx = cond_model.context

cond_values = conditioned(cond_model)
```

There are several internal functions that are used to determine whether a variable is conditioned, and if so, what its value is.

```{julia}
cond_ctx = cond_model.context

DynamicPPL.hasconditioned_nested(cond_ctx, @varname(x))
```

Expand All @@ -88,15 +86,20 @@ Notice that (neglecting `missing` values) the return value of `contextual_isassu
:::

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).
Since `x` is no longer a random variable, if we were to evaluate the model, we would find only one key in the `VarInfo`:
Since `x` is no longer a random variable, if we were to evaluate the model, we would find only one key:

```{julia}
keys(VarInfo(cond_model))
rand(cond_model)
```

## Joint behaviour: desiderata at the model level

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`.
:::{.callout-note}
The same points apply to both conditioning and fixing, so we will discuss it generically in terms of `CondFixContext`.
The code examples will use conditioning to demonstrate this.
:::

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 `CondFixContext`.

We begin by mentioning some high-level desiderata for their joint behaviour.
Take these models, for example:
Expand Down Expand Up @@ -127,9 +130,9 @@ with_inner_cond = outer2()

We want that:

1. `keys(VarInfo(outer()))` should return `[a.x, a.y]`;
2. `keys(VarInfo(with_outer_cond))` should return `[a.y]`;
3. `keys(VarInfo(with_inner_cond))` should return `[a.y]`,
1. `keys(rand(outer()))` should return `[a.x, a.y]`;
2. `keys(rand(with_outer_cond))` should return `[a.y]`;
3. `keys(rand(with_inner_cond))` should return `[a.y]`,

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

Expand All @@ -153,18 +156,18 @@ We do not specify the implementation details here, but we will sketch out someth
**Points (2) and (3)** are more tricky.
As the reader may surmise, the difference between them is the order in which the contexts are stacked.

For the _outer_ conditioning case (point (2)), the `ConditionContext` will contain a `VarName` that is already prefixed.
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.
We make the claim here that the best way to do this is to nest the `PrefixContext` _inside_ the `ConditionContext`.
For the _outer_ conditioning case (point (2)), the `CondFixContext` will contain a `VarName` that is already prefixed.
When we enter the inner submodel, this `CondFixContext` has to be passed down and somehow combined with the `PrefixContext` that is created when we enter the submodel.
We make the claim here that the best way to do this is to nest the `PrefixContext` _inside_ the `CondFixContext`.
This is indeed what happens, as can be demonstrated by running the model.

```{julia}
with_outer_cond()
```

For the _inner_ conditioning case (point (3)), the outer model is not run with any special context.
The inner model will itself contain a `ConditionContext` will contain a `VarName` that is not prefixed.
When we run the model, this `ConditionContext` should be then nested _inside_ a `PrefixContext` to form the final evaluation context.
The inner model will itself contain a `CondFixContext` will contain a `VarName` that is not prefixed.
When we run the model, this `CondFixContext` should be then nested _inside_ a `PrefixContext` to form the final evaluation context.
Again, we can run the model to see this in action:

```{julia}
Expand All @@ -174,13 +177,17 @@ with_inner_cond()
Putting all of the information so far together, what it means is that if we have these two inner contexts (taken from above):

```{julia}
using DynamicPPL: PrefixContext, ConditionContext, DefaultContext
using DynamicPPL: PrefixContext, DefaultContext, CondFixContext, Condition

inner_ctx_with_outer_cond = ConditionContext(
Dict(@varname(a.x) => 1.0), PrefixContext(@varname(a))
)
inner_ctx_with_inner_cond = PrefixContext(
@varname(a), ConditionContext(Dict(@varname(x) => 1.0))
outer_cond = @vnt begin
a.x := 1.0
end
inner_ctx_with_outer_cond = CondFixContext{Condition}(outer_cond, PrefixContext(@varname(a)))

inner_cond = @vnt begin
x := 1.0
end
inner_ctx_with_inner_cond = PrefixContext(@varname(a), CondFixContext{Condition}(inner_cond)
)
```

Expand All @@ -207,7 +214,7 @@ This allows us to finally specify our task as follows:
## How do we do it?

(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.
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.
Once the `PrefixContext`s have been removed, one can then iterate through the context stack and check if any of the `CondFixContext`s contain the variable, or get the value itself.
For more details the reader is encouraged to read the source code.

(2a) We ensure that the context stack is correctly arranged by relying on the behaviour of `make_evaluate_args_and_kwargs`.
Expand All @@ -221,7 +228,7 @@ This is done inside the `@model` macro, or technically, its subsidiary function

## Nested submodels

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.
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 `CondFixContext`s.
For example, in this series of nested submodels,

```{julia}
Expand All @@ -248,11 +255,18 @@ The general strategy that we adopt is similar to above.
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:

```{julia}
vnt_by = @vnt begin
b.y := 1.0
end
vnt_x = @vnt begin
x := 1.0
end

big_ctx = PrefixContext(
@varname(a),
ConditionContext(
Dict(@varname(b.y) => 1.0),
PrefixContext(@varname(b), ConditionContext(Dict(@varname(x) => 1.0))),
CondFixContext{Condition}(
vnt_by,
PrefixContext(@varname(b), CondFixContext{Condition}(vnt_x)),
),
)
```
Expand Down Expand Up @@ -303,7 +317,7 @@ When editing this code, it is worth being mindful of this as a potential source
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.
:::

## Loose ends 1: Manual prefixing
## Loose end: Manual prefixing

Sometimes users may want to manually prefix a model, for example:

Expand Down Expand Up @@ -337,9 +351,3 @@ prefixed_model = prefix(model, :a)

(model.context, prefixed_model.context)
```

## Loose ends 2: FixedContext

Finally, note that all of the above also applies to the interaction between `PrefixContext` and `FixedContext`, except that the functions have different names.
(`FixedContext` behaves the same way as `ConditionContext`, except that unlike conditioned variables, fixed variables do not contribute to the log probability density.)
This generally results in a large amount of code duplication, but the concepts that underlie both contexts are exactly the same.
Loading
Loading