Skip to content

Conversation

@torfjelde
Copy link
Member

No description provided.

torfjelde and others added 30 commits July 15, 2021 20:08
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Copy link
Member

@yebai yebai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm halfway through reviewing this PR; will add more comments after dinner.

Comment on lines 107 to 109
contextualize,
observations,
conditioned,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually want to export these functions?

Suggested change
contextualize,
observations,
conditioned,

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was intentional if that's what you mean, in particular I think people will want the observations / conditioned method. With that being said, maybe we shouldn't export in the next release but delay it until we've tried it out a bit and are happy with it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think we want to apply the principle that we only export functions with a strong use-case. It would help to avoid frequent breaking changes and/or maintenance burdens since we are still experimenting with things.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good 👍

function contextual_isassumption(context::AbstractContext, vn)
return contextual_isassumption(NodeTrait(context), context, vn)
end
function contextual_isassumption(context::ConditionContext, vn)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation of contextual_isassumption looks slightly inconsistent because it dispatches using both contexts and traits. I think this is fine for now, but we probably want to consider improving it in future PRs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit uncertain what you mean here 😕 This pattern we're using all over the place. Traits represents a default implementation, but we we still allow direct overloads, e.g. here ConditionContext and PrefixContext needs special handling but the rest of the contexts can use the trait-implementations.

Notice how even the trait-impls above call the impl without the trait argument, thus allowing these specific overloads.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure - mixing traits and types kind of makes the code hard to read. But I'm happy to leave this as a future effort, given that the functionality is correct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure 👍

# In this case, the below will return `true` since the first branch
# will be hit.
# 3. We are working with a `ConditionContext` _and_ it's in the model arguments,
# i.e. we're trying to override the value. This is currently NOT supported.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by what will happen in the 3rd situation: will the current implementation error/warn, or it will fail/override silently?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, no this just means that in this scenario ConditionContext won't have any effect, i.e.

@model function demo(x)
    x ~ Normal()
end
m = demo(missing)
(m | (x = 1.0))() # <= `x` is still considered `missing` because we can't change variables used in args through `condition`

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, let us add this to the docstring since it makes things clearer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do 👍

I'll also add tests now that we've landed on taking this approach.

Co-authored-by: Hong Ge <hg344@cam.ac.uk>
Copy link
Member

@yebai yebai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work, I left many comments below. Overall, I think the PR implemented condition/deconditioning in a nice non-breaking way.

I think the mixed-use of traits and nested contexts requires some further discussions, but happy to do that in a separate effort.

Comment on lines 107 to 109
contextualize,
observations,
conditioned,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think we want to apply the principle that we only export functions with a strong use-case. It would help to avoid frequent breaking changes and/or maintenance burdens since we are still experimenting with things.

# In this case, the below will return `true` since the first branch
# will be hit.
# 3. We are working with a `ConditionContext` _and_ it's in the model arguments,
# i.e. we're trying to override the value. This is currently NOT supported.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, let us add this to the docstring since it makes things clearer.

function contextual_isassumption(context::AbstractContext, vn)
return contextual_isassumption(NodeTrait(context), context, vn)
end
function contextual_isassumption(context::ConditionContext, vn)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite sure - mixing traits and types kind of makes the code hard to read. But I'm happy to leave this as a future effort, given that the functionality is correct.

require_particles(spl::Sampler) = false

_getindex(x, inds::Tuple) = _getindex(view(x, first(inds)...), Base.tail(inds))
_getindex(x, inds::Tuple) = _getindex(Base.maybeview(x, first(inds)...), Base.tail(inds))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add some doc on what are the possible x types to improve clarity.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All types are allowed with maybeview:) Also this is more properly addressed in #295

end
function tilde_observe(context::MiniBatchContext, right, left, vname, vi)
return context.loglike_scalar * tilde_observe(context.context, right, left, vname, vi)
function tilde_observe(context::MiniBatchContext, sampler, right, left, vi)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we are missing the argument vname here..

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is actually because we never use tilde_observe with vname. See impl of tilde_observe!.

This is also why this passed through the tests; we have absolutely no tests for the vname-included versions (and some of contexts are missing impls for this even on master).

values::NamedTuple{Names}, context::ConditionContext
) where {Names}
# Note that this potentially overrides values from `context`, thus giving
# precedence to the outmost `ConditionContext`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we at least throw a warning if overrides happen here? It might cause very subtle bugs otherwise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning will slow things down + I don't think it should warn because what we're doing here is setting the conditioned value, right? And so if you're setting something, you expect that value to be the one that ends up in the model. This is the case with the above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also IMO, a seperate note, any such logic should go into condition and/or decondition, not ConditionContext constructor.

src/contexts.jl Outdated
getvalue(context, vn)
Return the value of the parameter corresponding to `vn` from `context`.
If `context` does not contain the value for `vn`, then `nothing` is returned,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If context does not contain the value for vn, then nothing is returned,

Do we need to distinguish from the case where the value of a variable is missing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah, getvalue will return missing if missing is indeed the value, not otherwise (and nor should it).

IMO we should actually just error here rather than return nothing 👍

Comment on lines +325 to +326
condition(; values...) = condition(DefaultContext(), (; values...))
condition(values::NamedTuple) = condition(DefaultContext(), values)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comment: maybe do not provide these convenient condition functions, but always requiring the user to pass a context instead. Semantically, it feels a bit odd to condition on a set of variables without a context/model.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sort of view it as an alternative constructor of ConditionContext and I don't expect condition to be used externally on anything except with Model. But I agree that the semantics are a bit weird.

How about this? We make ConditionContext callable!

julia> @model function demo()
           x ~ Normal()
           return x
       end
demo (generic function with 1 method)

julia> m = demo();

julia> m()
-0.5714991589000286

julia> cm = m |> condition(x=1.0);

julia> cm()
1.0

This is actually pretty neat, because I can easily imagine a pattern where people have some specific conditioning they want to do on several instances of a model, e.g. different parameterizations, etc., in which case it makes sense to do something like

julia> conditioner = condition(x=1.0);

julia> conditioner(m)()
1.0

# is that the outermost `context` takes precendence, hence when resolving
# the `conditioned` variables we need to ensure that `context.values` takes
# precedence over decendants of `context`.
return merge(context.values, conditioned(childcontext(context)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's throw a warning here when overrides happen.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it's a bit strange to warn only when the user try to retrieve the conditioned values, no? If we're going to warn, I prefer you previous suggestion of doing so in condition instead. I'm still a bit on the edge regarding that though because it means that if you intentially want to override a value, you'll see warnings everytime you use the context + it will have a significant performance impact.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I.e. we really need a opt-out mechanism if we add it + I'm saying that condition is a setter and hence we shouldn't be surprised if a value is replaced.

Comment on lines 290 to 352
"""
hasvalue(context, vn)
Return `true` if `vn` is found in `context`.
"""
hasvalue(context, vn) = false

function hasvalue(context::ConditionContext{vars}, vn::VarName{sym}) where {vars,sym}
return sym in vars
end
function hasvalue(
context::ConditionContext{vars}, vn::AbstractArray{<:VarName{sym}}
) where {vars,sym}
return sym in vars
end

"""
getvalue(context, vn)
Return value of `vn` in `context`.
"""
function getvalue(context::AbstractContext, vn)
return error("context $(context) does not contain value for $vn")
end
getvalue(context::ConditionContext, vn) = _getvalue(context.values, vn)

"""
hasvalue_nested(context, vn)
Return `true` if `vn` is found in `context` or any of its descendants.
"""
function hasvalue_nested(context::AbstractContext, vn)
return hasvalue_nested(NodeTrait(hasvalue_nested, context), context, vn)
end
hasvalue_nested(::IsLeaf, context, vn) = hasvalue(context, vn)
function hasvalue_nested(::IsParent, context, vn)
return hasvalue(context, vn) || hasvalue_nested(childcontext(context), vn)
end
function hasvalue_nested(context::PrefixContext, vn)
return hasvalue_nested(childcontext(context), prefix(context, vn))
end

"""
getvalue_nested(context, vn)
Return the value of the parameter corresponding to `vn` from `context` or its descendants.
"""
function getvalue_nested(context::AbstractContext, vn)
return getvalue_nested(NodeTrait(getvalue_nested, context), context, vn)
end
function getvalue_nested(::IsLeaf, context, vn)
return error("context $(context) does not contain value for $vn")
end
function getvalue_nested(context::PrefixContext, vn)
return getvalue_nested(childcontext(context), prefix(context, vn))
end
function getvalue_nested(::IsParent, context, vn)
return if hasvalue(context, vn)
getvalue(context, vn)
else
getvalue_nested(childcontext(context), vn)
end
end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be possible to do away with the "nested" versions (though they might be useful in other case) if we make isassumption also return whatever value we're going to use. See my comment on isassumption for more info.

Comment on lines 37 to 44
if !($(DynamicPPL.inargnames)($vn, __model__)) ||
$(DynamicPPL.inmissings)($vn, __model__)
true
else
$expr === missing
end
else
# Evaluate the LHS
$(maybe_view(expr)) === missing
false
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we wanted to do away with the getvalue_nested and hasvalue_nested (again, these are convenient to have even if we're not using them here but obviously their utility is reduced significantly), we could make this return both the Bool corresponding to whether it's an assumption and maybe a value, i.e. nothing or the value we've extracted from context. It would end up looking something like

function isassumption(expr::Union{Symbol,Expr})
    vn = gensym(:vn)
    val = gensym(:vn)

    return quote
        let $vn = $(varname(expr)), $val = $(DynamicPPL.contextual_isassumption)(__context__, $vn)
            if $val !== missing
                if !($(DynamicPPL.inargnames)($vn, __model__)) ||
                   $(DynamicPPL.inmissings)($vn, __model__)
                    true
                else
                    $expr === missing ? (true, missing) : (false, $val)
                end
            else
                false, missing
            end
        end
    end
end

with contextual_isassumption now returning missing if it's considered an assumption or anything else if it's considered an observation.

Similarly we'd have to add an additional variable in generate_tilde, etc. which is used if we hit the observe branch and the value return from isassumption is not missing.

All in all, IMO it ends up looking more complicated than the current implementation of getvalue_nested, etc. so I'm not in favour of doing it but wanted to let people know it's an option.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good!

@yebai
Copy link
Member

yebai commented Aug 4, 2021

bors try

bors bot added a commit that referenced this pull request Aug 4, 2021
@bors
Copy link
Contributor

bors bot commented Aug 4, 2021

try

Build failed:

@torfjelde
Copy link
Member Author

Format will fail here because of domluna/JuliaFormatter.jl#449 (comment)

@torfjelde
Copy link
Member Author

Btw, I've left some responses to your comments.

Also, I need to add some tests for condition and decondition on models just to be sure that it interacts nicely with submodels, etc. This should already be covered by the tests for the context, but I just want to be 100% certain.

@bors bors bot deleted the branch tor/context-traits August 5, 2021 12:32
@bors bors bot closed this Aug 5, 2021
@torfjelde
Copy link
Member Author

It seems like bors mistakenly closed this issue o.O I'll push the branch to remote again, and complete it.

bors bot pushed a commit that referenced this pull request Aug 14, 2021
This PR introduces `condition` and `decondition`. This is really just a reopening of #294 that I can't reopen directly due to the target branch now being deleted.

Co-authored-by: Hong Ge <hg344@cam.ac.uk>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants