Skip to content

Commit 85f8f92

Browse files
committed
Fix implementation of minimum and maximum
This addresses some of the issues discussed in JuliaLang/julia#45932. For Julia 1.8.0 it completely fixes minimum and maximum, for earlier versions of Julia it only partially fixes some of the issues. The change completely removes the specialised implementation of extrema. It also completely removes the specialised implementation of minimum and maximum for Mag and Arf. The default implementations work well, though are slower due to more allocations. In the future it could maybe be an option to have a faster version using MutableArithmetics.jl. The Base implementation of minimum and maximum is fixed for Arb by overloading Base._fast to make it correct for Arb. Before 1.8.0 this is not enough to fix all problems and it thus keeps the specialised implementation for Julia before 1.8.0-rc3 (latest release candidate as of writing), for later versions the specialised implementation can be removed.
1 parent 9bf2735 commit 85f8f92

File tree

3 files changed

+145
-70
lines changed

3 files changed

+145
-70
lines changed

README.md

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -173,29 +173,15 @@ TODO: Come up with more examples
173173
### Implementation details
174174
In some cases the implementation in Julia implicitly makes certain
175175
assumptions to improve performance and this can lead to issues. For
176-
example the `maximum` method in Julia checks for `NaN` results (on
177-
which is short fuses) using `x == x`, which works for most numerical
178-
types but not for `Arb` (`x == x` is only true if the radius is zero).
179-
See <https://github.com/JuliaLang/julia/issues/36287> for some more
180-
details. Arblib implements its own `maximum` method which gives
181-
rigorous results, but it only covers the case
182-
`maximum(AbstractFloat{Arb})`.
183-
184-
``` julia
185-
julia> f = i -> Arb((i, i + 1));
186-
187-
julia> A = f.(0:1000);
188-
189-
julia> maximum(A)
190-
[1.00e+3 +/- 1.01]
191-
192-
julia> maximum(A, dims = 1)
193-
1-element Array{Arb,1}:
194-
[+/- 1.01]
195-
196-
julia> maximum(f, 0:1000)
197-
[+/- 1.01]
198-
```
176+
example, prior to Julia version 1.8 the `minimum` and `maximum`
177+
methods in Julia checked for `NaN` results (on which is short fuses)
178+
using `x == x`, which works for most numerical types but not for `Arb`
179+
(`x == x` is only true if the radius is zero). See
180+
<https://github.com/JuliaLang/julia/issues/36287> and in particular
181+
<https://github.com/JuliaLang/julia/issues/45932> for more details.
182+
Since Julia version 1.8 the `minimum` and `maximum` methods work
183+
correctly for `Arb`, for earlier versions of Julia it only works
184+
correctly in some cases.
199185

200186
These types of problems are the hardest to find since they are not
201187
clear from the documentation but you have to read the implementation,

src/arithmetic.jl

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ for f in [:inv, :sqrt, :log, :log1p, :exp, :expm1, :atan, :cosh, :sinh]
2222
@eval Base.$f(x::MagOrRef) = $(Symbol(f, :!))(zero(x), x)
2323
end
2424

25+
Base.min(x::MagOrRef, y::MagOrRef) = Arblib.min!(zero(x), x, y)
26+
Base.max(x::MagOrRef, y::MagOrRef) = Arblib.max!(zero(x), x, y)
27+
Base.minmax(x::MagOrRef, y::MagOrRef) = (min(x, y), max(x, y))
28+
2529
### Arf
2630
function Base.sign(x::ArfOrRef)
2731
isnan(x) && return Arf(NaN) # Follow Julia and return NaN
@@ -74,6 +78,10 @@ function root(x::ArfOrRef, k::Integer)
7478
return y
7579
end
7680

81+
Base.min(x::ArfOrRef, y::ArfOrRef) = Arblib.min!(zero(x), x, y)
82+
Base.max(x::ArfOrRef, y::ArfOrRef) = Arblib.max!(zero(x), x, y)
83+
Base.minmax(x::ArfOrRef, y::ArfOrRef) = (min(x, y), max(x, y))
84+
7785
### Arb and Acb
7886
for (jf, af) in [(:+, :add!), (:-, :sub!), (:*, :mul!), (:/, :div!)]
7987
@eval Base.$jf(x::ArbOrRef, y::Union{ArbOrRef,ArfOrRef,_BitInteger}) =
@@ -174,6 +182,10 @@ function sinhcosh(x::Union{ArbOrRef,AcbOrRef})
174182
return (s, c)
175183
end
176184

185+
Base.min(x::ArbOrRef, y::ArbOrRef) = Arblib.min!(zero(x), x, y)
186+
Base.max(x::ArbOrRef, y::ArbOrRef) = Arblib.max!(zero(x), x, y)
187+
Base.minmax(x::ArbOrRef, y::ArbOrRef) = (min(x, y), max(x, y))
188+
177189
### Acb
178190
function Base.:(*)(x::AcbOrRef, y::Complex{Bool})
179191
if real(y)
@@ -194,38 +206,49 @@ Base.imag(z::AcbLike; prec = _precision(z)) = get_imag!(Arb(; prec), z)
194206
Base.conj(z::AcbLike) = conj!(Acb(prec = _precision(z)), z)
195207
Base.abs(z::AcbLike) = abs!(Arb(prec = _precision(z)), z)
196208

197-
### min and max
198-
for T in [MagOrRef, ArfOrRef, ArbOrRef]
199-
for op in [:min, :max]
200-
# min/max
201-
@eval Base.$op(x::$T, y::$T) = $(Symbol(op, :!))(zero(x), x, y)
202-
# minimum/maximum
203-
# The default implementation of minimum/maximum in Julia is
204-
# not correct for Arb, we implemented a correct version when
205-
# no specific dimension is given. The default give correct
206-
# results for Mag and Arf but wrong results for Arb in some
207-
# cases, be careful!
208-
@eval function Base.$(Symbol(op, :imum))(A::AbstractArray{<:$T})
209-
isempty(A) &&
210-
throw(ArgumentError("reducing over an empty collection is not allowed"))
211-
res = copy(first(A))
212-
for x in Iterators.drop(A, 1)
213-
$(Symbol(op, :!))(res, res, x)
214-
end
215-
return res
209+
### minimum and maximum
210+
# The default implemented in Julia have several issues for Arb types.
211+
# See https://github.com/JuliaLang/julia/issues/45932.
212+
# Note that it works fine for Mag and Arf.
213+
214+
# Before 1.8.0 there is no way to fix the implementation in Base.
215+
# Instead we define a new method. Note that this doesn't fully fix the
216+
# problem, there is no way to dispatch on for example
217+
# minimum(x -> Arb(x), [1, 2, 3, 4]) or an array with only some Arb.
218+
if VERSION < v"1.8.0-rc3"
219+
function Base.minimum(A::AbstractArray{<:ArbOrRef})
220+
isempty(A) &&
221+
throw(ArgumentError("reducing over an empty collection is not allowed"))
222+
res = copy(first(A))
223+
for x in Iterators.drop(A, 1)
224+
Arblib.min!(res, res, x)
216225
end
226+
return res
217227
end
218-
@eval Base.minmax(x::$T, y::$T) = (min(x, y), max(x, y))
219228

220-
@eval function Base.extrema(A::AbstractArray{<:$T})
229+
function Base.maximum(A::AbstractArray{<:ArbOrRef})
221230
isempty(A) &&
222231
throw(ArgumentError("reducing over an empty collection is not allowed"))
223-
l = copy(first(A))
224-
u = copy(first(A))
232+
res = copy(first(A))
225233
for x in Iterators.drop(A, 1)
226-
min!(l, l, x)
227-
max!(u, u, x)
234+
Arblib.max!(res, res, x)
228235
end
229-
return (l, u)
236+
return res
230237
end
231238
end
239+
240+
# Since 1.8.0 it is possible to fix the Base implementation by
241+
# overloading some internal methods. This also works before 1.8.0 but
242+
# doesn't solve the full problem.
243+
244+
# The default implementation in Base is not correct for Arb
245+
Base._fast(::typeof(min), x::Arb, y::Arb) = min(x, y)
246+
Base._fast(::typeof(min), x::Arb, y) = min(x, y)
247+
Base._fast(::typeof(min), x, y::Arb) = min(x, y)
248+
Base._fast(::typeof(max), x::Arb, y::Arb) = max(x, y)
249+
Base._fast(::typeof(max), x::Arb, y) = max(x, y)
250+
Base._fast(::typeof(max), x, y::Arb) = max(x, y)
251+
252+
# Mag, Arf and Arb don't have signed zeros
253+
Base.isbadzero(::typeof(min), x::Union{Mag,Arf,Arb}) = false
254+
Base.isbadzero(::typeof(max), x::Union{Mag,Arf,Arb}) = false

test/arithmetic.jl

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,6 @@
2929
@test min(Mag(1), Mag(2)) == Mag(1)
3030
@test max(Mag(1), Mag(2)) == Mag(2)
3131
@test minmax(Mag(1), Mag(2)) == minmax(Mag(2), Mag(1)) == (Mag(1), Mag(2))
32-
@test minimum(Mag[10:20; 0:9]) == Mag(0)
33-
@test maximum(Mag[10:20; 0:9]) == Mag(20)
34-
@test extrema(Mag[10:20; 0:9]) == (Mag(0), Mag(20))
3532
end
3633

3734
@testset "Arf" begin
@@ -74,9 +71,6 @@
7471
@test min(Arf(1), Arf(2)) == Arf(1)
7572
@test max(Arf(1), Arf(2)) == Arf(2)
7673
@test minmax(Arf(1), Arf(2)) == minmax(Arf(2), Arf(1)) == (Arf(1), Arf(2))
77-
@test minimum(Arf[10:20; 0:9]) == Arf(0)
78-
@test maximum(Arf[10:20; 0:9]) == Arf(20)
79-
@test extrema(Arf[10:20; 0:9]) == (Arf(0), Arf(20))
8074
end
8175

8276
@testset "$T" for T in [Arb, Acb]
@@ -188,21 +182,6 @@
188182
@test all(Arblib.contains.(minmax(Arb((0, 2)), Arb((-1, 3))), (-1, 0)))
189183
@test all(Arblib.contains.(minmax(Arb((0, 2)), Arb((-1, 3))), (2, 3)))
190184
@test all(.!Arblib.contains.(minmax(Arb((0, 2)), Arb((-1, 3))), (3, -1)))
191-
@test minimum(Arb[10:20; 0:9]) == Arb(0)
192-
@test maximum(Arb[10:20; 0:9]) == Arb(20)
193-
@test extrema(Arb[10:20; 0:9]) == (Arb(0), Arb(20))
194-
A = [Arb((i, i + 1)) for i = 0:10]
195-
@test Arblib.contains(minimum(A), Arb((0, 1)))
196-
@test Arblib.contains(minimum(reverse(A)), Arb((0, 1)))
197-
@test Arblib.contains(maximum(A), Arb((10, 11)))
198-
@test Arblib.contains(maximum(reverse(A)), Arb((10, 11)))
199-
@test all(Arblib.contains.(extrema(A), (Arb((0, 1)), Arb((10, 11)))))
200-
# These fails with the default implementation of minimum and maximum
201-
@test Arblib.contains(
202-
minimum([Arb((-i, -i + 1)) for i = 0:1000]),
203-
Arb((-1000, -999)),
204-
)
205-
@test Arblib.contains(maximum([Arb((i, i + 1)) for i = 0:1000]), Arb((1000, 1001)))
206185
end
207186

208187
@testset "Acb - specific" begin
@@ -243,4 +222,91 @@
243222
@test abs(Acb(3, 4)) isa Arb
244223
@test abs(Acb(3, 4)) == Arb(5)
245224
end
225+
226+
@testset "minimum/maximum/extrema" begin
227+
# See https://github.com/JuliaLang/julia/issues/45932 for
228+
# discussions about the issues with the Base implementation
229+
230+
# Currently there is no special implementation of extrema, the
231+
# default implementation works well. But to help find future
232+
# issues we test it here as well.
233+
234+
A = Arb[10:20; 0:9]
235+
@test minimum(A) == 0
236+
@test maximum(A) == 20
237+
@test extrema(A) == (0, 20)
238+
239+
A = [Arb((i, i + 1)) for i = 0:20]
240+
@test contains(minimum(A), Arb((0, 1)))
241+
@test contains(minimum(reverse(A)), Arb((0, 1)))
242+
@test contains(maximum(A), Arb((20, 21)))
243+
@test contains(maximum(reverse(A)), Arb((20, 21)))
244+
@test all(contains.(extrema(A), (Arb((0, 1)), Arb((20, 21)))))
245+
@test all(contains.(extrema(reverse(A)), (Arb((0, 1)), Arb((20, 21)))))
246+
247+
# Fails in Julia version < 1.8 with default implementation due
248+
# to short circuiting in Base.mapreduce_impl
249+
A = [setball(Arb, 2, 1); zeros(Arb, 257)]
250+
@test iszero(minimum(A))
251+
@test iszero(maximum(-A))
252+
@test iszero(extrema(A)[1])
253+
@test iszero(extrema(-A)[2])
254+
# Before 1.8.0 these still fails and there is no real way to
255+
# overload them
256+
if VERSION < v"1.8.0-rc3"
257+
@test_broken iszero(minimum(identity, A))
258+
@test_broken iszero(maximum(identity, -A))
259+
else
260+
@test iszero(minimum(identity, A))
261+
@test iszero(maximum(identity, -A))
262+
end
263+
# These work
264+
@test iszero(extrema(identity, A)[1])
265+
@test iszero(extrema(identity, -A)[2])
266+
267+
# Fails with default implementation due to Base._fast
268+
#A = [Arb(0); [setball(Arb, 0, i) for i in reverse(0:257)]]
269+
A = [setball(Arb, 0, i) for i = 0:257]
270+
@test contains(minimum(A), -257)
271+
@test contains(maximum(A), 257)
272+
@test contains(extrema(A)[1], -257)
273+
@test contains(extrema(A)[2], 257)
274+
@test contains(minimum(identity, A), -257)
275+
@test contains(maximum(identity, A), 257)
276+
@test contains(extrema(identity, A)[1], -257)
277+
@test contains(extrema(identity, A)[2], 257)
278+
279+
# Fails with default implementation due to both short circuit
280+
# and Base._fast
281+
A = [setball(Arb, 0, i) for i = 0:1000]
282+
@test contains(minimum(A), -1000)
283+
@test contains(maximum(A), 1000)
284+
@test contains(extrema(A)[1], -1000)
285+
@test contains(extrema(A)[2], 1000)
286+
if VERSION < v"1.8.0-rc3"
287+
@test_broken contains(minimum(identity, A), -1000)
288+
@test_broken contains(maximum(identity, A), 1000)
289+
else
290+
@test contains(minimum(identity, A), -1000)
291+
@test contains(maximum(identity, A), 1000)
292+
end
293+
@test contains(extrema(identity, A)[1], -1000)
294+
@test contains(extrema(identity, A)[2], 1000)
295+
296+
@test !Base.isbadzero(min, zero(Mag))
297+
@test !Base.isbadzero(min, zero(Arf))
298+
@test !Base.isbadzero(min, zero(Arb))
299+
300+
@test !Base.isbadzero(max, zero(Mag))
301+
@test !Base.isbadzero(max, zero(Arf))
302+
@test !Base.isbadzero(max, zero(Arb))
303+
304+
@test !Base.isgoodzero(min, zero(Mag))
305+
@test !Base.isgoodzero(min, zero(Arf))
306+
@test !Base.isgoodzero(min, zero(Arb))
307+
308+
@test !Base.isgoodzero(max, zero(Mag))
309+
@test !Base.isgoodzero(max, zero(Arf))
310+
@test !Base.isgoodzero(max, zero(Arb))
311+
end
246312
end

0 commit comments

Comments
 (0)