TuringLang / TuringLang/DynamicPPL.jl

Handle fixing and conditioning together

Open
#1,012 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Julia
Stars
286
Forks
41
Avg merge
1d 11h
Merged PRs (30d)
34

Description

Consider:

using DynamicPPL, Distributions

for op1 in [condition, fix]
    for op2 in [condition, fix]
        @model g() = x ~ Normal()
        g_op = op1(g(), (; x = 1.0))
        @model f() = a ~ to_submodel(g_op)
        f_op = op2(f(), (@varname(a.x) => 2.0))
        println("$op1 $op2 => $(f_op())")
    end
end

The result (last tested on DynamicPPL 0.41):

condition condition => 2.0
condition fix => 2.0
fix condition => 1.0
fix fix => 2.0

For fixfix and concon, there's no issue because the values are merged in a way that respects the outer. (it's somewhere in getconditioned / getfixed I think)

For confix and fixcon, unfortunately, the logic in the codebase says that fixing always takes precedence over conditioning, because fix is in the first branch and condition is in the last branch

https://github.com/TuringLang/DynamicPPL.jl/blob/0b7213f071cb27ca80ee8b6d4fee2bae8fdec693/src/compiler.jl#L448-L461

I discovered while writing tests for nested condition/fix. This was the test I had intended to add to test/submodel.jl.

    @testset "precedence for conditioning/fixing the same variable twice" begin
        # Check that the outermost conditioning takes precedence. I'm not sure
        # if there is a strong a priori reason for this, but it is currently
        # the case and thus this test just ensures that the behaviour doesn't
        # suddenly change without it being a conscious decision to do so.
        @testset "$op1,$op2" for op1 in [condition, fix], op2 in [condition, fix]
            @model function f()
                x ~ Normal()
            end
            fcond = op1(f(), (; x=1.0)) # this is ignored
            @model function g()
                return a ~ to_submodel(fcond)
            end
            gcond = op2(g(), (@varname(a.x) => 2.0)) # this takes precedence
            @test gcond() == 2.0
        end
    end

How to solve?

Right now, conditioning and fixing go via what's now called CondFixContext. However, the conditioned values and fixed values are still stored in separate 'layers' of this.

The way to prevent this would be to 'flatten' the layers such that conditioned variables and fixed variables are part of the same VarNamedTuple. They would be held inside a VNT together with a flag that indicates whether it's conditioned or fixed. Thus instead of having a data structure that holds two VNTs, one conditioned and one fixed, we would only have one VNT that holds both.

We could then use templated_setindex_no_overwrite!! or something similar to issue an error when attempting to fix a variable that is already conditioned.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/compiler.jl lines 448-461 and trace the getconditioned/getfixed logic for nested condition and fix operations. Review the intended regression test in test/submodel.jl and the existing CondFixContext representation. Done means the outermost conditioning or fixing takes precedence consistently for all four operation pairs, with tests covering the behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
julia
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.