Skip to content

Drop .~ syntax entirely #825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
yebai opened this issue Feb 28, 2025 · 5 comments
Open

Drop .~ syntax entirely #825

yebai opened this issue Feb 28, 2025 · 5 comments

Comments

@yebai
Copy link
Member

yebai commented Feb 28, 2025

The .~ syntax allows one to define parameters in a vectorised manner:

x = Vector(undef, 4) 
x .~ Distribution()

#804 introduced extra constraints on the RHS: it has to be univariate distributions. This makes the need for the broadcasting syntax . questionable: it is much less flexible than Julia's broadcasting. So, using the broadcasting .~ might be confusing actually. I think we should further simplify vectorised tilde into

x = Vector(undef, 4) 
x ~ UnivariateDistribution()

This is semantically equivalent to x ~ filldist(UnivariateDistribution(), 4) but with a cleaner syntax. It also provides a clean syntax for supporting (univariate) identically-independently-distributed (IID) distributions discussed previously.

This proposal is also consistent with the Stan syntax for vectorised random variables.

@mhauru
Copy link
Member

mhauru commented Feb 28, 2025

I would find it semantically confusing to say that a vector is distributed according to a univariate distribution. This also goes strongly against the convention of Julia where you have to use broadcasted . operations to e.g. apply binary operations with a scalar element-wise, or assign elements of a vector as in a = Vector(undef, 4); a .= 0.0. Many other languages allow things like randn(3) + 1.0 but Julia has made the explicit decision to forbid it, and I think it's been a good decision. I don't think the . in .~ complicates our interface much, and it does keep the distinction between arrays/scalars and univariates/multivariates more clear and much more in line with the rest of Julia.

I appreciate that our .~ doesn't implement the full power of usual Julia broadcasting with ., but it does implement a subset of the functionality one would expect from ., and when it works it behaves consistently with Julia's standard . broadcasting. In other cases it errors in a helpful manner. I think this is preferable to having a syntax that goes against the conventions of how broadcasting and mixing scalars and vectors works in Julia.

@penelopeysm
Copy link
Member

penelopeysm commented Feb 28, 2025

I'm not opposed to the syntax change, but I'm unsure about how to implement this because at macro time we can't determine whether we need to use filldist or not

@model function f()
    y ~ Normal()
    x = Array{Float64}(undef, 2, 3)
    x ~ Normal()
end

Here we don't want to use filldist for y, but we want to expand the x to filldist(Normal(), 2, 3), and I don't immediately see how we can differentiate between this at macro time. One possible solution would be to always use filldist - i.e. for y we would expand to filldist(Normal(), 1) - but that might be undesirable from a performance point of view.

.~ lets us escape this ambiguity because we assume that anything on the lhs of .~ needs filldist and anything that isn't doesn't need it.

I haven't thought about it super deeply yet though

@penelopeysm
Copy link
Member

If we had @parameters struct that might be doable ;D

@yebai
Copy link
Member Author

yebai commented Feb 28, 2025

We could consider contributing a new iid_distribution(dist::UnivariateDistribution, N::Int) to the Distributions.jl package or keep it inside DynamicPPL initially. Then, we can have

y ~ Normal()  # standard univariate RV
x ~ iid_distribution(Normal(), 2, 3)  # similar to `.~` 

This would allow us to replace .~ with iid_distribution and customise condition / fix / etc model operations for iid_distribution.

@torfjelde
Copy link
Member

I appreciate that our .~ doesn't implement the full power of usual Julia broadcasting with ., but it does implement a subset of the functionality one would expect from ., and when it works it behaves consistently with Julia's standard . broadcasting.

Not particularly relevant to the full discussion, but to add to your commeont here @mhauru: it's also worth noting that some things that would make sense if we followed broadcasting semantics in Julia fully doesn't quite make sense in a @model.

An example is

    x = Matrix(undef, 2, 1)
    x .~ reshape(fill(Normal(), 2), 1, 2)

This would technically work in Turing, but results in sampling both of the components of x twice and accumulating that into logp. Simiarly during logp-evaluation, you'd double-count x[1, 1] and x[2, 1] in your logp.

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

No branches or pull requests

4 participants