Expected type promotion with user-defined type not happening

I traced a bug in some of my code down to some behavior that I don’t quite understand regarding type promotion. I’m not sure if the issue is with the MonteCarloMeasurements package I’m using or my general misunderstanding of the Julia language.

I am attempting to create an array of Particles variables. Since some of the elements of the array are not uncertain, I was counting on the fact that creating a combined array of Particles and Float64 will automatically promote the Float64s to Particles type. Here is an example of the behavior I was expecting:

julia> using MonteCarloMeasurements

julia> [3.2, 5.6±1.0]
2-element Array{Particles{Float64,500},1}:
 3.2 ± 1.8e-15
 5.6 ± 1.0

Notice the 3.2 became 3.2 ± 1.8e-15, a Particles{Float64,500} type. This is the behavior I expect and want to happen. Now if I define a struct with field val and a helper function for retrieving that val so I can call it’s vectorized form on an array, the array formed by the helper function is type Array{Real,1} when I would expect it to be an Array{Particles{Float64,500}},1} based off of the above promotion rule:

julia> struct Blah
       val
       end

       val(arg::Blah) = arg.val
       
       b = Blah(3.2)
       c = Blah(5.6±1.0)

 julia> val.([b, c])
2-element Array{Real,1}:
 3.2       
 5.61 ± 1.0

This also happens if I use a list comprehension instead of a vectorized function call. If I create the array manually, though, it acts like I expect it to:

julia> [b.a, c.a]
2-element Array{Particles{Float64,500},1}:
 3.2 ± 1.8e-15
 5.61 ± 1.0  

What am I missing?

(Also, as a side note: what is going on with 5.6 ± 1.0 becoming 5.61 ± 1.0?)

I will have a look at the promotion issue and see if there is anything I can do about it. As for the 5.60 turning into 5.61, this is due to the monte-carlo sampling of the distribution being non-deterministic. The variance of the mean in this sampling decreases as 1/N where N is the number of particles, in this case the std of the mean is 1/\sqrt(500) \approx 0.045. However, due to the systematic sampling employed, the std will actually decrease as \sim 1/N which is why it’s less than expected. See further analysis here

3 Likes

The promotion issue is due to julia not knowing how to promote two Blah with different field types.

If you parameterize your Blah and define the proper promotion rule, it works out

struct Blah{T}
    val::T
end

julia> T = Base.promote_typeof(b,c)
Blah # Julia promoted to an abstract type

julia> Base.promote_rule(::Type{Blah{T}}, ::Type{Blah{S}}) where {T,S} = Blah{promote_type(T,S)} # Define the promotion rule

julia> Base.convert(::Type{Blah{T}}, b::Blah{S}) where {T,S} = Blah{T}(T(b.val)) # define a convert method

julia> T = Base.promote_typeof(b,c)
Blah{Particles{Float64,500}} # Now the promotion works as expected

julia> val.([b, c])
2-element Array{Particles{Float64,500},1}:
 3.2 ± 1.8e-15
 5.6 ± 1.0 

Indeed, without the promotion rule, you get the same behavior with Float64/Int

julia> b = Blah(1)
Blah{Int64}(1)

julia> c = Blah(1.)
Blah{Float64}(1.0)

julia> [b, c]
2-element Array{Blah,1}:
 Blah{Int64}(1)    
 Blah{Float64}(1.0)

julia> val.([b, c])
2-element Array{Real,1}:
 1  
 1.0
3 Likes

Ah, that makes sense. Thank you.