Skip to content

Non-contiguous block slicing operations #462

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "BlockArrays"
uuid = "8e7c35d0-a365-5155-bbbb-fb81a777f24e"
version = "1.6.3"
version = "1.7.0"

[deps]
ArrayLayouts = "4c555306-a7a7-4459-81d9-ec55ddd5c99a"
Expand Down
67 changes: 50 additions & 17 deletions src/blockindices.jl
Original file line number Diff line number Diff line change
Expand Up @@ -140,23 +140,23 @@ julia> a[BlockIndex((2,2), (2,3))]
20
```
"""
struct BlockIndex{N,TI<:Tuple{Vararg{Integer,N}},Tα<:Tuple{Vararg{Integer,N}}}
struct BlockIndex{N,TI<:Tuple{Vararg{Any,N}},Tα<:Tuple{Vararg{Any,N}}}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generalization enables constructing indexing objects such as Block(1)[Block(2)] or Block(1)[Block(2)[3]], for example in the case when a block array has blocks that are themselves blocked. I have use cases for that where I want to construct a block array where the blocks have extra structure and custom non-integer indexing (in particular, the blocks have a Kronecker product structure, so I want to be able to slice the blocks while preserving that structure).

I::TI
α::Tα
end

@inline BlockIndex(a::NTuple{N,Block{1}}, b::Tuple) where N = BlockIndex(Int.(a), b)
@inline BlockIndex(::Tuple{}, b::Tuple{}) = BlockIndex{0,Tuple{},Tuple{}}((), ())

@inline BlockIndex(a::Integer, b::Integer) = BlockIndex((a,), (b,))
@inline BlockIndex(a::Tuple, b::Integer) = BlockIndex(a, (b,))
@inline BlockIndex(a::Integer, b::Tuple) = BlockIndex((a,), b)
@inline BlockIndex(a, b) = BlockIndex((a,), (b,))
@inline BlockIndex(a::Tuple, b) = BlockIndex(a, (b,))
@inline BlockIndex(a, b::Tuple) = BlockIndex((a,), b)
@inline BlockIndex() = BlockIndex((), ())

@inline BlockIndex(a::Block, b::Tuple) = BlockIndex(a.n, b)
@inline BlockIndex(a::Block, b::Integer) = BlockIndex(a, (b,))
@inline BlockIndex(a::Block, b) = BlockIndex(a, (b,))

@inline function BlockIndex(I::Tuple{Vararg{Integer,N}}, α::Tuple{Vararg{Integer,M}}) where {M,N}
@inline function BlockIndex(I::Tuple{Vararg{Any,N}}, α::Tuple{Vararg{Any,M}}) where {M,N}
M <= N || throw(ArgumentError("number of indices must not exceed the number of blocks"))
α2 = ntuple(k -> k <= M ? α[k] : 1, N)
BlockIndex(I, α2)
Expand All @@ -182,10 +182,10 @@ end
checkbounds(::Type{Bool}, A::AbstractArray{<:Any,N}, I::AbstractVector{<:BlockIndex{N}}) where N =
all(checkbounds.(Bool, Ref(A), I))

struct BlockIndexRange{N,R<:Tuple{Vararg{AbstractUnitRange{<:Integer},N}},I<:Tuple{Vararg{Integer,N}},BI<:Integer} <: AbstractArray{BlockIndex{N,NTuple{N,BI},I},N}
struct BlockIndexRange{N,R<:Tuple{Vararg{AbstractVector,N}},I<:Tuple{Vararg{Any,N}},BI} <: AbstractArray{BlockIndex{N,NTuple{N,BI},I},N}
block::Block{N,BI}
indices::R
function BlockIndexRange(block::Block{N,BI}, inds::R) where {N,BI<:Integer,R<:Tuple{Vararg{AbstractUnitRange{<:Integer},N}}}
function BlockIndexRange(block::Block{N,BI}, inds::R) where {N,BI<:Integer,R<:Tuple{Vararg{AbstractVector,N}}}
I = Tuple{eltype.(inds)...}
return new{N,R,I,BI}(block,inds)
end
Expand All @@ -198,20 +198,20 @@ represents a cartesian range inside a block.
"""
BlockIndexRange

BlockIndexRange(block::Block{N}, inds::Vararg{AbstractUnitRange{<:Integer},N}) where {N} =
BlockIndexRange(block::Block{N}, inds::Vararg{AbstractVector,N}) where {N} =
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This generalization enables constructing non-contiguous subblock slices such as Block(2)[[1, 3]]. My biggest concern about this change is that BlockIndexRange is kind of a strange name for that object since it isn't a range. I can't really think of a better name though. One thing to consider could be renaming this struct to something more general like BlockIndexVector (or maybe BlockIndices, though that clashes with an alternative definition of BlockIndices proposed in #356) and then define BlockIndexRange as a type alias where the indices are constrained to AbstractUnitRange{<:Integer}.

BlockIndexRange(block,inds)

block(R::BlockIndexRange) = R.block

copy(R::BlockIndexRange) = BlockIndexRange(R.block, map(copy, R.indices))

getindex(::Block{0}) = BlockIndex()
getindex(B::Block{N}, inds::Vararg{Integer,N}) where N = BlockIndex(B,inds)
getindex(B::Block{N}, inds::Vararg{AbstractUnitRange{<:Integer},N}) where N = BlockIndexRange(B,inds)
getindex(B::Block{N}, inds::Vararg{Any,N}) where N = BlockIndex(B,inds)
getindex(B::Block{N}, inds::Vararg{AbstractVector,N}) where N = BlockIndexRange(B,inds)
getindex(B::Block{1}, inds::Colon) = B
getindex(B::Block{1}, inds::Base.Slice) = B

@propagate_inbounds getindex(B::BlockIndexRange{1}, kr::AbstractUnitRange{<:Integer}) = BlockIndexRange(B.block, B.indices[1][kr])
@propagate_inbounds getindex(B::BlockIndexRange{N}, kr::Vararg{AbstractVector,N}) where N = BlockIndexRange(B.block, map(getindex, B.indices, kr))
@propagate_inbounds getindex(B::BlockIndexRange{N}, inds::Vararg{Int,N}) where N = B.block[Base.reindex(B.indices, inds)...]

eltype(R::BlockIndexRange) = eltype(typeof(R))
Expand Down Expand Up @@ -256,10 +256,11 @@ Block(bs::BlockIndexRange) = bs.block
"""
BlockSlice(block, indices)

Represent an AbstractUnitRange{<:Integer} of indices that attaches a block.
Represents an AbstractUnitRange{<:Integer} of indices attached to a block,
a subblock, or a range of blocks.

Upon calling `to_indices()`, Blocks are converted to BlockSlice objects to represent
the indices over which the Block spans.
the indices over which the block, subblock, or range of blocks spans.

This mimics the relationship between `Colon` and `Base.Slice`.
"""
Expand All @@ -269,6 +270,7 @@ struct BlockSlice{BB,T<:Integer,INDS<:AbstractUnitRange{T}} <: AbstractUnitRange
end

Block(bs::BlockSlice{<:Block}) = bs.block
Block(bs::BlockSlice{<:BlockIndexRange}) = Block(bs.block)


for f in (:axes, :unsafe_indices, :axes1, :first, :last, :size, :length,
Expand All @@ -282,21 +284,52 @@ _indices(B) = B
@propagate_inbounds getindex(S::BlockSlice, i::Integer) = getindex(S.indices, i)
@propagate_inbounds getindex(S::BlockSlice{<:Block{1}}, k::AbstractUnitRange{<:Integer}) =
BlockSlice(S.block[_indices(k)], S.indices[_indices(k)])
@propagate_inbounds getindex(S::BlockSlice{<:BlockIndexRange{1}}, k::AbstractUnitRange{<:Integer}) =
@propagate_inbounds getindex(S::BlockSlice{<:BlockIndexRange{1,<:Tuple{AbstractUnitRange{<:Integer}}}}, k::AbstractUnitRange{<:Integer}) =
BlockSlice(S.block[_indices(k)], S.indices[_indices(k)])

# Avoid creating a SubArray wrapper in certain non-allocating cases
@propagate_inbounds view(C::CartesianIndices{N}, bs::Vararg{BlockSlice,N}) where {N} = view(C, map(x->x.indices, bs)...)

Block(bs::BlockSlice{<:BlockIndexRange}) = Block(bs.block)
"""
NoncontiguousBlockSlice(blocks, indices)

Represents an AbstractVector of indices attached to a (potentially non-contiguous) subblock,
set of blocks, or set of subblocks. This is the generalization of `BlockSlice` to
non-contiguous slices.

Upon calling `to_indices()`, a collection of blocks are converted to NoncontiguousBlockSlice objects to represent
the indices over which the blocks span.

This mimics the relationship between `Colon` and `Base.Slice`, `Block` and `BlockSlice`, etc.
"""
struct NoncontiguousBlockSlice{BB,T,INDS<:AbstractVector{T}} <: AbstractVector{T}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a relaunch of #459, hopefully with a better name, more context, and a better docstring. The idea is that BlockSlice is analogous to Slice for contiguous blockwise slices, for example when you slice with Block(2), Block(2)[1:2], and Block.(1:2) those get converted to BlockSlice by to_indices in order to store both the contiguous absolute range but also preserve the original blockwise information. Here, a NoncontiguousBlockSlice gets constructed by to_indices when you slice with non-contiguous blockwise slices such as [Block(1), Block(3)], [Block(1)[1:2], Block(3)[2:3]], and Block(2)[[2, 4]].

The reason for having two types is basically for the purpose of subyping: BlockSlice is an AbstractUnitRange and that information is used in a various parts of code, while this covers more general cases where the slices are not equivalent to unit ranges.

block::BB
indices::INDS
end

Block(bs::NoncontiguousBlockSlice{<:Block}) = bs.block
Block(bs::NoncontiguousBlockSlice{<:BlockIndexRange}) = Block(bs.block)

for f in (:axes, :unsafe_indices, :axes1, :first, :last, :size, :length,
:unsafe_length, :start)
@eval $f(S::NoncontiguousBlockSlice) = $f(S.indices)
end

_indices(B::NoncontiguousBlockSlice) = B.indices

@propagate_inbounds getindex(S::NoncontiguousBlockSlice, i::Integer) = getindex(S.indices, i)
@propagate_inbounds getindex(S::NoncontiguousBlockSlice{<:Block{1}}, k::AbstractVector{<:Integer}) =
NoncontiguousBlockSlice(S.block[_indices(k)], S.indices[_indices(k)])
@propagate_inbounds getindex(S::NoncontiguousBlockSlice{<:BlockIndexRange{1,<:Tuple{AbstractVector}}}, k::AbstractVector{<:Integer}) =
NoncontiguousBlockSlice(S.block[_indices(k)], S.indices[_indices(k)])
@propagate_inbounds getindex(S::NoncontiguousBlockSlice{<:AbstractVector{<:Block{1}}}, k::Block{1}) =
BlockSlice(S.block[Int(k)], getindex(S.indices, k))

struct BlockRange{N,R<:NTuple{N,AbstractUnitRange{<:Integer}}} <: AbstractArray{Block{N,Int},N}
indices::R
BlockRange{N,R}(inds::R) where {N,R} = new{N,R}(inds)
end


# The following is adapted from Julia v0.7 base/multidimensional.jl
# definition of CartesianRange

Expand Down
5 changes: 3 additions & 2 deletions src/views.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function unblock(A, inds, I)
end

_blockslice(B, a::AbstractUnitRange) = BlockSlice(B, a)
_blockslice(B, a) = a # drop block structure
_blockslice(B, a) = NoncontiguousBlockSlice(B, a)

# Allow `ones(2)[Block(1)[1:1], Block(1)[1:1]]` which is
# similar to `ones(2)[1:1, 1:1]`.
Expand Down Expand Up @@ -150,10 +150,11 @@ block(A::Block) = A
@inline view(block_arr::AbstractBlockArray{<:Any,N}, blocks::Vararg{BlockSlice1, N}) where N =
view(block_arr, map(block,blocks)...)

const BlockSlices = Union{Base.Slice,BlockSlice{<:BlockRange{1}}}
const BlockSlices = Union{Base.Slice,BlockSlice{<:BlockRange{1}},NoncontiguousBlockSlice{<:AbstractVector{<:Block{1}}}}
# view(V::SubArray{<:Any,N,NTuple{N,BlockSlices}},

_block_reindex(b::BlockSlice, i::Block{1}) = b.block[Int(i)]
_block_reindex(b::NoncontiguousBlockSlice, i::Block{1}) = b.block[Int(i)]
_block_reindex(b::Slice, i::Block{1}) = i

@inline view(V::SubArray{<:Any,N,<:AbstractBlockArray,<:NTuple{N,BlockSlices}}, block::Block{N}) where N =
Expand Down
16 changes: 16 additions & 0 deletions test/test_blockarrays.jl
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,22 @@ end
@test A[Block(1)[1], Block(1)[1:1]] == BlockArray(A)[Block(1)[1], Block(1)[1:1]] == A[1,1:1]
end

@testset "Non-contiguous BlockIndexRange" begin
a = BlockedArray(randn(5), [2,3])
@test a[Block(2)[[1,3]]] == a[[3,5]]
A = BlockedArray(randn(5,5), [2,3], [2,3])
@test A[Block(2,2)[[1,3],[2,3]]] == A[[3,5],[4,5]]
@test A[Block(2,2)[[1,3],1:2]] == A[[3,5],3:4]
end

@testset "Nested block indexing" begin
a = BlockedArray(randn(4), [2,2])
b = BlockedArray(randn(4), [2,2])
A = mortar([a,b])
@test A[Block(2)[Block(1)]] == A[Block(2)][Block(1)] == b[Block(1)]
@test A[Block(2)[Block(1)[2]]] == A[Block(2)][Block(1)[2]] == b[Block(1)[2]]
end

@testset "BlockIndexRange blocks" begin
a = mortar([Block(1)[1:2], Block(3)[2:3]])
@test a[Block(1)] === Block(1)[1:2]
Expand Down
49 changes: 48 additions & 1 deletion test/test_blockindices.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module TestBlockIndices

using BlockArrays, FillArrays, Test, StaticArrays, ArrayLayouts
using OffsetArrays
import BlockArrays: BlockIndex, BlockIndexRange, BlockSlice
import BlockArrays: BlockIndex, BlockIndexRange, BlockSlice, NoncontiguousBlockSlice

@testset "Blocks" begin
@test Int(Block(2)) === Integer(Block(2)) === Number(Block(2)) === 2
Expand Down Expand Up @@ -86,16 +86,27 @@ import BlockArrays: BlockIndex, BlockIndexRange, BlockSlice
@testset "BlockIndex" begin
@test Block()[] == BlockIndex()
@test Block(1)[1] == BlockIndex((1,),(1,))
@test Block(1)[Block(1)] == BlockIndex((1,),(Block(1),))
@test Block(1)[1:2] == BlockIndexRange(Block(1),(1:2,))
@test Block(1)[[1,3]] == BlockIndexRange(Block(1),([1,3],))
@test Block(1,1)[1,1] == BlockIndex((1,1),(1,1)) == BlockIndex((1,1),(1,))
@test Block(1,1)[1:2,1:2] == BlockIndexRange(Block(1,1),(1:2,1:2))
@test Block(1,1)[[1,3],1:2] == BlockIndexRange(Block(1,1),([1,3],1:2))
@test Block(1)[1:3][1:2] == BlockIndexRange(Block(1),1:2)
@test Block(1)[[1,3,5]][[1,3]] == BlockIndexRange(Block(1),[1,5])
@test Block(1)[[1,3,5]][2:3] == BlockIndexRange(Block(1),[3,5])
@test Block(1)[2:4][[1,3]] == BlockIndexRange(Block(1),[2,4])
@test Block(1,1)[1:3,1:3][1:2,1:2] == BlockIndexRange(Block(1,1),1:2,1:2)
@test Block(1,1)[1:3,1:3][1:2,[1,3]] == BlockIndexRange(Block(1,1),1:2,[1,3])
@test BlockIndex((2,2,2),(2,)) == BlockIndex((2,2,2),(2,1,)) == BlockIndex((2,2,2),(2,1,1))
@test BlockIndex(2,(2,)) === BlockIndex((2,),(2,))
@test BlockIndex(UInt(2),(2,)) === BlockIndex((UInt(2),),(2,))
@test BlockIndex(Block(2),2) === BlockIndex(Block(2),(2,))
@test BlockIndex(Block(2),UInt(2)) === BlockIndex(Block(2),(UInt(2),))
@test BlockIndex(Block(2),Block(2)) === BlockIndex(Block(2),(Block(2),))
@test copy(Block(1)[1:2]) === BlockIndexRange(Block(1),1:2)
@test copy(Block(1)[[1,3]]) == BlockIndexRange(Block(1),[1,3])
@test copy(Block(1)[[1,3]]) ≢ BlockIndexRange(Block(1),[1,3])
end

@testset "BlockRange" begin
Expand Down Expand Up @@ -566,6 +577,7 @@ end
b = blockedrange([1,2,3])
@test b[Block(3)[2]] == b[Block(3)][2] == 5
@test b[Block(3)[2:3]] == b[Block(3)][2:3] == 5:6
@test b[Block(3)[[3,2]]] == b[Block(3)][[3,2]] == [6,5]
end

@testset "BlockRange indexing" begin
Expand Down Expand Up @@ -814,6 +826,10 @@ end
@test b[1:2] ≡ b[1:2][1:2] ≡ BlockSlice(Block(5)[1:2],1:2)
@test Block(b) ≡ Block(5)

bi = BlockSlice(Block(2)[2:4],3:5)
@test Block(bi) ≡ Block(2)
@test bi[2:3] ≡ BlockSlice(Block(2)[3:4],4:5)

@testset "OneTo converts" begin
for b in (BlockSlice(Block(1), 1:1), BlockSlice(Block.(1:1), 1:1), BlockSlice(Block(1)[1:1], 1:1))
@test convert(typeof(b), Base.OneTo(1)) ≡ b
Expand All @@ -831,6 +847,37 @@ end
end
end

@testset "NoncontiguousBlockSlice" begin
b = NoncontiguousBlockSlice([Block(2),Block(1)], mortar([3:5,1:2]))
@test length(b) == 5
for i in eachindex(b.indices)
@test b[i] === b.indices[i]
end
@test b[Block(1)] === BlockSlice(Block(2), 3:5)
@test b[Block(2)] === BlockSlice(Block(1), 1:2)
@test BlockArrays._indices(b) == mortar([3:5,1:2])

b = NoncontiguousBlockSlice(Block(3), 2:4)
@test b[2:3] == NoncontiguousBlockSlice(Block(3)[3:4], 3:4)
@test b[[1,3]] == NoncontiguousBlockSlice(Block(3)[[1,3]], [2,4])
@test Block(b) === Block(3)
@test BlockArrays._indices(b) === 2:4

b = NoncontiguousBlockSlice(Block(3)[[2,4,6]], [3,5,7])
@test b isa NoncontiguousBlockSlice{<:BlockIndexRange{1}}
@test Block(b) === Block(3)
@test BlockArrays._indices(b) == [3,5,7]
@test b[2:3] == NoncontiguousBlockSlice(Block(3)[[4,6]], [5,7])
@test b[2:3] isa NoncontiguousBlockSlice{<:BlockIndexRange{1}}
@test Block(b) === Block(3)
@test Block(b[2:3]) === Block(3)
@test BlockArrays._indices(b[2:3]) == [5,7]
@test b[[1,3]] == NoncontiguousBlockSlice(Block(3)[[2,6]], [3,7])
@test b[[1,3]] isa NoncontiguousBlockSlice{<:BlockIndexRange{1}}
@test Block(b[[1,3]]) === Block(3)
@test BlockArrays._indices(b[[1,3]]) == [3,7]
end

#=
[1,1 1,2] | [1,3 1,4 1,5]
--------------------------
Expand Down
19 changes: 18 additions & 1 deletion test/test_blockviews.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module TestBlockViews

using BlockArrays, ArrayLayouts, Test
using FillArrays
import BlockArrays: NoncontiguousBlockSlice

# useds to force SubArray return
bview(a, b) = Base.invoke(view, Tuple{AbstractArray,Any}, a, b)
Expand Down Expand Up @@ -217,6 +218,22 @@ bview(a, b) = Base.invoke(view, Tuple{AbstractArray,Any}, a, b)
@test A[Block.(2:3)] == A[2:end]
end

@testset "getindex and view with Block-vector" begin
A = BlockArray(reshape(collect(1:(6*12)),6,12), 1:3, 3:5)
V = view(A, [Block(3),Block(2)], [Block(3),Block(2)])
@test V[Block(1,1)] == A[Block(3,3)]
@test V[Block(2,1)] == A[Block(2,3)]
@test V[Block(1,2)] == A[Block(3,2)]
@test V[Block(2,2)] == A[Block(2,2)]
I = parentindices(V)
@test I[1] isa NoncontiguousBlockSlice{<:Vector{<:Block{1}}}
@test I[2] isa NoncontiguousBlockSlice{<:Vector{<:Block{1}}}
@test view(V, Block(1,1)) === view(A, Block(3,3))
@test view(V, Block(2,1)) === view(A, Block(2,3))
@test view(V, Block(1,2)) === view(A, Block(3,2))
@test view(V, Block(2,2)) === view(A, Block(2,2))
end

@testset "non-allocation blocksize" begin
A = BlockArray(randn(5050), 1:100)
@test blocksize(A) == (100,)
Expand Down Expand Up @@ -314,7 +331,7 @@ bview(a, b) = Base.invoke(view, Tuple{AbstractArray,Any}, a, b)
@test a[Block(1)[1:3]] ≡ view(a,Block(1)[1:3]) ≡ view(v,Block(1)[1:3]) ≡ 7:9
end

@testset "blockrange-of-blockreange" begin
@testset "blockrange-of-blockrange" begin
a = mortar([7:9,5:6])
v = view(a,Block.(1:2))
@test view(v, Block(1)) ≡ 7:9
Expand Down
Loading