Skip to content

Commit

Permalink
Fix extrema(x; dims) for inputs with NaN/missing (JuliaLang#43604)
Browse files Browse the repository at this point in the history
* Define `extrema` using `mapreduce`; support `init`

* Fix tests for SparseArrays

* API clean and export `extrema!`

* Re-implement `reducedim_init` for extrema

* Merge `master` to pull in JuliaSparse/SparseArrays.jl#63

* Mark `BigInt` tests as broken

Co-authored-by: Milan Bouchet-Valat <[email protected]>
Co-authored-by: Simeon Schaub <[email protected]>
Co-authored-by: Takafumi Arakaki <[email protected]>
Co-authored-by: Tim Holy <[email protected]>
  • Loading branch information
5 people authored and LilithHafner committed Feb 22, 2022
1 parent dd395f3 commit 8636018
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 149 deletions.
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ Standard library changes
arithmetic to error if the result may be wrapping. Or use a package such as SaferIntegers.jl when
constructing the range. ([#40382])
* TCP socket objects now expose `closewrite` functionality and support half-open mode usage ([#40783]).
* `extrema` now supports `init` keyword argument ([#36265], [#43604]).
* Intersect returns a result with the eltype of the type-promoted eltypes of the two inputs ([#41769]).
* `Iterators.countfrom` now accepts any type that defines `+`. ([#37747])

Expand Down
13 changes: 13 additions & 0 deletions base/compiler/compiler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ include("compiler/abstractinterpretation.jl")
include("compiler/typeinfer.jl")
include("compiler/optimize.jl") # TODO: break this up further + extract utilities

# required for bootstrap
# TODO: find why this is needed and remove it.
function extrema(x::Array)
isempty(x) && throw(ArgumentError("collection must be non-empty"))
vmin = vmax = x[1]
for i in 2:length(x)
xi = x[i]
vmax = max(vmax, xi)
vmin = min(vmin, xi)
end
return vmin, vmax
end

include("compiler/bootstrap.jl")
ccall(:jl_set_typeinf_func, Cvoid, (Any,), typeinf_ext_toplevel)

Expand Down
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ export
eachindex,
eachrow,
eachslice,
extrema!,
extrema,
fill!,
fill,
Expand Down
74 changes: 0 additions & 74 deletions base/multidimensional.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1695,80 +1695,6 @@ _unique_dims(A::AbstractArray, dims::Colon) = invoke(unique, Tuple{Any}, A)
end
end

"""
extrema(A::AbstractArray; dims) -> Array{Tuple}
Compute the minimum and maximum elements of an array over the given dimensions.
# Examples
```jldoctest
julia> A = reshape(Vector(1:2:16), (2,2,2))
2×2×2 Array{Int64, 3}:
[:, :, 1] =
1 5
3 7
[:, :, 2] =
9 13
11 15
julia> extrema(A, dims = (1,2))
1×1×2 Array{Tuple{Int64, Int64}, 3}:
[:, :, 1] =
(1, 7)
[:, :, 2] =
(9, 15)
```
"""
extrema(A::AbstractArray; dims = :) = _extrema_dims(identity, A, dims)

"""
extrema(f, A::AbstractArray; dims) -> Array{Tuple}
Compute the minimum and maximum of `f` applied to each element in the given dimensions
of `A`.
!!! compat "Julia 1.2"
This method requires Julia 1.2 or later.
"""
extrema(f, A::AbstractArray; dims=:) = _extrema_dims(f, A, dims)

_extrema_dims(f, A::AbstractArray, ::Colon) = _extrema_itr(f, A)

function _extrema_dims(f, A::AbstractArray, dims)
sz = size(A)
for d in dims
sz = setindex(sz, 1, d)
end
T = promote_op(f, eltype(A))
B = Array{Tuple{T,T}}(undef, sz...)
return extrema!(f, B, A)
end

@noinline function extrema!(f, B, A)
require_one_based_indexing(B, A)
sA = size(A)
sB = size(B)
for I in CartesianIndices(sB)
fAI = f(A[I])
B[I] = (fAI, fAI)
end
Bmax = CartesianIndex(sB)
@inbounds @simd for I in CartesianIndices(sA)
J = min(Bmax,I)
BJ = B[J]
fAI = f(A[I])
if fAI < BJ[1]
B[J] = (fAI, BJ[2])
elseif fAI > BJ[2]
B[J] = (BJ[1], fAI)
end
end
return B
end
extrema!(B, A) = extrema!(identity, B, A)

# Show for pairs() with Cartesian indices. Needs to be here rather than show.jl for bootstrap order
function Base.showarg(io::IO, r::Iterators.Pairs{<:Integer, <:Any, <:Any, T}, toplevel) where T <: Union{AbstractVector, Tuple}
print(io, "pairs(::$T)")
Expand Down
52 changes: 0 additions & 52 deletions base/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -504,58 +504,6 @@ julia> minmax('c','b')
"""
minmax(x,y) = isless(y, x) ? (y, x) : (x, y)

"""
extrema(itr) -> Tuple
Compute both the minimum and maximum element in a single pass, and return them as a 2-tuple.
# Examples
```jldoctest
julia> extrema(2:10)
(2, 10)
julia> extrema([9,pi,4.5])
(3.141592653589793, 9.0)
```
"""
extrema(itr) = _extrema_itr(identity, itr)

"""
extrema(f, itr) -> Tuple
Compute both the minimum and maximum of `f` applied to each element in `itr` and return
them as a 2-tuple. Only one pass is made over `itr`.
!!! compat "Julia 1.2"
This method requires Julia 1.2 or later.
# Examples
```jldoctest
julia> extrema(sin, 0:π)
(0.0, 0.9092974268256817)
```
"""
extrema(f, itr) = _extrema_itr(f, itr)

function _extrema_itr(f, itr)
y = iterate(itr)
y === nothing && throw(ArgumentError("collection must be non-empty"))
(v, s) = y
vmin = vmax = f(v)
while true
y = iterate(itr, s)
y === nothing && break
(x, s) = y
fx = f(x)
vmax = max(fx, vmax)
vmin = min(fx, vmin)
end
return (vmin, vmax)
end

extrema(x::Real) = (x, x)
extrema(f, x::Real) = (y = f(x); (y, y))

## definitions providing basic traits of arithmetic operators ##

"""
Expand Down
77 changes: 71 additions & 6 deletions base/reduce.jl
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ julia> prod(1:5; init = 1.0)
"""
prod(a; kw...) = mapreduce(identity, mul_prod, a; kw...)

## maximum & minimum
## maximum, minimum, & extrema
_fast(::typeof(min),x,y) = min(x,y)
_fast(::typeof(max),x,y) = max(x,y)
function _fast(::typeof(max), x::AbstractFloat, y::AbstractFloat)
Expand Down Expand Up @@ -634,11 +634,6 @@ function mapreduce_impl(f, op::Union{typeof(max), typeof(min)},
start = first + 1
simdstop = start + chunk_len - 4
while simdstop <= last - 3
# short circuit in case of NaN or missing
(v1 == v1) === true || return v1
(v2 == v2) === true || return v2
(v3 == v3) === true || return v3
(v4 == v4) === true || return v4
@inbounds for i in start:4:simdstop
v1 = _fast(op, v1, f(A[i+0]))
v2 = _fast(op, v2, f(A[i+1]))
Expand Down Expand Up @@ -785,6 +780,76 @@ Inf
"""
minimum(a; kw...) = mapreduce(identity, min, a; kw...)

"""
extrema(itr; [init]) -> (mn, mx)
Compute both the minimum `mn` and maximum `mx` element in a single pass, and return them
as a 2-tuple.
The value returned for empty `itr` can be specified by `init`. It must be a 2-tuple whose
first and second elements are neutral elements for `min` and `max` respectively
(i.e. which are greater/less than or equal to any other element). As a consequence, when
`itr` is empty the returned `(mn, mx)` tuple will satisfy `mn ≥ mx`. When `init` is
specified it may be used even for non-empty `itr`.
!!! compat "Julia 1.8"
Keyword argument `init` requires Julia 1.8 or later.
# Examples
```jldoctest
julia> extrema(2:10)
(2, 10)
julia> extrema([9,pi,4.5])
(3.141592653589793, 9.0)
julia> extrema([]; init = (Inf, -Inf))
(Inf, -Inf)
```
"""
extrema(itr; kw...) = extrema(identity, itr; kw...)

"""
extrema(f, itr; [init]) -> (mn, mx)
Compute both the minimum `mn` and maximum `mx` of `f` applied to each element in `itr` and
return them as a 2-tuple. Only one pass is made over `itr`.
The value returned for empty `itr` can be specified by `init`. It must be a 2-tuple whose
first and second elements are neutral elements for `min` and `max` respectively
(i.e. which are greater/less than or equal to any other element). It is used for non-empty
collections. Note: it implies that, for empty `itr`, the returned value `(mn, mx)` satisfies
`mn ≥ mx` even though for non-empty `itr` it satisfies `mn ≤ mx`. This is a "paradoxical"
but yet expected result.
!!! compat "Julia 1.2"
This method requires Julia 1.2 or later.
!!! compat "Julia 1.8"
Keyword argument `init` requires Julia 1.8 or later.
# Examples
```jldoctest
julia> extrema(sin, 0:π)
(0.0, 0.9092974268256817)
julia> extrema(sin, Real[]; init = (1.0, -1.0)) # good, since -1 ≤ sin(::Real) ≤ 1
(1.0, -1.0)
```
"""
extrema(f, itr; kw...) = mapreduce(ExtremaMap(f), _extrema_rf, itr; kw...)

# Not using closure since `extrema(type, itr)` is a very likely use-case and it's better
# to avoid type-instability (#23618).
struct ExtremaMap{F} <: Function
f::F
end
ExtremaMap(::Type{T}) where {T} = ExtremaMap{Type{T}}(T)
@inline (f::ExtremaMap)(x) = (y = f.f(x); (y, y))

# TODO: optimize for inputs <: AbstractFloat
@inline _extrema_rf((min1, max1), (min2, max2)) = (min(min1, min2), max(max1, max2))

## findmax, findmin, argmax & argmin

"""
Expand Down
Loading

0 comments on commit 8636018

Please sign in to comment.