diff --git a/Project.toml b/Project.toml index 59ed0e35..97dfd0e9 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "MonteCarloMeasurements" uuid = "0987c9cc-fe09-11e8-30f0-b96dd679fdca" authors = ["baggepinnen "] -version = "0.8.1" +version = "0.8.2" [deps] Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" diff --git a/docs/src/index.md b/docs/src/index.md index 7fb53e4e..6a71577e 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -280,6 +280,13 @@ p = 0 ± 1 ``` Ideally, half of the particles should turn out negative and half positive when applying `negsquare(p)`. However, this will not happen as the `x > 0` is not defined for uncertain values. To circumvent this, define `negsquare` as a primitive using [`register_primitive`](@ref) described in [Overloading a new function](@ref). Particles will then be propagated one by one through the entire function `negsquare`. Common such functions from `Base`, such as `max/min` etc. are already registered. +## Comparison mode +Some functions perform checks like `if error < tol`. If `error isa Particles`, this will use a very conservative check by default by checking that all particles ∈ `error` fulfill the check. There are a few different options available for how to compare two uncertain quantities, chosen by specifying a comparison mode. The modes are chosen by `unsafe_comparisons(mode)` and the options are +- `:safe`: the default described above, throws an error if uncertain values share support. +- `:montecarlo`: slightly less conservative than `:safe`, checks if either all pairwise particles fulfill the comparison, *or* all pairwise particles fail the comparison. If some pairs pass and some fail, an error is thrown. +- `:reduction`: Reduce uncertain values to a single number, e.g. by calling `mean` (default) before performing the comparison, never throws an error. + +To sum up, if two uncertain values are compared, and they have no mutual support, then all comparison modes are equal. If they share support, `:safe` will error and `:montecarlo` will work if the all pairwise particles either pass or fail the comparison. `:reduction` will always work, but is maximally unsafe in the sense that it might not perform a meaningful check for your application. diff --git a/src/MonteCarloMeasurements.jl b/src/MonteCarloMeasurements.jl index 2f683c21..0f64d7de 100644 --- a/src/MonteCarloMeasurements.jl +++ b/src/MonteCarloMeasurements.jl @@ -9,18 +9,32 @@ using Distributions, StatsBase, Requires const DEFAULT_NUM_PARTICLES = 10000 const DEFAULT_STATIC_NUM_PARTICLES = 100 +""" +The function used to reduce particles to a number for comparison. Defaults to `mean`. Change using `unsafe_comparisons`. +""" const COMPARISON_FUNCTION = Ref{Function}(mean) -const USE_UNSAFE_COMPARIONS = Ref(false) +const COMPARISON_MODE = Ref(:safe) """ unsafe_comparisons(onoff=true; verbose=true) Toggle the use of a comparison function without warning. By default `mean` is used to reduce particles to a floating point number for comparisons. This function can be changed, example: `set_comparison_function(median)` + + unsafe_comparisons(mode=:reduction; verbose=true) +One can also specify a comparison mode, `mode` can take the values `:safe, :montecarlo, :reduction`. `:safe` is the same as calling `unsafe_comparisons(false)` and `:reduction` corresponds to `true`. +If """ -function unsafe_comparisons(onoff=true; verbose=true) - USE_UNSAFE_COMPARIONS[] = onoff - if onoff && verbose - @info "Unsafe comparisons using the function `$(COMPARISON_FUNCTION[])` has been enabled globally. Use `@unsafe` to enable in a local expression only or `unsafe_comparisons(false)` to turn off unsafe comparisons" +function unsafe_comparisons(mode=true; verbose=true) + mode == false && (mode = :safe) + mode == true && (mode = :reduction) + COMPARISON_MODE[] = mode + if mode != :safe && verbose + if mode === :reduction + @info "Unsafe comparisons using the function `$(COMPARISON_FUNCTION[])` has been enabled globally. Use `@unsafe` to enable in a local expression only or `unsafe_comparisons(false)` to turn off unsafe comparisons" + elseif mode === :montecarlo + @info "Comparisons using the monte carlo has been enabled globally. Call `unsafe_comparisons(false)` to turn off unsafe comparisons" + end end + mode ∉ (:safe, :montecarlo, :reduction) && error("Got unsupported comparison model") end """ set_comparison_function(f) @@ -47,7 +61,7 @@ macro unsafe(ex) :(res) end quote - previous_state = USE_UNSAFE_COMPARIONS[] + previous_state = COMPARISON_MODE[] unsafe_comparisons(true, verbose=false) local res try diff --git a/src/particles.jl b/src/particles.jl index f8a72a65..ca913f24 100644 --- a/src/particles.jl +++ b/src/particles.jl @@ -372,26 +372,54 @@ Base.:(==)(p1::AbstractParticles{T,N},p2::AbstractParticles{T,N}) where {T,N} = Base.:(!=)(p1::AbstractParticles{T,N},p2::AbstractParticles{T,N}) where {T,N} = p1.particles != p2.particles -function _comparison_operator(p) - length(p) == 1 && return - USE_UNSAFE_COMPARIONS[] || error("Comparison operators are not well defined for uncertain values and are currently turned off. Call `unsafe_comparisons(true)` to enable comparison operators for particles using the current reduction function $(COMPARISON_FUNCTION[]). Change this function using `set_comparison_function(f)`.") +function zip_longest(a,b) + l = max(length(a), length(b)) + Iterators.take(zip(Iterators.cycle(a), Iterators.cycle(b)), l) +end + +function safe_comparison(a,b,op::F) where F + all(((a,b),)->op(a,b), Iterators.product(extrema(a),extrema(b))) && return true + !any(((a,b),)->op(a,b), Iterators.product(extrema(a),extrema(b))) && return false + _comparison_error() +end + +function do_comparison(a,b,op::F) where F + mode = COMPARISON_MODE[] + if mode === :reduction + op(COMPARISON_FUNCTION[](a), COMPARISON_FUNCTION[](b)) + elseif mode === :montecarlo + all(((a,b),)->op(a,b), zip_longest(a,b)) && return true + !any(((a,b),)->op(a,b), zip_longest(a,b)) && return false + _comparison_error() + elseif mode === :safe + safe_comparison(a,b,op) + else + error("Got unsupported comparison mode.") + end +end + +function _comparison_error() + msg = "Comparison of uncertain values using comparison mode $(COMPARISON_MODE[]) failed. Comparison operators are not well defined for uncertain values. Call `unsafe_comparisons(true)` to enable comparison operators for particles using the current reduction function $(COMPARISON_FUNCTION[]). Change this function using `set_comparison_function(f)`. " + if COMPARISON_MODE[] === :safe + msg *= "For safety reasons, the default safe comparison function is maximally conservative and tests if the extreme values of the distributions fulfil the comparison operator." + elseif COMPARISON_MODE[] === :montecarlo + msg *= "For safety reasons, montecarlo comparison is conservative and tests if pairwise particles fulfil the comparison operator. If some do *and* some do not, this error is thrown. Consider if you can define a primitive function ([docs](https://baggepinnen.github.io/MonteCarloMeasurements.jl/stable/overloading/#Overloading-a-new-function-1)) or switch to `unsafe_comparisons(:reduction)`" + end + + error(msg) end function Base.:<(a::Real,p::AbstractParticles) - _comparison_operator(p) - a < COMPARISON_FUNCTION[](p) + do_comparison(a,p,<) end function Base.:<(p::AbstractParticles,a::Real) - _comparison_operator(p) - COMPARISON_FUNCTION[](p) < a + do_comparison(p,a,<) end function Base.:<(p::AbstractParticles, a::AbstractParticles) - _comparison_operator(p) - COMPARISON_FUNCTION[](p) < COMPARISON_FUNCTION[](a) + do_comparison(p,a,<) end function Base.:(<=)(p::AbstractParticles{T,N}, a::AbstractParticles{T,N}) where {T,N} - _comparison_operator(p) - COMPARISON_FUNCTION[](p) <= COMPARISON_FUNCTION[](a) + do_comparison(p,a,<=) end """ diff --git a/test/runtests.jl b/test/runtests.jl index f3b99a8b..e9587d61 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -33,6 +33,7 @@ Random.seed!(0) @test [0,0] ∓ [1.,1.] isa MonteCarloMeasurements.MvParticles @info "Done" + PT = Particles for PT = (Particles, StaticParticles) @testset "$(repr(PT))" begin @info "Running tests for $PT" @@ -87,20 +88,44 @@ Random.seed!(0) @test_throws ErrorException p>p @test_throws ErrorException p>=p @test_throws ErrorException p<=p - @unsafe begin - @test -10 < p - @test p <= p - @test p >= p - @test !(p < p) - @test !(p > p) - @test (p < 1+p) - @test (p+1 > p) + + for mode in (:montecarlo, :reduction, :safe) + @show mode + unsafe_comparisons(mode, verbose=false) + @test p<100+p + @test p+100>p + @test p+100>=p + @test p<=100+p + + @test p<100 + @test 100>p + @test 100>=p + @test p<=100 + @unsafe begin + @test -10 < p + @test p <= p + @test p >= p + @test !(p < p) + @test !(p > p) + @test (p < 1+p) + @test (p+1 > p) + end end + @test_throws ErrorException p

p @test_throws ErrorException @unsafe error("") # Should still be safe after error + @test_throws ErrorException p>=p @test_throws ErrorException p<=p + unsafe_comparisons(:montecarlo, verbose=false) + @test p>=p + @test p<=p + @test !(pp) + + unsafe_comparisons(false) + @unsafe tv = 2 @test tv == 2 @unsafe tv1,tv2 = 1,2