But it turns out not that easy. I cannot understand how to “convert” type of object_dist replacing all Distributions with floats, and how to build a (nested) struct from a flat vector of values, filling in only some of the fields - those that are specified as Distributions.
Any ideas on how to do that, or maybe design something similar, but simpler?
using Distributions
struct MappedDistribution{F, D <: Tuple}
f::F
parameter_distributions::D
end
function MappedDistribution(f, args...)
MappedDistribution(f, args)
end
function Base.rand(o::MappedDistribution)
params = map(rand, o.parameter_distributions)
o.f(params...)
end
struct MyObject
field_one
field_two
field_three
end
d = MappedDistribution(MyObject,
Normal(0, 2),
MappedDistribution(tuple, Uniform(-2, 1), Normal(0, 1)),
MappedDistribution(() -> 5)
)
rand(d)
#MyObject(0.7235862717111876, (0.8576196118399584, -1.0640162037995087), 5)
If you want keyword arguments I would use QuickTypes or Parameters and just add ;kw... to to the mapped distribution constructor.
Personally I would only allow distribution arguments. If you really really want to be able to pass 5, introduce another indirection:
_lift(x) = Singleton(x)
_lift(x::Distribution) = x
nparams is easy to define. param_dists can be defined recursively. from_params seems tricky. Also for what do you need these functions for? I suspect you are really after something slightly different.
First of all, thanks for you answer!
Well, maybe you are correct that I really need something different, but this was the most straightforward way I could think of. The actual problem is that I have a model like the following:
struct Params
# parameters, both plain Floats, tuples, and a few nested structs
...
end
function evaluate(p::Params, x::Float)
...
end
function loglike(p::Params, x::Float, y::Float)
...
end
and want to use an (external) monte carlo sampler to fit this model. The main thing it requires is a function from a Vector{Float} with nparams elements to Params: sampler works with plain arrays only, and I would really prefer using structs for parameters. Then it need priors of course, and I thought that a really convenient way to write them would be like I show in the first post: Params(field_one = Normal(0, 2), field_two = (Uniform(-2, 1), Normal(0, 1)), field_three = 5).
Do you think there are better or easier approaches?
Agreed, if you need it for an external program, the flattening is reasonable. Here you go:
using Distributions
using QuickTypes
struct Mapped{F, A<:Tuple, K}
f::F
args::A
kw::K
end
function Base.rand(o::Mapped)
args = map(rand, o.args)
kw = Dict(k => rand(v) for (k,v) in o.kw)
o.f(args...; kw...)
end
nparams(o::Distribution) = 1 #TODO univariate
function nparams(o::Mapped)
a = mapreduce(nparams, +, o.args, init=0)
k = mapreduce(nparams, +, values(o.kw), init=0)
a + k
end
param_dists(o::Distribution) = o
function param_dists(o::Mapped)
a = map(param_dists, o.args)
k = map(param_dists, values(o.kw))
vcat(a..., k...)
end
from_params(o::Distribution, x) = first(x)
function from_params(o::Mapped, x)
i = 0
args = map(o.args) do dist
n = nparams(dist)
index = (i+1):(i+n)
i += n
from_params(dist, x[index])
end
kw = Dict(key => begin
n = nparams(dist)
index = (i+1):(i+n)
i += n
from_params(dist, x[index])
end for (key, dist) in o.kw)
o.f(args...; kw...)
end
# sugar
pushforward(f, args...; kw...) = Mapped(f, args, kw)
product(args...) = pushforward(tuple, args...)
constant(x) = pushforward(()->x)
@qstruct MyObject(;
field_one,
field_two,
field_three,
)
d = pushforward(MyObject,
field_one=Normal(0, 2),
field_two=product(Uniform(-2, 1), Normal(0, 1)),
field_three=constant(5),
)
@show rand(d)
@show nparams(d)
@show param_dists(d)
@show from_params(d, [1,2,3.])