Skip to content

Commit 3daa183

Browse files
jw3126StefanKarpinski
authored andcommitted
Add accumulate, accumulate! (#18931)
1 parent 416f5f2 commit 3daa183

File tree

9 files changed

+245
-103
lines changed

9 files changed

+245
-103
lines changed

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ Library improvements
6060
you can now do e.g. `[A I]` and it will concatenate an appropriately sized
6161
identity matrix ([#19305]).
6262

63+
* New `accumulate` and `accumulate!` functions, which generalize `cumsum` and `cumprod`. Also known as a [scan](https://en.wikipedia.org/wiki/Prefix_sum) operation ([#18931]).
64+
6365
Compiler/Runtime improvements
6466
-----------------------------
6567

@@ -76,6 +78,8 @@ Deprecated or removed
7678

7779
* `Dates.recur` has been deprecated in favor of `filter` ([#19288])
7880

81+
* `cummin` and `cummax` have been deprecated in favor of `accumulate`.
82+
7983
Julia v0.5.0 Release Notes
8084
==========================
8185

base/abstractarray.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1415,7 +1415,7 @@ function typed_hvcat{T}(::Type{T}, rows::Tuple{Vararg{Int}}, as...)
14151415
T[rs...;]
14161416
end
14171417

1418-
## Reductions and scans ##
1418+
## Reductions and accumulates ##
14191419

14201420
function isequal(A::AbstractArray, B::AbstractArray)
14211421
if A === B return true end

base/arraymath.jl

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -488,49 +488,3 @@ ctranspose{T<:Real}(A::AbstractVecOrMat{T}) = transpose(A)
488488

489489
transpose(x::AbstractVector) = [ transpose(v) for i=of_indices(x, OneTo(1)), v in x ]
490490
ctranspose{T}(x::AbstractVector{T}) = T[ ctranspose(v) for i=of_indices(x, OneTo(1)), v in x ]
491-
492-
# see discussion in #18364 ... we try not to widen type of the resulting array
493-
# from cumsum or cumprod, but in some cases (+, Bool) we may not have a choice.
494-
rcum_promote_type{T<:Number}(op, ::Type{T}) = promote_op(op, T)
495-
rcum_promote_type{T}(op, ::Type{T}) = T
496-
497-
# handle sums of Vector{Bool} and similar. it would be nice to handle
498-
# any AbstractArray here, but it's not clear how that would be possible
499-
rcum_promote_type{T,N}(op, ::Type{Array{T,N}}) = Array{rcum_promote_type(op,T), N}
500-
501-
for (f, f!, fp, op) = ((:cumsum, :cumsum!, :cumsum_pairwise!, :+),
502-
(:cumprod, :cumprod!, :cumprod_pairwise!, :*) )
503-
# in-place cumsum of c = s+v[range(i1,n)], using pairwise summation
504-
@eval function ($fp){T}(v::AbstractVector, c::AbstractVector{T}, s, i1, n)
505-
local s_::T # for sum(v[range(i1,n)]), i.e. sum without s
506-
if n < 128
507-
@inbounds s_ = v[i1]
508-
@inbounds c[i1] = ($op)(s, s_)
509-
for i = i1+1:i1+n-1
510-
@inbounds s_ = $(op)(s_, v[i])
511-
@inbounds c[i] = $(op)(s, s_)
512-
end
513-
else
514-
n2 = n >> 1
515-
s_ = ($fp)(v, c, s, i1, n2)
516-
s_ = $(op)(s_, ($fp)(v, c, ($op)(s, s_), i1+n2, n-n2))
517-
end
518-
return s_
519-
end
520-
521-
@eval function ($f!)(result::AbstractVector, v::AbstractVector)
522-
li = linearindices(v)
523-
li != linearindices(result) && throw(DimensionMismatch("input and output array sizes and indices must match"))
524-
n = length(li)
525-
if n == 0; return result; end
526-
i1 = first(li)
527-
@inbounds result[i1] = v1 = v[i1]
528-
n == 1 && return result
529-
($fp)(v, result, v1, i1+1, n-1)
530-
return result
531-
end
532-
533-
@eval function ($f){T}(v::AbstractVector{T})
534-
return ($f!)(similar(v, rcum_promote_type($op, T)), v)
535-
end
536-
end

base/deprecated.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,10 @@ end
145145
@deprecate chol(A::Number, ::Type{Val{:L}}) ctranspose(chol(A))
146146
@deprecate chol(A::AbstractMatrix, ::Type{Val{:L}}) ctranspose(chol(A))
147147

148+
@deprecate cummin(A, dim=1) accumulate(min, A, dim=1)
149+
@deprecate cummax(A, dim=1) accumulate(max, A, dim=1)
150+
151+
148152
# Number updates
149153

150154
# rem1 is inconsistent for x==0: The result should both have the same

base/exports.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -497,12 +497,12 @@ export
497497
colon,
498498
conj!,
499499
copy!,
500-
cummax,
501-
cummin,
502500
cumprod,
503501
cumprod!,
504502
cumsum,
505503
cumsum!,
504+
accumulate,
505+
accumulate!,
506506
cumsum_kbn,
507507
eachindex,
508508
extrema,

base/multidimensional.jl

Lines changed: 150 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -481,49 +481,62 @@ end
481481
end
482482
end
483483

484-
for (f, fmod, op) = ((:cummin, :_cummin!, :min), (:cummax, :_cummax!, :max))
485-
@eval function ($f)(v::AbstractVector)
486-
n = length(v)
487-
cur_val = v[1]
488-
res = similar(v, n)
489-
res[1] = cur_val
490-
for i in 2:n
491-
cur_val = ($op)(v[i], cur_val)
492-
res[i] = cur_val
493-
end
494-
return res
495-
end
496484

497-
@eval function ($f)(A::AbstractArray, axis::Integer)
498-
axis > 0 || throw(ArgumentError("axis must be a positive integer"))
499-
res = similar(A)
500-
axis > ndims(A) && return copy!(res, A)
501-
inds = indices(A)
502-
if isempty(inds[axis])
503-
return res
485+
# see discussion in #18364 ... we try not to widen type of the resulting array
486+
# from cumsum or cumprod, but in some cases (+, Bool) we may not have a choice.
487+
rcum_promote_type{T,S<:Number}(op, ::Type{T}, ::Type{S}) = promote_op(op, T, S)
488+
rcum_promote_type{T<:Number}(op, ::Type{T}) = rcum_promote_type(op, T,T)
489+
rcum_promote_type{T}(op, ::Type{T}) = T
490+
491+
# handle sums of Vector{Bool} and similar. it would be nice to handle
492+
# any AbstractArray here, but it's not clear how that would be possible
493+
rcum_promote_type{T,N}(op, ::Type{Array{T,N}}) = Array{rcum_promote_type(op,T), N}
494+
495+
# accumulate_pairwise slightly slower then accumulate, but more numerically
496+
# stable in certain situtations (e.g. sums).
497+
# it does double the number of operations compared to accumulate,
498+
# though for cheap operations like + this does not have much impact (20%)
499+
function _accumulate_pairwise!{T, Op}(op::Op, c::AbstractVector{T}, v::AbstractVector, s, i1, n)::T
500+
@inbounds if n < 128
501+
s_ = v[i1]
502+
c[i1] = op(s, s_)
503+
for i = i1+1:i1+n-1
504+
s_ = op(s_, v[i])
505+
c[i] = op(s, s_)
504506
end
505-
R1 = CartesianRange(inds[1:axis-1])
506-
R2 = CartesianRange(inds[axis+1:end])
507-
($fmod)(res, A, R1, R2, axis)
507+
else
508+
n2 = n >> 1
509+
s_ = _accumulate_pairwise!(op, c, v, s, i1, n2)
510+
s_ = op(s_, _accumulate_pairwise!(op, c, v, op(s, s_), i1+n2, n-n2))
508511
end
512+
return s_
513+
end
509514

510-
@eval @noinline function ($fmod)(res, A::AbstractArray, R1::CartesianRange, R2::CartesianRange, axis::Integer)
511-
inds = indices(A, axis)
512-
i1 = first(inds)
513-
for I2 in R2
514-
for I1 in R1
515-
res[I1, i1, I2] = A[I1, i1, I2]
516-
end
517-
for i = i1+1:last(inds)
518-
for I1 in R1
519-
res[I1, i, I2] = ($op)(A[I1, i, I2], res[I1, i-1, I2])
520-
end
521-
end
522-
end
523-
res
524-
end
515+
function accumulate_pairwise!{Op}(op::Op, result::AbstractVector, v::AbstractVector)
516+
li = linearindices(v)
517+
li != linearindices(result) && throw(DimensionMismatch("input and output array sizes and indices must match"))
518+
n = length(li)
519+
n == 0 && return result
520+
i1 = first(li)
521+
@inbounds result[i1] = v1 = v[i1]
522+
n == 1 && return result
523+
_accumulate_pairwise!(op, result, v, v1, i1+1, n-1)
524+
return result
525+
end
526+
527+
function accumulate_pairwise{T}(op, v::AbstractVector{T})
528+
out = similar(v, rcum_promote_type(op, T))
529+
return accumulate_pairwise!(op, out, v)
530+
end
525531

526-
@eval ($f)(A::AbstractArray) = ($f)(A, 1)
532+
function cumsum!(out, v::AbstractVector, axis::Integer=1)
533+
# for types prone to numerical stability issues, we want
534+
# accumulate_pairwise.
535+
axis == 1 ? accumulate_pairwise!(+, out, v) : copy!(out,v)
536+
end
537+
538+
function cumsum!{T <: Integer}(out, v::AbstractVector{T}, axis::Integer=1)
539+
axis == 1 ? accumulate!(+, out, v) : copy!(out,v)
527540
end
528541

529542
"""
@@ -550,8 +563,12 @@ julia> cumsum(a,2)
550563
4 9 15
551564
```
552565
"""
553-
cumsum{T}(A::AbstractArray{T}, axis::Integer=1) = cumsum!(similar(A, rcum_promote_type(+, T)), A, axis)
554-
cumsum!(B, A::AbstractArray) = cumsum!(B, A, 1)
566+
function cumsum{T}(A::AbstractArray{T}, axis::Integer=1)
567+
out = similar(A, rcum_promote_type(+, T))
568+
cumsum!(out, A, axis)
569+
end
570+
cumsum!(B, A, axis::Integer=1) = accumulate!(+, B, A, axis)
571+
555572
"""
556573
cumprod(A, dim=1)
557574
@@ -576,13 +593,99 @@ julia> cumprod(a,2)
576593
4 20 120
577594
```
578595
"""
579-
cumprod(A::AbstractArray, axis::Integer=1) = cumprod!(similar(A), A, axis)
580-
cumprod!(B, A) = cumprod!(B, A, 1)
596+
cumprod(A::AbstractArray, axis::Integer=1) = accumulate(*, A, axis)
597+
cumprod!(B, A, axis::Integer=1) = accumulate!(*, B, A, axis)
598+
599+
"""
600+
accumulate(op, A, dim=1)
581601
582-
cumsum!(B, A, axis::Integer) = cumop!(+, B, A, axis)
583-
cumprod!(B, A, axis::Integer) = cumop!(*, B, A, axis)
602+
Cumulative operation `op` along a dimension `dim` (defaults to 1). See also
603+
[`accumulate!`](:func:`accumulate!`) to use a preallocated output array, both for performance and
604+
to control the precision of the output (e.g. to avoid overflow). For common operations
605+
there are specialized variants of `accumulate`, see:
606+
[`cumsum`](:func:`cumsum`), [`cumprod`](:func:`cumprod`)
607+
608+
```jldoctest
609+
julia> accumulate(+, [1,2,3])
610+
3-element Array{Int64,1}:
611+
1
612+
3
613+
6
614+
615+
julia> accumulate(*, [1,2,3])
616+
3-element Array{Int64,1}:
617+
1
618+
2
619+
6
620+
```
621+
"""
622+
function accumulate(op, A, axis::Integer=1)
623+
out = similar(A, rcum_promote_type(op, eltype(A)))
624+
accumulate!(op, out, A, axis)
625+
end
584626

585-
function cumop!(op, B, A, axis::Integer)
627+
628+
"""
629+
accumulate(op, v0, A)
630+
631+
Like `accumulate`, but using a starting element `v0`. The first entry of the result will be
632+
`op(v0, first(A))`. For example:
633+
634+
```jldoctest
635+
julia> accumulate(+, 100, [1,2,3])
636+
3-element Array{Int64,1}:
637+
101
638+
103
639+
106
640+
641+
julia> accumulate(min, 0, [1,2,-1])
642+
3-element Array{Int64,1}:
643+
0
644+
0
645+
-1
646+
```
647+
"""
648+
function accumulate(op, v0, A, axis::Integer=1)
649+
T = rcum_promote_type(op, typeof(v0), eltype(A))
650+
out = similar(A, T)
651+
accumulate!(op, out, v0, A, 1)
652+
end
653+
654+
function accumulate!{Op}(op::Op, B, A::AbstractVector, axis::Integer=1)
655+
isempty(A) && return B
656+
v1 = first(A)
657+
_accumulate1!(op, B, v1, A, axis)
658+
end
659+
660+
function accumulate!(op, B, v0, A::AbstractVector, axis::Integer=1)
661+
isempty(A) && return B
662+
v1 = op(v0, first(A))
663+
_accumulate1!(op, B, v1, A, axis)
664+
end
665+
666+
667+
function _accumulate1!(op, B, v1, A::AbstractVector, axis::Integer=1)
668+
axis > 0 || throw(ArgumentError("axis must be a positive integer"))
669+
inds = linearindices(A)
670+
inds == linearindices(B) || throw(DimensionMismatch("linearindices of A and B don't match"))
671+
axis > 1 && return copy!(B, A)
672+
i1 = inds[1]
673+
cur_val = v1
674+
B[i1] = cur_val
675+
@inbounds for i in inds[2:end]
676+
cur_val = op(cur_val, A[i])
677+
B[i] = cur_val
678+
end
679+
return B
680+
end
681+
682+
"""
683+
accumulate!(op, B, A, dim=1)
684+
685+
Cumulative operation `op` on `A` along a dimension, storing the result in `B`. The dimension defaults to 1.
686+
See also [`accumulate`](:func:`accumulate`).
687+
"""
688+
function accumulate!(op, B, A, axis::Integer=1)
586689
axis > 0 || throw(ArgumentError("axis must be a positive integer"))
587690
inds_t = indices(A)
588691
indices(B) == inds_t || throw(DimensionMismatch("shape of B must match A"))
@@ -603,12 +706,12 @@ function cumop!(op, B, A, axis::Integer)
603706
else
604707
R1 = CartesianRange(indices(A)[1:axis-1]) # not type-stable
605708
R2 = CartesianRange(indices(A)[axis+1:end])
606-
_cumop!(op, B, A, R1, inds_t[axis], R2) # use function barrier
709+
_accumulate!(op, B, A, R1, inds_t[axis], R2) # use function barrier
607710
end
608711
return B
609712
end
610713

611-
@noinline function _cumop!(op, B, A, R1, ind, R2)
714+
@noinline function _accumulate!(op, B, A, R1, ind, R2)
612715
# Copy the initial element in each 1d vector along dimension `axis`
613716
i = first(ind)
614717
@inbounds for J in R2, I in R1

doc/stdlib/arrays.rst

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,6 +1499,32 @@ Indexing, Assignment, and Concatenation
14991499
Array functions
15001500
---------------
15011501

1502+
.. function:: accumulate(op, A, dim=1)
1503+
1504+
.. Docstring generated from Julia source
1505+
1506+
Cumulative operation ``op`` along a dimension ``dim`` (defaults to 1). See also :func:`accumulate!` to use a preallocated output array, both for performance and to control the precision of the output (e.g. to avoid overflow). For common operations there are specialized variants of accumulate, see: :func:`cumsum`\ , :func:`cumprod`
1507+
1508+
.. doctest::
1509+
1510+
julia> accumulate(+, [1,2,3])
1511+
3-element Array{Int64,1}:
1512+
1
1513+
3
1514+
6
1515+
1516+
julia> accumulate(*, [1,2,3])
1517+
3-element Array{Int64,1}:
1518+
1
1519+
2
1520+
6
1521+
1522+
.. function:: accumulate!(op, B, A, dim=1)
1523+
1524+
.. Docstring generated from Julia source
1525+
1526+
Cumulative operation ``op`` on ``A`` along a dimension, storing the result in ``B``\ . The dimension defaults to 1. See also :func:`accumulate`\ .
1527+
15021528
.. function:: cumprod(A, dim=1)
15031529

15041530
.. Docstring generated from Julia source

0 commit comments

Comments
 (0)