Skip to content

Commit 9a95543

Browse files
Merge pull request #2 from gabrielgellner/vector_of_array
Add code that gives an nice Array interface to a vector of arrays
2 parents cadcf1f + 3807a3c commit 9a95543

File tree

7 files changed

+236
-7
lines changed

7 files changed

+236
-7
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,34 @@
88
RecursiveArrayTools.jl is a set of tools for dealing with recursive arrays like
99
arrays of arrays. The current functionality includes:
1010

11+
### Types
12+
13+
```julia
14+
ArrayPartition(x::AbstractArray...)
15+
```
16+
17+
An `ArrayPartition` `A` is an array which is made up of different arrays `A.x`.
18+
These index like a single array, but each subarray may have a different type.
19+
However, broadcast is overloaded to loop in an efficient manner, meaning that
20+
`A .+= 2.+B` is type-stable in its computations, even if `A.x[i]` and `A.x[j]`
21+
do not match types. A full array interface is included for completeness, which
22+
allows this array type to be used in place of a standard array in places where
23+
such a type stable broadcast may be needed. One example is in heterogeneous
24+
differential equations for [DifferentialEquations.jl](https://github.com/JuliaDiffEq/DifferentialEquations.jl).
25+
26+
An `ArrayPartition` acts like a single array. `A[i]` indexes through the first
27+
array, then the second, etc. all linearly. But `A.x` is where the arrays are stored.
28+
Thus for
29+
30+
```julia
31+
using RecursiveArrayTools
32+
A = ArrayPartition(y,z)
33+
```
34+
35+
We would have `A.x[1]==y` and `A.x[2]==z`. Broadcasting like `f.(A)` is efficient.
36+
37+
### Functions
38+
1139
```julia
1240
recursivecopy!(b::Array{T,N},a::Array{T,N})
1341
```
@@ -33,3 +61,16 @@ copyat_or_push!{T}(a::AbstractVector{T},i::Int,x)
3361

3462
If `i<length(x)`, it's simply a `recursivecopy!` to the `i`th element. Otherwise it will
3563
`push!` a `deepcopy`.
64+
65+
```julia
66+
recursive_one(a)
67+
```
68+
69+
Calls `one` on the bottom container to get the "true element one type"
70+
71+
```julia
72+
mean{T<:AbstractArray}(vecvec::Vector{T})
73+
mean{T<:AbstractArray}(matarr::Matrix{T},region=0)
74+
```
75+
76+
Generalized mean functions for vectors of arrays and matrix of arrays.

src/RecursiveArrayTools.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@ module RecursiveArrayTools
77
import Base: mean
88

99
include("utils.jl")
10+
include("vector_of_array.jl")
1011
include("array_partition.jl")
1112

13+
export VectorOfArray, vecarr_to_arr
14+
1215
export recursivecopy!, vecvecapply, copyat_or_push!, vecvec_to_mat, recursive_one,mean
1316

1417
export ArrayPartition

src/array_partition.jl

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,45 @@ function ArrayPartition{T}(x,::Type{Val{T}}=Val{false})
1111
end
1212
Base.similar(A::ArrayPartition) = ArrayPartition((similar(x) for x in A.x)...)
1313
Base.similar(A::ArrayPartition,dims::Tuple) = ArrayPartition((similar(x,dim) for (x,dim) in zip(A.x,dims))...)
14+
Base.similar(A::ArrayPartition,T,dims::Tuple) = ArrayPartition((similar(x,T,dim) for (x,dim) in zip(A.x,dims))...)
1415
Base.copy(A::ArrayPartition) = Base.similar(A)
1516
Base.zeros(A::ArrayPartition) = ArrayPartition((zeros(x) for x in A.x)...)
1617

18+
# Special to work with units
19+
function Base.ones(A::ArrayPartition)
20+
B = similar(A::ArrayPartition)
21+
for i in eachindex(A.x)
22+
B.x[i] .= eltype(A.x[i])(one(first(A.x[i])))
23+
end
24+
B
25+
end
26+
27+
Base.:+(A::ArrayPartition, B::ArrayPartition) = ArrayPartition((x .+ y for (x,y) in zip(A.x,B.x))...)
28+
Base.:+(A::Number, B::ArrayPartition) = ArrayPartition((A .+ x for x in B.x)...)
29+
Base.:+(A::ArrayPartition, B::Number) = ArrayPartition((B .+ x for x in A.x)...)
30+
Base.:-(A::ArrayPartition, B::ArrayPartition) = ArrayPartition((x .- y for (x,y) in zip(A.x,B.x))...)
31+
Base.:-(A::Number, B::ArrayPartition) = ArrayPartition((A .- x for x in B.x)...)
32+
Base.:-(A::ArrayPartition, B::Number) = ArrayPartition((x .- B for x in A.x)...)
1733
Base.:*(A::Number, B::ArrayPartition) = ArrayPartition((A .* x for x in B.x)...)
1834
Base.:*(A::ArrayPartition, B::Number) = ArrayPartition((x .* B for x in A.x)...)
1935
Base.:/(A::ArrayPartition, B::Number) = ArrayPartition((x ./ B for x in A.x)...)
2036
Base.:\(A::Number, B::ArrayPartition) = ArrayPartition((x ./ A for x in B.x)...)
2137

38+
if VERSION < v"0.6-"
39+
Base.:.+(A::ArrayPartition, B::ArrayPartition) = ArrayPartition((x .+ y for (x,y) in zip(A.x,B.x))...)
40+
Base.:.+(A::Number, B::ArrayPartition) = ArrayPartition((A .+ x for x in B.x)...)
41+
Base.:.+(A::ArrayPartition, B::Number) = ArrayPartition((B .+ x for x in A.x)...)
42+
Base.:.-(A::ArrayPartition, B::ArrayPartition) = ArrayPartition((x .- y for (x,y) in zip(A.x,B.x))...)
43+
Base.:.-(A::Number, B::ArrayPartition) = ArrayPartition((A .- x for x in B.x)...)
44+
Base.:.-(A::ArrayPartition, B::Number) = ArrayPartition((x .- B for x in A.x)...)
45+
Base.:.*(A::ArrayPartition, B::ArrayPartition) = ArrayPartition((x .* y for (x,y) in zip(A.x,B.x))...)
46+
Base.:.*(A::Number, B::ArrayPartition) = ArrayPartition((A .* x for x in B.x)...)
47+
Base.:.*(A::ArrayPartition, B::Number) = ArrayPartition((x .* B for x in A.x)...)
48+
Base.:./(A::ArrayPartition, B::ArrayPartition) = ArrayPartition((x ./ y for (x,y) in zip(A.x,B.x))...)
49+
Base.:./(A::ArrayPartition, B::Number) = ArrayPartition((x ./ B for x in A.x)...)
50+
Base.:.\(A::Number, B::ArrayPartition) = ArrayPartition((x ./ A for x in B.x)...)
51+
end
52+
2253
@inline function Base.getindex( A::ArrayPartition,i::Int)
2354
@boundscheck i > length(A) && throw(BoundsError("Index out of bounds"))
2455
@inbounds for j in 1:length(A.x)
@@ -52,9 +83,9 @@ recursive_one(A::ArrayPartition) = recursive_one(first(A.x))
5283
Base.zero(A::ArrayPartition) = zero(first(A.x))
5384
Base.first(A::ArrayPartition) = first(A.x)
5485

55-
Base.start(A::ArrayPartition) = chain(A.x...)
56-
Base.next(iter::ArrayPartition,state) = next(state,state)
57-
Base.done(iter::ArrayPartition,state) = done(state,state)
86+
Base.start(A::ArrayPartition) = start(chain(A.x...))
87+
Base.next(A::ArrayPartition,state) = next(chain(A.x...),state)
88+
Base.done(A::ArrayPartition,state) = done(chain(A.x...),state)
5889

5990
Base.length(A::ArrayPartition) = sum((length(x) for x in A.x))
6091
Base.size(A::ArrayPartition) = (length(A),)
@@ -72,15 +103,12 @@ add_idxs{T<:ArrayPartition}(::Type{T},expr) = :($(expr).x[i])
72103
end
73104

74105
@generated function Base.broadcast(f,B::Union{Number,ArrayPartition}...)
75-
exs = ((add_idxs(B[i],:(B[$i])) for i in eachindex(B))...)
76106
arr_idx = 0
77107
for (i,b) in enumerate(B)
78108
if b <: ArrayPartition
79109
arr_idx = i
80110
break
81111
end
82112
end
83-
:(for i in eachindex(B[$arr_idx].x)
84-
broadcast(f,$(exs...))
85-
end)
113+
:(A = similar(B[$arr_idx]); broadcast!(f,A,B...); A)
86114
end

src/vector_of_array.jl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
abstract AbstractVectorOfArray{T, N} <: AbstractArray{T, N}
2+
3+
# Based on code from M. Bauman Stackexchange answer + Gitter discussion
4+
type VectorOfArray{T, N, A} <: AbstractVectorOfArray{T, N}
5+
u::A # A <: AbstractVector{<: AbstractArray{T, N - 1}}
6+
end
7+
8+
VectorOfArray{T, N}(vec::AbstractVector{T}, dims::NTuple{N}) = VectorOfArray{eltype(T), N, typeof(vec)}(vec)
9+
# Assume that the first element is representative all all other elements
10+
VectorOfArray(vec::AbstractVector) = VectorOfArray(vec, (size(vec[1])..., length(vec)))
11+
12+
# Interface for the linear indexing. This is just a view of the underlying nested structure
13+
@inline Base.endof(VA::AbstractVectorOfArray) = endof(VA.u)
14+
@inline Base.length(VA::AbstractVectorOfArray) = length(VA.u)
15+
# Linear indexing will be over the container elements, not the individual elements
16+
# unlike an true AbstractArray
17+
@inline Base.getindex{T, N}(VA::AbstractVectorOfArray{T, N}, I::Int) = VA.u[I]
18+
@inline Base.getindex{T, N}(VA::AbstractVectorOfArray{T, N}, I::Colon) = VA.u[I]
19+
@inline Base.getindex{T, N}(VA::AbstractVectorOfArray{T, N}, I::AbstractArray{Int}) = VA.u[I]
20+
21+
# Interface for the two dimensional indexing, a more standard AbstractArray interface
22+
@inline Base.size(VA::AbstractVectorOfArray) = (size(VA.u[1])..., length(VA.u))
23+
@inline Base.getindex{T, N}(VA::AbstractVectorOfArray{T, N}, I::Vararg{Int, N}) = VA.u[I[end]][Base.front(I)...]
24+
25+
# The iterator will be over the subarrays of the container, not the individual elements
26+
# unlike an true AbstractArray
27+
Base.start{T, N}(VA::AbstractVectorOfArray{T, N}) = 1
28+
Base.next{T, N}(VA::AbstractVectorOfArray{T, N}, state) = (VA[state], state + 1)
29+
Base.done{T, N}(VA::AbstractVectorOfArray{T, N}, state) = state >= length(VA.u) + 1
30+
31+
# Growing the array simply adds to the container vector
32+
Base.copy(VA::AbstractVectorOfArray) = typeof(VA)(copy(VA.u))
33+
Base.sizehint!{T, N}(VA::AbstractVectorOfArray{T, N}, i) = sizehint!(VA.u, i)
34+
Base.push!{T, N}(VA::AbstractVectorOfArray{T, N}, new_item::AbstractVector) = push!(VA.u, new_item)
35+
36+
function Base.append!{T, N}(VA::AbstractVectorOfArray{T, N}, new_item::AbstractVectorOfArray{T, N})
37+
for item in copy(new_item)
38+
push!(VA, item)
39+
end
40+
return VA
41+
end
42+
43+
# conversion tools
44+
vecarr_to_arr(VA::AbstractVectorOfArray) = cat(length(size(VA)), VA.u...)

test/basic_indexing.jl

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
2+
# Example Problem
3+
recs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
4+
testa = cat(2, recs...)
5+
testva = VectorOfArray(recs)
6+
testa[1:2, 1:2] == [1 4; 2 5]
7+
testva[1:2, 1:2] == [1 4; 2 5]
8+
testa[1:2, 1:2] == [1 4; 2 5]
9+
10+
# # ndims == 2
11+
recs = [rand(8) for i in 1:10]
12+
testa = cat(2, recs...)
13+
testva = VectorOfArray(recs)
14+
15+
# ## Linear indexing
16+
@test testva[1] == testa[:, 1]
17+
@test testva[:] == recs
18+
@test testva[end] == testa[:, end]
19+
@test testva[2:end] == [recs[i] for i = 2:length(recs)]
20+
21+
# ## (Int, Int)
22+
@test testa[5, 4] == testva[5, 4]
23+
24+
# ## (Int, Range) or (Range, Int)
25+
@test testa[1, 2:3] == testva[1, 2:3]
26+
@test testa[5:end, 1] == testva[5:end, 1]
27+
@test testa[:, 1] == testva[:, 1]
28+
@test testa[3, :] == testva[3, :]
29+
30+
# ## (Range, Range)
31+
@test testa[5:end, 1:2] == testva[5:end, 1:2]
32+
33+
# # ndims == 3
34+
recs = recs = [rand(10, 8) for i in 1:15]
35+
testa = cat(3, recs...)
36+
testva = VectorOfArray(recs)
37+
38+
# ## (Int, Int, Int)
39+
@test testa[1, 7, 14] == testva[1, 7, 14]
40+
41+
# ## (Int, Int, Range)
42+
@test testa[2, 3, 1:2] == testva[2, 3, 1:2]
43+
44+
# ## (Int, Range, Int)
45+
@test testa[2, 3:4, 1] == testva[2, 3:4, 1]
46+
47+
# ## (Int, Range, Range)
48+
@test testa[2, 3:4, 1:2] == testva[2, 3:4, 1:2]
49+
50+
# ## (Range, Int, Range)
51+
@test testa[2:3, 1, 1:2] == testva[2:3, 1, 1:2]
52+
53+
# ## (Range, Range, Int)
54+
@test testa[1:2, 2:3, 1] == testva[1:2, 2:3, 1]
55+
56+
# ## (Range, Range, Range)
57+
@test testa[2:3, 2:3, 1:2] == testva[2:3, 2:3, 1:2]
58+
59+
# ## Make sure that 1:1 like ranges are not collapsed
60+
@test testa[1:1, 2:3, 1:2] == testva[1:1, 2:3, 1:2]
61+
62+
# ## Test ragged arrays work, or give errors as needed
63+
#TODO: I am not really sure what the behavior of this is, what does Mathematica do?
64+
recs = [[1, 2, 3], [3 5; 6 7], [8, 9, 10, 11]]
65+
testva = VectorOfArray(recs) #TODO: clearly this printed form is nonsense
66+
@test testva[:, 1] == recs[1]
67+
testva[1:2, 1:2]

test/interface_tests.jl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
2+
recs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
3+
testva = VectorOfArray(recs)
4+
5+
for (i, elem) in enumerate(testva)
6+
@test elem == testva[i]
7+
end
8+
9+
push!(testva, [10, 11, 12])
10+
@test testva[:, end] == [10, 11, 12]
11+
testva2 = copy(testva)
12+
push!(testva2, [13, 14, 15])
13+
# make sure we copy when we pass containers
14+
@test size(testva) == (3, 4)
15+
@test testva2[:, end] == [13, 14, 15]
16+
17+
append!(testva, testva)
18+
@test testva[1:2, 5:6] == [1 4; 2 5]
19+
20+
# Test that adding a array of different dimension makes the array ragged
21+
push!(testva, [-1, -2, -3, -4])
22+
#testva #TODO: this screws up printing, try to make a fallback
23+
@test testva[1:2, 5:6] == [1 4; 2 5] # we just let the indexing happen if it works
24+
testva[4, 9] # == testva.data[9][4]
25+
@test_throws BoundsError testva[4:5, 5:6]
26+
@test testva[9] == [-1, -2, -3, -4]
27+
@test testva[end] == [-1, -2, -3, -4]
28+
29+
# Currently we enforce the general shape, they can just be different lengths, ie we
30+
# can't do
31+
# Decide if this is desired, or remove this restriction
32+
@test_throws MethodError push!(testva, [-1 -2 -3 -4])
33+
@test_throws MethodError push!(testva, [-1 -2; -3 -4])
34+
35+
# convert array from VectorOfArray
36+
recs = [rand(10, 7) for i = 1:8]
37+
testva = VectorOfArray(recs)
38+
testa = cat(3, recs...)
39+
@test vecarr_to_arr(testva) == testa
40+
41+
recs = [[1, 2, 3], [3 5; 6 7], [8, 9, 10, 11]]
42+
testva = VectorOfArray(recs)
43+
@test_throws DimensionMismatch vecarr_to_arr(testva)

test/runtests.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,7 @@ using Base.Test
55
tic()
66
@time @testset "Utils Tests" begin include("utils_test.jl") end
77
@time @testset "Partitions Tests" begin include("partitions_test.jl") end
8+
@time @testset "VecOfArr Indexing Tests" begin include("basic_indexing.jl") end
9+
@time @testset "VecOfArr Interface Tests" begin include("interface_tests.jl") end
810
toc()
11+
# Test the VectorOfArray code

0 commit comments

Comments
 (0)