Skip to content

Conversation

@torfjelde
Copy link
Member

@torfjelde torfjelde commented Jul 8, 2021

This is useful for a couple of reasons:

  1. Less expr-manipulation.
  2. When working with large models it can often be useful to just use @addlogprob! and other fancy stuff to speed things up. In those cases one might want to have different code for whether one is evaluating the model's logjoint or sampling, e.g. if isassumption(...) then do something, otherwise do something else. Hence it's super-convenient if we have a isassumption that the user can call from within the model.

I also checked using #248 and the if-statement indeed compiles away, as expected.

EDIT: We could also add a @isassumption(x[i]) that includes the ismissing, which might be convenient.

src/utils.jl Outdated
Comment on lines 16 to 30
"""
@isassumption(x)
Return `true` if `x` is an assumption and `false` otherwise.
Should only be used within a model-definition.
See also: [`isassumption`](@ref).
"""
macro isassumption(left)
return :(
$(DynamicPPL.isassumption)($(esc(:(__model__))), $(varname(left))) ||
ismissing($(esc(left)))
)
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.

Could also use this in generate_tilde instead of writing $(DynamicPPL.isassumption)(__model__, $vn) || ismissing($left) by hand.

Copy link
Member

Choose a reason for hiding this comment

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

I think this would be good. I would also be easier to modify the check.

Copy link
Member Author

Choose a reason for hiding this comment

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

Doneso.

@torfjelde torfjelde requested a review from devmotion July 8, 2021 01:24
src/compiler.jl Outdated

"""
isassumption(expr)
isassumption(model::Model, vn::VarName[, value])
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 we don't need the version that accepts a value? And we don't want it since otherwise we always have to evaluate the expression?

Copy link
Member Author

@torfjelde torfjelde Jul 8, 2021

Choose a reason for hiding this comment

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

Correct, we don't need it, but I think it's useful for users that wanna go "is this considered an assumption or not?" and they can check using isassumption(model, @varname(x), x).

Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't it be more useful for them if we define

macro isassumption(model, left)
    return isassumption_expr(model, left)
end

macro isassumption(left)
    return isassumption_expr(:__model__, left)
end

function isassumption_expr(model, left)
    return :(
        $(DynamicPPL.isassumption)($(esc(model)), $(esc(varname(left)))) ||
        $(esc(left)) === $(missing)
    )
end

? Then users could just use @isassumption(model, x) instead of having to deal with @varname.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we shouldn't even add the version without model and just always provide __model__ explicitly.

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 I actually did exactly this locally a couple of hours ago, but removed it because it won't work in the repl if left isn't defined, i.e. you can't do the equivalent of isassumption(model, vn) using the macro. This will IMO be more confusing to a user that just wants to check if a particular (model, vn) or (model, vn, value) combination is considered an assumption or not. I also don't think anyone will actually use @isassumption anywhere but within a @model, hence I figured we'd just leave it as is for now and then if someone asks the question we can just add it in.

Copy link
Member Author

@torfjelde torfjelde Jul 8, 2021

Choose a reason for hiding this comment

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

Exactly, we could do isdefined, and I'm happy with that if you prefer 👍

Though I it should be

function isassumption_expr(model, left)
    return if isliteral(left)
        :(false)
    else
        :(
            $(DynamicPPL.isassumption)($(esc(model)), $(esc(varname(left)))) ||
            ($(Expr(:escape, Expr(:isdefined, vsym(left)))) && $(esc(left)) === $(missing))
        )
    end
end

because using @isdefined, it will be expanded in the scope of DynamicPPL:

julia> @isassumption model(4.0) x
false

julia> x = missing
missing

julia> @isassumption model(4.0) x
false

julia> @macroexpand @isassumption model(4.0) x
:((DynamicPPL.isassumption)(model(4.0), (VarName){:x}()) || $(Expr(:isdefined, :(DynamicPPL.x))) && x === missing)

vs.

julia> @isassumption model(4.0) x
true

julia> @macroexpand @isassumption model(4.0) x
:((DynamicPPL.isassumption)(model(4.0), (VarName){:x}()) || $(Expr(:isdefined, :x)) && x === missing)

However, the example also shows that the current logic for isassumption/@isassumption is not really correct. Independent of the macro discussions, I think @isassumption model(4.0) b should return false and @isassumption model(4.0) x should always return false (since in the model x already has the value 4.0).

Agreed. I also stumbled upon another issue: missings as specified in the Model type does not propagate to submodels 😕

I guess #268 would address what you're talking about, but there's still a question of how to support submodels.

But these issues should probably not be addressed in this PR; IMO this PR should just make it somewhat convenient to use the existing functionality of isassumption within a user-model.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, this is definitely outside of the scope of this PR. #268 contains some interesting ideas but my feeling is that it is a quite drastic change and I am not completely convinced that every change is necessary or useful currently (e.g., intuitively I also thought that a lot could be done in a similar way with contexts). Maybe it would be better to change things more gradually, possibly also in a less breaking way.

I also just noticed that you dropped the check for inmissings, it seems? I think it would be nice to keep the API simple and only expose the macro version and drop the isassumption(model, vn, value) functions. On a second thought, I guess it would be helpful to have both a version with model argument (for checks in the REPL) and one without that uses __model__ (simplifies the use in the @model macro). Maybe we could just use

macro isassumption(left, model=:__model__)
    return isassumption(left, model)
end

function isassumption(left, model)
    varname = DynamicPPL.varname(left)
    vsym = DynamicPPL.vsym(left)
    return quote
        !inargnames($varname, $model) || inmissings($varname, $model) || (@isdefined($vsym) && $vsym === missing)
    end
end

Then we (and users) could use @isassumption(left) in the model and it should be equivalent to what we use currently, and one could still check @isassumption left model in the REPL (with the known flaws, but it would still be useful if used properly).

Copy link
Member Author

@torfjelde torfjelde Jul 9, 2021

Choose a reason for hiding this comment

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

my feeling is that it is a quite drastic change and I am not completely convinced that every change is necessary or useful currently

Agreed, but I've become more intrigued recently as there are some niceties that you get as a result, e.g. I don't think we can handle the failure case you noted above with b not in the model without an approach similar to that PR. But again, unclear if it's worth it given the major change it requires, and even if it achieves what we want (e.g. allow impl of decondition and condition). But I'll leave this discussion for that PR 👍

I also just noticed that you dropped the check for inmissings, it seems? I think it would be nice to keep the API simple and only expose the macro version and drop the isassumption(model, vn, value) functions. On a second thought, I guess it would be helpful to have both a version with model argument (for checks in the REPL) and one without that uses model (simplifies the use in the @model macro). Maybe we could just use

I didn't drop it, it's just hidden in the isassumption(model, vn), but I agree that it should just all be in the macro instead 👍

But I prefer @isassumption(x) and @isassumption(model, x) rather than @isassumption(x, model), i.e.

macro isassumption(left)
    return isassumption(:(__model__), left)
end

macro isassumption(model, left)
    return isassumption(model, left)
end

function isassumption(model, left)
    varname = DynamicPPL.varname(left)
    vsym = DynamicPPL.vsym(left)
    return quote
        !inargnames($varname, $model) || inmissings($varname, $model) || (@isdefined($vsym) && $vsym === missing)
    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.

I went with using isassumption in the model, which also allow passing symbol that evaluates to varname(left). This makes the expanded code somewhat less of a mouthful, in an attempt to alleviate the noise introduced by @isdefined (compared to old version).

Copy link
Member Author

@torfjelde torfjelde Jul 9, 2021

Choose a reason for hiding this comment

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

It does have the drawback that @isassumption isn't being used anywhere in the code though, so maybe I should add a test?

EDIT: Eh, you know what, I'll just use the macro as it will make things easier to maintain.

src/compiler.jl Outdated
2. `x` is among the input data to the model but with a value `missing`, or
3. `x` is among the input data to the model with a value other than missing,
2. `x` is among the input data to the model but with `value === missing`, or
3. `x` is among the input data to the model with a `value !== missing`,
Copy link
Member

Choose a reason for hiding this comment

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

This is not true?

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right 😕 I just matched the existing docs, but a bit confused as to why this was there.

Copy link
Member Author

Choose a reason for hiding this comment

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

Removed (3) now.

src/compiler.jl Outdated
Comment on lines 16 to 17
isassumption(model::Model, vn::VarName, value::Missing) = true
isassumption(model::Model, vn::VarName, value) = isassumption(model, vn)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
isassumption(model::Model, vn::VarName, value::Missing) = true
isassumption(model::Model, vn::VarName, value) = isassumption(model, vn)

src/utils.jl Outdated
Should only be used within a model-definition.
See also: [`isassumption`](@ref).
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we should explain more clearly what the differences are and when you should use isassumption and when @isassumption.

Copy link
Member Author

Choose a reason for hiding this comment

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

Improved the docstring; is current okay?

src/utils.jl Outdated
Comment on lines 16 to 30
"""
@isassumption(x)
Return `true` if `x` is an assumption and `false` otherwise.
Should only be used within a model-definition.
See also: [`isassumption`](@ref).
"""
macro isassumption(left)
return :(
$(DynamicPPL.isassumption)($(esc(:(__model__))), $(varname(left))) ||
ismissing($(esc(left)))
)
end
Copy link
Member

Choose a reason for hiding this comment

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

I think this would be good. I would also be easier to modify the check.

@torfjelde
Copy link
Member Author

Tests are passing btw.

@torfjelde torfjelde mentioned this pull request Jul 8, 2021
@torfjelde
Copy link
Member Author

torfjelde commented Jul 9, 2021

Btw @devmotion, I also added support for expressions such as x[:, i] which have been missing, e.g. the following works nicely now:

 julia> @model function demo2(x, y)
           m ~ Normal()
           x ~ Normal(m, 1.0)
           y[1] ~ Normal(x, 1.0)

           if length(y) > 1
               y[2:end] ~ MvNormal(length(y) - 1, 1.0)
           end

           return (; m, x, y)
       end
demo2 (generic function with 1 method)

julia> m = demo2(missing, [missing, 2.0, 3.0]);

julia> m()
(m = 0.4976316735942513, x = 0.44900997623088473, y = [0.2536455471649265, 2.0, 3.0])

julia> m = demo2(missing, [1.0, missing, missing]);

julia> m()
(m = -1.1722550192844885, x = -1.0166096659206363, y = Union{Missing, Float64}[1.0, 3.4953329133243147, -1.697987882528696])
julia> m = demo2(missing, [missing, missing, missing]);

julia> m()
(m = 1.065221683727756, x = 1.4561957002666124, y = Union{Missing, Float64}[1.327755997039602, 2.6958766955778954, -0.7770607374830496])

It of course isn't deducible at compile-time for those particular cases, but I think it's better to fall back to this rather than not having the feature + some ambiguous error message about a certain type not being supported in logpdf.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
function isassumption(expr::Union{Symbol,Expr})
vn = gensym(:vn)
macro isassumption(left)
return esc(isassumption(:(__model__), left))
Copy link
Member

Choose a reason for hiding this comment

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

Could we use the other approach and only escape whatever has to be escaped? Usually this is safer and one does not require to interpolate DynamicPPL in the expressions.

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 did that initially but kept finding cases where things would break so I just gave up 😅

I guess I'll try again.

Copy link
Member Author

@torfjelde torfjelde Jul 19, 2021

Choose a reason for hiding this comment

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

I don't think we can avoid escaping the full expression if we want to use it within another macro 😕
E.g. addlogprob! and others also do this.


sym = vsym(left)
return :(
(!$(DynamicPPL.inargnames)($vn, $model) || $(DynamicPPL.inmissings)($vn, $model)) ||
Copy link
Member

Choose a reason for hiding this comment

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

See above, maybe we could avoid having to interpolate DynamicPPL if we only escape the relevant parts?

src/compiler.jl Outdated
is_entirely_missing(x) = false
function is_entirely_missing(x::AbstractArray{>:Missing})
missings = missing .=== x
if all(missings)
Copy link
Member

Choose a reason for hiding this comment

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

Just check count(===(missing), x) to avoid the allocation and checking both all and any?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea 👍

src/compiler.jl Outdated
julia> @macroexpand @isassumption(x)
:((!((DynamicPPL.inargnames)((VarName){:x}(), __model__)) || (DynamicPPL.inmissings)((VarName){:x}(), __model__)) || $(Expr(:isdefined, :x)) && x === missing)
julia> @macroexpand @isassumption m x
Copy link
Member

Choose a reason for hiding this comment

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

I guess it would be better to check the intended behaviour - the macro expansion is difficult to read and easy to break.

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 was debating that myself, and happy to do what you suggest 👍

pointwise_loglikelihoods,
# Convenience macros
@addlogprob!,
@isassumption,
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 need to export @isassumption? It seems it is only used internally by the compiler.

Copy link
Member Author

Choose a reason for hiding this comment

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

The motivation of this PR is to provide something that the end-user can use, e.g. if you really want to squeeze out performance you might want to do something like

if @isassumption(x)
    x ~ Dist()
else
    @addlogprob! f(x)
end

There currently is no good way of doing this, hence this PR.

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 also think it's useful to expose a macro that users can use to check whether or not something is treated as an assumption, even when not used within @model. People are often confused by this 😕 But this is not the main-motivation behind the PR; the above is.

@torfjelde
Copy link
Member Author

Closed in favour of #301 which introduces the contextual_isassumption which in turn covers most of the usecases.

@torfjelde torfjelde closed this Aug 14, 2021
@yebai yebai deleted the tor/isassumption branch January 31, 2022 20:01
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.

4 participants