diff --git a/docs/src/developers.md b/docs/src/developers.md index c6ac0a0..6d1e16a 100644 --- a/docs/src/developers.md +++ b/docs/src/developers.md @@ -65,6 +65,6 @@ It must implement [`loval`](@ref), [`hival`](@ref), [`lohi`](@ref), [`basetype`] supported arithmetic, and any needed derived methods. These may include [`mid`](@ref), [`wid`](@ref), [`rad`](@ref), [`mag`](@ref), [`mig`](@ref), [`hull`](@ref), [`emptyset`](@ref), [`isempty_tn`](@ref), [`isnan_tn`](@ref), [`isinf_tn`](@ref), -[`isfinite_tn`](@ref), and `iszero`. +[`isfinite_tn`](@ref), and [`iszero_tn`](@ref). Use `isthick(x)` to recognize both kinds of implementation. diff --git a/docs/src/index.md b/docs/src/index.md index 27e3f70..6feb28a 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -33,6 +33,10 @@ returning `true` would imply that `x == y` *for any choice* `x ∈ X` and `y ∈ unless `X` and `Y` are either empty or each contain only a single value. Concretely, if `1..3` constructs an interval, then `1..3 == 1..3` returning `true` would require that `1.5 == 1.5` and also `1.5 == 2.5` since both `1.5` and `2.5` can be drawn from `1..3`. This is obviously impossible, thus having `1..3 == 1..3` return `true` would be a violation of the FPTN; it must error instead. +Unary predicates have the same constraint. If `X` contains both zero and nonzero values, +`iszero(X)` cannot return one `Bool`. It therefore errors; [`iszero_tn`](@ref) instead +reports whether `X` is the additive identity `[0, 0]`. + Because numbers are iterable in Julia, set operations like `X ⊆ Y` also cannot be defined (it would require that each number in `X` is a subset of every number in `Y`); however, operations like `intersect(X, Y)` (i.e., `X ∩ Y`) are valid because `x ∩ y` returns `∅` if `x != y` and `∅` is a subset of all other sets. To avoid violating the FPTN, we replace operators like `==` with custom operators that work only on `ThickNumber{T}` but not `T`. For `Base` Julia functions, a convention is to add `_tn` after the standard function name: `isequal_tn(X, Y)` replaces the "intent" of `isequal(X, Y)`. Often these have unicode equivalents, which typically (though not always) involve a "dot" somewhere in the symbol. @@ -60,6 +64,7 @@ You can also check a few basic properties, like whether the values contained in - [`isfinite_tn(X)`](@ref) - [`isinf_tn(X)`](@ref) - [`isnan_tn(X)`](@ref) +- [`iszero_tn(X)`](@ref): whether `X` is the single point zero ### Type information diff --git a/docs/src/user_api.md b/docs/src/user_api.md index 9c44673..5a3ad25 100644 --- a/docs/src/user_api.md +++ b/docs/src/user_api.md @@ -32,6 +32,7 @@ wid isfinite_tn isinf_tn isnan_tn +iszero_tn ``` ## Comparison operators diff --git a/ext/ThickNumbersForwardDiffExt.jl b/ext/ThickNumbersForwardDiffExt.jl index aa713a4..f647707 100644 --- a/ext/ThickNumbersForwardDiffExt.jl +++ b/ext/ThickNumbersForwardDiffExt.jl @@ -45,10 +45,10 @@ ForwardDiff._div_partial(partial::Real, x::ThickNumber) = partial / x ForwardDiff._div_partial(partial::ThickNumber, x::Real) = partial / x ForwardDiff._div_partial(partial::ThickNumber, x::ThickNumber) = partial / x -# ForwardDiff's `iszero_tuple` tests each partial with `==`, which ThickNumber -# disables. Test exact-zeroness with `iszero` instead (defined for ThickNumber and, -# recursively, for nested Duals over ThickNumbers). -ForwardDiff.iszero_tuple(tup::NTuple{N,V}) where {N,V<:ThickLike} = all(iszero, tup) +# ForwardDiff needs an unambiguous zero test for thick partials. +ThickNumbers.iszero_tn(d::ThickDual) = iszero_tn(value(d)) & iszero_tn(partials(d)) +ThickNumbers.iszero_tn(p::Partials{N,V}) where {N,V<:ThickLike} = all(iszero_tn, p.values) +ForwardDiff.iszero_tuple(tup::NTuple{N,V}) where {N,V<:ThickLike} = all(iszero_tn, tup) Base.promote_rule(::Type{TN}, ::Type{Dual{T,V,N}}) where {TN<:ThickNumber,T,V<:Number,N} = Dual{T, promote_dual(TN, V),N} diff --git a/src/ThickNumbers.jl b/src/ThickNumbers.jl index ece4093..744f2b0 100644 --- a/src/ThickNumbers.jl +++ b/src/ThickNumbers.jl @@ -22,7 +22,7 @@ export emptyset, hull, issubset_tn, ⫃, is_strict_subset_tn, ⪽, issupset_tn, export isequal_tn, iseq_tn, ≐, isapprox_tn, ⩪, isless_tn, ≺, ≻, ⪯, ⪰ # Unary -export isfinite_tn, isinf_tn, isnan_tn +export isfinite_tn, isinf_tn, isnan_tn, iszero_tn # Types @@ -320,10 +320,17 @@ mig(x::ThickNumber{T}) where T = zero(T) ∈ x ? zero(T) : min(abs(loval(x)), ab mag(x::Real) = abs(x) mig(x::Real) = abs(x) -# A thick number is zero exactly when it is the degenerate set `[0, 0]`. Unlike -# `==`, this is unambiguous, so it is defined (and needed by ForwardDiff, which -# tests partials for exact zero). -Base.iszero(x::ThickNumber) = iszero(loval(x)) & iszero(hival(x)) +""" + iszero_tn(x) + +Return `true` if both endpoints of `x` are zero, `false` otherwise. +For point numbers, equivalent to `iszero(x)`. +""" +iszero_tn(x::ThickNumber) = iszero(loval(x)) & iszero(hival(x)) + +iszero_tn(x::Number) = iszero(x) + +Base.iszero(::ThickNumber) = throw(FPTNException(iszero, iszero_tn)) Base.promote_rule(::Type{ThickNumber{T}}, ::Type{ThickNumber{S}}) where {T<:Number,S<:Number} = ThickNumber{promote_type(T,S)} diff --git a/test/runtests.jl b/test/runtests.jl index 36b0e2d..0ac943a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -31,11 +31,13 @@ using MidRadArith @test mig(-3.0) === 3.0 @test mag(2) === 2 @test mig(2) === 2 - # iszero: true only for the degenerate set [0, 0] - @test iszero(Interval(0, 0)) - @test !iszero(Interval(0, 1)) - @test !iszero(Interval(-1, 0)) - @test !iszero(Interval(-1, 1)) + # Only [0, 0] is the additive identity. + @test iszero_tn(Interval(0, 0)) + @test !iszero_tn(Interval(0, 1)) + @test !iszero_tn(Interval(-1, 0)) + @test !iszero_tn(Interval(-1, 1)) + @test iszero_tn(0.0) && !iszero_tn(1.0) + @test_throws FPTNException iszero(Interval(0, 0)) @test lohi(Interval{Float64}, 1, 3) === x @test midrad(Interval{Float64}, 2, 1) === x @test basetype(Interval{Float64}) === Interval