Skip to content

Commit fce4cf6

Browse files
author
Michael Abbott
committed
Merge branch 'master' into ragged
2 parents d6326ae + 48b7680 commit fce4cf6

File tree

6 files changed

+216
-34
lines changed

6 files changed

+216
-34
lines changed

.github/workflows/TagBot.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name: TagBot
2+
on:
3+
schedule:
4+
- cron: 0 * * * *
5+
jobs:
6+
TagBot:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: JuliaRegistries/TagBot@v1
10+
with:
11+
token: ${{ secrets.GITHUB_TOKEN }}

Project.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "LazyStack"
22
uuid = "1fad7336-0346-5a1a-a56f-a06ba010965b"
33
authors = ["Michael Abbott"]
4-
version = "0.0.5"
4+
version = "0.0.7"
55

66
[deps]
77
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
@@ -11,8 +11,8 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1111
ZygoteRules = "700de1a5-db45-46bc-99cf-38207098b444"
1212

1313
[compat]
14-
NamedDims = "0.2.12"
15-
OffsetArrays = "0.11, 0.12, 0.13"
14+
NamedDims = "0.2.16"
15+
OffsetArrays = "0.11, 0.12, 0.13, 1.0"
1616
ZygoteRules = "0.1, 0.2, 0.3"
1717
julia = "1"
1818

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,29 @@
44

55
This package exports one function, `stack`, for turning a list of arrays
66
into one `AbstractArray`. Given several arrays with the same `eltype`,
7-
or an array of such arrays, it returns a lazy `Stacked{T,N}` view of these.
7+
or an array of such arrays, it returns a lazy `Stacked{T,N}` view of these:
88

99
```julia
1010
stack([zeros(2,2), ones(2,2)]) # isa Stacked{Float64, 3, <:Vector{<:Matrix}}
1111
stack([1,2,3], 4:6) # isa Stacked{Int, 2, <:Tuple{<:Vector, <:UnitRange}}
1212
```
1313

1414
Given a generator, it instead iterates through the elements and writes into a new array.
15-
(This is lazy only in that it need not `collect` the generator first.)
16-
The same method is also used for any list of arrays of heterogeneous element type.
15+
Given a function and then some arrays, it behaves like `map(f, A, B)` but immediately writes
16+
into a new array:
1717

1818
```julia
1919
stack([i,2i] for i in 1:5) # isa Matrix{Int} # size(ans) == (2, 5)
20+
stack(*, eachcol(ones(2,4)), 1:4) # == Matrix(stack(map(*, eachcol(...), 1:4)))
21+
```
22+
23+
The same `stack_iter` method is also used for any list of arrays of heterogeneous element type,
24+
and for arrays of tuples. Notice that like `map(identity, Any[1, 1.0, 5im])`, this promotes using
25+
`promote_typejoin`, to `Number` here, rather than to `Complex{Float64}`.
26+
27+
```julia
2028
stack([1,2], [3.0, 4.0], [5im, 6im]) # isa Matrix{Number} # size(ans) == (2, 3)
29+
stack([(i,2.0,3//j) for i=1:4, j=1:5])# isa Array{Real, 3} # size(ans) == (3, 4, 5)
2130
```
2231

2332
The slices must all have the same `size`, but they (and the container)
@@ -33,6 +42,8 @@ This one plays well with [OffsetArrays.jl](https://github.com/JuliaArrays/Offset
3342
Besides which, there are several other ways to achieve similar things:
3443

3544
* For an array of arrays, you can also use [`JuliennedArrays.Align`](https://bramtayl.github.io/JuliennedArrays.jl/latest/#JuliennedArrays.Align). This requires (or enables) you to specify which dimensions of the output belong to the sub-arrays, instead of writing `PermutedDimsArray(stack(...), ...)`.
36-
* There is also [`RecursiveArrayTools.VectorOfArray`](https://github.com/JuliaDiffEq/RecursiveArrayTools.jl#vectorofarray) which as its name hints only allows a one-dimensional container.
45+
* There is also [`RecursiveArrayTools.VectorOfArray`](https://github.com/JuliaDiffEq/RecursiveArrayTools.jl#vectorofarray) which as its name hints only allows a one-dimensional container. Linear indexing retreives a slice, not an element, which is sometimes surprising.
3746
* For a tuple of arrays, [`LazyArrays.Hcat`](https://github.com/JuliaArrays/LazyArrays.jl#concatenation) is at present faster to index than `stack`, but doesn't allow arbitrary dimensions.
3847
* For a generator of arrays, the built-in `reduce(hcat,...)` may work, but it slow compared to `stack`: see [test/speed.jl](test/speed.jl) for some examples.
48+
49+
The package [ArraysOfArrays.jl](https://oschulz.github.io/ArraysOfArrays.jl/stable/#section_ArrayOfSimilarArrays-1) solves the opposite problem, of accessing one large array as if it were many slices. As does [`JuliennedArrays.Slices`](https://bramtayl.github.io/JuliennedArrays.jl/latest/#JuliennedArrays.Slices-Union{Tuple{NumberOfDimensions},%20Tuple{Item},%20Tuple{AbstractArray{Item,NumberOfDimensions},Vararg{Int64,N}%20where%20N}}%20where%20NumberOfDimensions%20where%20Item), and of course [`Base.eachslice`](https://docs.julialang.org/en/v1/base/arrays/#Base.eachslice).

src/LazyStack.jl

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module LazyStack
22

33
export stack, rstack
44

5-
#===== Overloads =====#
5+
#===== Tuples =====#
66

77
ndims(A) = Base.ndims(A)
88
ndims(::Tuple) = 1
@@ -12,6 +12,13 @@ size(A) = Base.size(A)
1212
size(t::Tuple) = tuple(length(t))
1313
size(t::NamedTuple) = tuple(length(t))
1414

15+
similar_vector(x::AbstractArray, n::Int) = similar(x, n::Int)
16+
similar_vector(x::Tuple, n::Int) = Vector{eltype(x)}(undef, n::Int)
17+
similar_vector(x::NamedTuple, n::Int) = Vector{eltype(x)}(undef, n::Int)
18+
19+
# eltype(x::Tuple) = Base.promote_type(x...) # to match choice below
20+
# eltype(x::NamedTuple) = Base.promote_type(x...)
21+
1522
#===== Slices =====#
1623

1724
"""
@@ -56,6 +63,7 @@ stack(xs::AbstractArray{T}...) where {T} = stack(xs)
5663

5764
function stack_slices(xs::AT, ::Val{T}, ::Val{N}) where {T,N,AT}
5865
length(xs) >= 1 || throw(ArgumentError("stacking an empty collection is not allowed"))
66+
storage_type(first(xs)) <: Union{Array, AbstractRange} || return stack_iter(xs)
5967
s = size(first(xs))
6068
for x in xs
6169
size(x) == s || throw(DimensionMismatch(
@@ -83,7 +91,7 @@ outer_ndims(x::Stacked{T,N,<:Tuple}) where {T,N} = 1
8391

8492
inner_ndims(x::Stacked) = ndims(x) - outer_ndims(x)
8593

86-
@inline function Base.getindex(x::Stacked{T}, inds::Integer...) where {T}
94+
@inline function Base.getindex(x::Stacked{T,N}, inds::Vararg{Integer,N}) where {T,N}
8795
@boundscheck checkbounds(x, inds...)
8896
IN, ON = inner_ndims(x), outer_ndims(x)
8997
outer = @inbounds getindex(x.slices, ntuple(d -> inds[d+IN], ON)...)
@@ -98,6 +106,14 @@ Base.collect(x::Stacked{T,2,<:AbstractArray{<:AbstractArray{T,1}}}) where {T} =
98106

99107
Base.view(x::Stacked{T,2,<:AbstractArray{<:AbstractArray{T,1}}}, ::Colon, i::Int) where {T} = x.slices[i]
100108

109+
function Base.push!(x::Stacked{T,N,<:AbstractVector}, y::AbstractArray) where {T,N}
110+
s = size(first(x.slices))
111+
isempty(y) || size(y) == s || throw(DimensionMismatch(
112+
"slices being stacked must share a common size. Expected $s, got $(size(y))"))
113+
push!(x.slices, y)
114+
x
115+
end
116+
101117
function Base.showarg(io::IO, x::Stacked, toplevel)
102118
print(io, "stack(")
103119
Base.showarg(io, parent(x), false)
@@ -111,12 +127,20 @@ ITERS = [:Flatten, :Drop, :Filter]
111127
for iter in ITERS
112128
@eval ndims(::Iterators.$iter) = 1
113129
end
130+
ndims(gen::Base.Generator) = ndims(gen.iter)
131+
ndims(zed::Iterators.Zip) = maximum(ndims, zed.is)
132+
if VERSION < v"1.1"
133+
ndims(zed::Iterators.Zip2) = max(ndims(zed.a), ndims(zed.b))
134+
end
135+
136+
similar_vector(x, n::Int) = throw(ArgumentError())
114137

115138
"""
116139
stack(::Generator)
117140
stack(::Array{T}, ::Array{S}, ...)
118141
119-
This constructs a new array. Can handle inconsistent eltypes, but not inconsistent sizes.
142+
This constructs a new array, calling `stack_iter`.
143+
Can handle inconsistent eltypes, but not inconsistent sizes.
120144
121145
```
122146
julia> stack([i i; i 10i] for i in 1:2:3)
@@ -166,10 +190,9 @@ function stack_iter(itr)
166190

167191
w, val = vstack_plus(itr)
168192

169-
z = reshape(w, size(val)..., outsize...)::Array
193+
z = reshape(w, size(val)..., outsize...)
170194

171-
z′ = maybe_add_offsets(z, val)
172-
maybe_add_names(z′, val)
195+
rewrap_like(z, val)
173196
end
174197

175198
vstack(itr) = first(vstack_plus(itr))
@@ -178,12 +201,13 @@ function vstack_plus(itr)
178201
zed = iterate(itr)
179202
zed === nothing && throw(ArgumentError("stacking an empty collection is not allowed"))
180203
val, state = zed
204+
val isa Union{AbstractArray, Tuple, NamedTuple} || return collect(itr), val
181205

182206
s = size(val)
183207
n = Base.haslength(itr) ? prod(s)*length(itr) : nothing
184208

185-
v = Vector{eltype(val)}(undef, something(n, prod(s)))
186-
copyto!(v, 1, no_offsets(val), 1, prod(s))
209+
v = similar_vector(no_wraps(val), something(n, prod(s)))
210+
copyto!(v, 1, no_wraps(val), 1, prod(s))
187211

188212
w = stack_rest(v, 0, n, s, itr, state)::Vector
189213
w, val
@@ -200,9 +224,9 @@ function stack_rest(v, i, n, s, itr, state)
200224
i += 1
201225
if eltype(val) <: eltype(v)
202226
if n isa Int
203-
copyto!(v, i*prod(s)+1, no_offsets(val), 1, prod(s))
227+
copyto!(v, i*prod(s)+1, no_wraps(val), 1, prod(s))
204228
else
205-
append!(v, vec(no_offsets(val)))
229+
append!(v, vec(no_wraps(val)))
206230
end
207231
else
208232

@@ -212,9 +236,9 @@ function stack_rest(v, i, n, s, itr, state)
212236
copyto!(v′, v)
213237

214238
if n isa Int
215-
copyto!(v′, i*prod(s)+1, no_offsets(val), 1, prod(s))
239+
copyto!(v′, i*prod(s)+1, no_wraps(val), 1, prod(s))
216240
else
217-
append!(v′, vec(no_offsets(val)))
241+
append!(v′, vec(no_wraps(val)))
218242
end
219243

220244
return stack_rest(v′, i, n, s, itr, state)
@@ -223,53 +247,85 @@ function stack_rest(v, i, n, s, itr, state)
223247
end
224248
end
225249

250+
"""
251+
stack(fun, iters...)
252+
stack(eachcol(A), eachslice(B, dims=3)) do a, b
253+
f(a,b)
254+
end
255+
256+
If the first argument is a function, then this is mapped over the other argumenst.
257+
The result should be `== stack(map(fun, iters...))`, but done using `stack_iter`
258+
to return a dense `Array` instead of a `Stacked` object.
259+
"""
260+
stack(fun::Function, iter) = stack(fun(arg) for arg in iter)
261+
function stack(fun::Function, iters...)
262+
if all(Base.haslength, iters)
263+
sz = size(first(iters))
264+
all(a -> size(a)==sz, iters) || throw(DimensionMismatch(
265+
"sizes of all argumens must match, in stack(f, A, B, C). " *
266+
"This is slightly stricter than map(f, A, B, C), for now."))
267+
end
268+
stack(fun(args...) for args in zip(iters...))
269+
end
270+
226271
#===== Offset =====#
227272

228273
using OffsetArrays
229274

230-
no_offsets(a) = a
231-
no_offsets(a::OffsetArray) = parent(a)
275+
no_wraps(a) = a
276+
no_wraps(a::OffsetArray) = parent(a)
232277

233-
maybe_add_offsets(A, a) = A
234-
maybe_add_offsets(A, a::OffsetArray) = OffsetArray(A, axes(a)..., axes(A, ndims(A)))
278+
rewrap_like(A, a) = A
279+
function rewrap_like(A, a::OffsetArray)
280+
B = rewrap_like(A, parent(a))
281+
OffsetArray(B, axes(a)..., axes(A, ndims(A)))
282+
end
235283

236284
#===== NamedDims =====#
237285

238286
using NamedDims
239287

288+
ensure_named(a::AbstractArray, L::Tuple) = NamedDimsArray(a, L)
289+
ensure_named(a::NamedDimsArray, L::Tuple) = refine_names(a, L)
290+
240291
# array of arrays
241292
stack(xs::NamedDimsArray{<:Any,<:AbstractArray}) =
242-
NamedDimsArray(stack(parent(xs)), getnames(xs))
293+
ensure_named(stack(parent(xs)), getnames(xs))
243294
stack(x::AT) where {AT <: AbstractArray{<:NamedDimsArray{L,T,IN},ON}} where {T,IN,ON,L} =
244-
NamedDimsArray(Stacked{T, IN+ON, AT}(x), getnames(x))
295+
ensure_named(Stacked{T, IN+ON, AT}(x), getnames(x))
245296

246297
getnames(xs::AbstractArray{<:AbstractArray}) =
247298
(dimnames(eltype(xs))..., dimnames(xs)...)
248299

249300
# tuple of arrays
250301
stack(x::AT) where {AT <: Tuple{Vararg{NamedDimsArray{L,T,IN}}}} where {T,IN,L} =
251-
NamedDimsArray(Stacked{T, IN+1, AT}(x), getnames(x))
302+
ensure_named(stack(map(parent, x)), getnames(x))
252303

253304
getnames(xs::Tuple{Vararg{<:NamedDimsArray}}) =
254305
(dimnames(first(xs))..., :_)
255306

256307
# generators
308+
#=
257309
function stack(xs::Base.Generator{<:NamedDimsArray{L}}) where {L}
258310
w = stack_iter(xs)
259311
l = (ntuple(_ -> :_, ndims(w)-length(L))..., L...)
260-
NamedDimsArray(w, l)
312+
ensure_named(w, l)
261313
end
262314
263315
function stack(xs::Base.Generator{<:Iterators.ProductIterator{<:Tuple{<:NamedDimsArray}}})
264316
w = stack_iter(xs)
265317
L = Tuple(Iterators.flatten(map(dimnames, ms.iter.iterators)))
266318
l = (ntuple(_ -> :_, ndims(w)-length(L))..., L...)
267-
NamedDimsArray(w, l)
319+
ensure_named(w, l)
268320
end
321+
=#
269322

270-
maybe_add_names(A, a) = A
271-
maybe_add_names(A, a::NamedDimsArray{L}) where {L} =
272-
NamedDimsArray(A, (L..., ntuple(_ -> :_, ndims(A) - ndims(a))...))
323+
function rewrap_like(A, a::NamedDimsArray{L}) where {L}
324+
B = rewrap_like(A, parent(a))
325+
ensure_named(B, (L..., ntuple(_ -> :_, ndims(A) - ndims(a))...))
326+
end
327+
328+
no_wraps(a::NamedDimsArray) = no_wraps(parent(a))
273329

274330
"""
275331
stack(name, things...)
@@ -281,7 +337,7 @@ this will be the name of the last dimension of the resulting `NamedDimsArray`.
281337
function LazyStack.stack(s::Symbol, args...)
282338
data = stack(args...)
283339
name_last = ntuple(d -> d==ndims(data) ? s : :_, ndims(data))
284-
NamedDimsArray(data, name_last)
340+
ensure_named(data, name_last)
285341
end
286342

287343
#===== Zygote =====#
@@ -300,6 +356,18 @@ end
300356
stack(gen), Δ -> error("not yet!")
301357
end
302358

359+
@adjoint function Base.collect(x::Stacked)
360+
collect(x), tuple
361+
end
362+
363+
#===== CuArrays =====#
364+
# Send these to stack_iter, by testing storage_type(first(xs)) <: Array
365+
366+
function storage_type(x::AbstractArray)
367+
p = parent(x)
368+
typeof(x) === typeof(p) ? typeof(x) : storage_type(p)
369+
end
370+
303371
#===== Ragged =====#
304372

305373
"""
@@ -372,4 +440,5 @@ end
372440
tupleindices(t::Tuple) = ((i,) for i in 1:length(t))
373441
tupleindices(A::AbstractArray) = (Tuple(I) for I in CartesianIndices(A))
374442

443+
375444
end # module

0 commit comments

Comments
 (0)