diff --git a/.gitignore b/.gitignore index 8c960ec..3f02ca7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.jl.cov *.jl.*.cov *.jl.mem +Manifest.toml diff --git a/Project.toml b/Project.toml index 0f60571..5ef8a72 100644 --- a/Project.toml +++ b/Project.toml @@ -11,8 +11,8 @@ OffsetArrays = "0.8, 0.9, 0.10, 0.11, 1" julia = "1" [extras] -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] test = ["Test", "Documenter"] diff --git a/src/TiledIteration.jl b/src/TiledIteration.jl index 6161ae3..c6ab805 100644 --- a/src/TiledIteration.jl +++ b/src/TiledIteration.jl @@ -11,9 +11,12 @@ else _inc(state, iter) = inc(state, iter.indices) end -export TileIterator, EdgeIterator, padded_tilesize, TileBuffer, RelaxStride, RelaxLastTile +export TileIterator, FixedTileRange, FixedTile, TileIndices, EdgeIterator, padded_tilesize, TileBuffer, RelaxStride, RelaxLastTile +include("compat.jl") include("tileiterator.jl") +include("tilerange.jl") +include("tileindices.jl") const L1cachesize = 2^15 const cachelinesize = 64 diff --git a/src/compat.jl b/src/compat.jl new file mode 100644 index 0000000..a138ae6 --- /dev/null +++ b/src/compat.jl @@ -0,0 +1,8 @@ +# https://github.com/JuliaLang/julia/pull/29442 +if VERSION <= v"1.0.5" + _oneunit(x) = Base.oneunit(x) + _oneunit(::CartesianIndex{N}) where {N} = _oneunit(CartesianIndex{N}) + _oneunit(::Type{CartesianIndex{N}}) where {N} = CartesianIndex(ntuple(x -> 1, Val(N))) +else + _oneunit = Base.oneunit +end diff --git a/src/tileindices.jl b/src/tileindices.jl new file mode 100644 index 0000000..4b307d9 --- /dev/null +++ b/src/tileindices.jl @@ -0,0 +1,86 @@ +### TileIndices + +struct TileIndices{T, N, R<:AbstractTileRange} <: AbstractArray{T, N} + indices::NTuple{N, R} + function TileIndices(indices::NTuple{N, <:AbstractTileRange{T}}) where {N, T} + new{NTuple{N, T}, N, eltype(indices)}(indices) + end +end + +""" + TileIndices(indices, sz, Δ=sz; keep_last=true) + +Construct a sliding tile along axes `indices` with fixed sliding strides `Δ` and tile size `sz`. + +# Arguments + +- `indices`: tuple of ranges, or `CartesianIndices`. +- `sz`: The size of each tile. If keyword `keep_last=true`, the last tile size might be smaller than + `sz`. +- `Δ=sz`: For each dimension `i` and `r = indices[i]`, the sliding stride `Δ[i]` is defined as + `first(r[n]) - first(r[n-1])`. Using a stride `Δ[i] < sz[i]` means there are overlaps between each + adjacent tile along this dimension. +- `keep_last=true` (keyword): this keyword affects the cases when the last tile size is smaller + than `sz`, in which case, `true`/`false` tells `TileIndices` to keep/discard the last tile. + +# Examples + +```jldoctest; setup=:(using TiledIteration) +julia> TileIndices((1:4, 0:5), (3, 4), (2, 3)) +2×2 TileIndices{Tuple{UnitRange{$Int},UnitRange{$Int}},2,FixedTileRange{UnitRange{$Int},$Int,UnitRange{$Int}}}: + (1:3, 0:3) (1:3, 3:5) + (3:4, 0:3) (3:4, 3:5) + +julia> TileIndices((1:4, 0:5), (3, 4), (2, 3); keep_last=false) +1×1 TileIndices{Tuple{UnitRange{$Int},UnitRange{$Int}},2,FixedTileRange{UnitRange{$Int},$Int,UnitRange{$Int}}}: + (1:3, 0:3) +``` + +When `sz` and `Δ` are scalars, it affects each dimension equivalently. + +```jldoctest; setup=:(using TiledIteration) +julia> TileIndices((1:4, 0:5), 3, 2) +2×3 TileIndices{Tuple{UnitRange{$Int},UnitRange{$Int}},2,FixedTileRange{UnitRange{$Int},$Int,UnitRange{$Int}}}: + (1:3, 0:2) (1:3, 2:4) (1:3, 4:5) + (3:4, 0:2) (3:4, 2:4) (3:4, 4:5) + +julia> TileIndices((1:4, 0:5), 3, 2; keep_last=false) +1×2 TileIndices{Tuple{UnitRange{$Int},UnitRange{$Int}},2,FixedTileRange{UnitRange{$Int},$Int,UnitRange{$Int}}}: + (1:3, 0:2) (1:3, 2:4) +``` + +!!! note + This method is equivalent to `TileIndices(indices, FixedTile(sz, Δ; keep_last=keep_last))`. +""" +TileIndices(indices, n, Δ=n; kwargs...) = TileIndices(indices, FixedTile(n, Δ; kwargs...)) + + +""" + TileIndices(indices, s::AbstractTileStrategy) + +Construct a sliding tile along axes `indices` using provided tile strategy `s`. + +`indices` are tuple of ranges, or `CartesianIndices`. + +Currently available strategies are: + +- [`FixedTile`](@ref): each tile is of the same fixed size (except the last one). + +For usage examples, please refer to the docstring of each tile strategy. +""" +TileIndices(indices, s::AbstractTileStrategy) = TileIndices(s(indices)) + +Base.size(iter::TileIndices) = map(length, iter.indices) + +Base.@propagate_inbounds function Base.getindex( + iter::TileIndices{T, N}, + inds::Vararg{Int, N}) where {N, T} + map(getindex, iter.indices, inds) +end +Base.@propagate_inbounds function Base.getindex( + iter::TileIndices{T, N}, + inds::Vararg{Int, N}) where {N, T<:NTuple{N, CartesianIndices}} + tile = map(getindex, iter.indices, inds) + # reformulate into CartesianIndices{N} + CartesianIndices(mapreduce(I->I.indices, (i,j)->(i..., j...), tile)) +end diff --git a/src/tilerange.jl b/src/tilerange.jl new file mode 100644 index 0000000..1e742d0 --- /dev/null +++ b/src/tilerange.jl @@ -0,0 +1,222 @@ +# AbstractTileRange Protocols +# +# The parent range `R` is supposed to be an array of scalar indices, including: +# - ranges +# - vectors of integers +# - CartesianIndices{1} +# +# A tile range generated from `R` is a iterator whose elements are also an array of scalar indices. +# It's an indices-specific block array. +# +# Reference: https://docs.julialang.org/en/v1.5/manual/arrays/#man-supported-index-types +# +# +# | scalar index | indices(tile) | +# |-------------------|----------------- | +# | range/vector | AbstractTileRange | +# | CartesianIndices | TileIndices | +# +# AbstractTileStrategy serves as an adapter for `AbstractTileRange` and `TileIndices` to give a more +# flexible API specification. + + +abstract type AbstractTileRange{R} <: AbstractArray{R, 1} end +abstract type AbstractTileStrategy end + +const Range1 = Union{OrdinalRange{<:Integer}, AbstractVector{<:Integer}, CartesianIndices{1}} + +### FixedTileRange and FixedTile + +""" + FixedTileRange(r, n, [Δ=n]; keep_last=true) + +Construct a sliding tile along range `r` with fixed sliding stride `Δ` and tile length `n`. + +# Arguments + +- `r`: a range, `CartesianIndices` or `Vector` +- `n::Integer`: The length of each tile. If keyword `keep_last=true`, the last tile length might be + less than `n`. +- `Δ::Union{Integer, CartesianIndex{1}}=n`: The sliding stride `Δ` is defined as `first(r[n]) - + first(r[n-1])`. Using a stride `Δ FixedTileRange(2:10, 3) +3-element FixedTileRange{UnitRange{Int64},Int64,UnitRange{Int64}}: + 2:4 + 5:7 + 8:10 + +julia> FixedTileRange(1:10, 4) +3-element FixedTileRange{UnitRange{Int64},Int64,UnitRange{Int64}}: + 1:4 + 5:8 + 9:10 + +julia> FixedTileRange(1:10, 4, 2) +4-element FixedTileRange{UnitRange{Int64},Int64,UnitRange{Int64}}: + 1:4 + 3:6 + 5:8 + 7:10 + +julia> FixedTileRange(1:10, 4; keep_last=false) +2-element FixedTileRange{UnitRange{Int64},Int64,UnitRange{Int64}}: + 1:4 + 5:8 + +julia> FixedTileRange(1:2:10, 4) +2-element FixedTileRange{StepRange{Int64,Int64},Int64,StepRange{Int64,Int64}}: + 1:2:7 + 5:2:9 +``` + +Besides an `AbstractUnitRange`, the input range `r` can also be a `CartesianIndices{1}` or more +generally, an `AbstractVector{<:Integer}`: + +```jldoctest; setup=:(using TiledIteration) +julia> FixedTileRange(CartesianIndices((1:10, )), 4) +3-element FixedTileRange{CartesianIndices{1,Tuple{UnitRange{Int64}}},Int64,CartesianIndices{1,Tuple{UnitRange{Int64}}}}: + [CartesianIndex(1,), CartesianIndex(2,), CartesianIndex(3,), CartesianIndex(4,)] + [CartesianIndex(5,), CartesianIndex(6,), CartesianIndex(7,), CartesianIndex(8,)] + [CartesianIndex(9,), CartesianIndex(10,)] +``` + +!!! warning + It usually has bad indexing performance if `r` is not lazily evaluated. For example, + `FixedTileRange(collect(1:10), 4)` creates a new `Vector` of length `4` everytime when + `getindex` is called. +""" +struct FixedTileRange{R, T, RP} <: AbstractTileRange{R} + parent::RP + n::T + Δ::T + keep_last::Bool + + # keep `length` information to avoid unnecessary calculation and thus is more performant + length::T + + function FixedTileRange(parent::R, n::T, Δ; keep_last::Bool=true) where {R<:Range1, T} + _length = _fixedtile_length(parent, n, Δ, keep_last) + new{_eltype(R), T, R}(parent, n, Δ, keep_last, _length) + end +end +FixedTileRange(r::Range1, n::Integer; kwargs...) = FixedTileRange(r, n, n; kwargs...) + +_eltype(::Type{R}) where R<:OrdinalRange = StepRange{eltype(R), eltype(R)} +_eltype(::Type{R}) where R<:AbstractUnitRange = UnitRange{eltype(R)} +_eltype(::Type{R}) where R<:AbstractVector = R # this includes CartesianIndices{1} + +_int(x::Integer) = x +_int(x::CartesianIndex{1}) = first(x.I) + +function _fixedtile_length(r::OrdinalRange{T}, n, Δ, keep_last) where T<:Integer + _round = keep_last ? ceil : floor + start, step, stop = first(r), Base.step(r), last(r) + return _round(T, (stop - step*n - start + 1)/_int(Δ)) + 1 +end +function _fixedtile_length(r::CartesianIndices{1}, n, Δ, keep_last) + _fixedtile_length(r.indices[1], n, Δ, keep_last) +end +function _fixedtile_length(r::AbstractVector{T}, n, Δ, keep_last) where T<:Integer + _fixedtile_length(UnitRange{T}(first(r), last(r)), n, Δ, keep_last) +end + +Base.size(r::FixedTileRange) = (r.length, ) +Base.length(r::FixedTileRange) = r.length + +Base.@propagate_inbounds function Base.getindex(r::FixedTileRange{R, T}, i::Int) where {R, T} + convert(R, _getindex(r.parent, r.n, r.Δ, i)) +end +Base.@propagate_inbounds function Base.getindex(r::FixedTileRange{R, T}, i::Int) where {R<:CartesianIndices, T} + # inter-operation between CartesianIndex and Integer is not very well defined + # for this reason, we deconstruct CartesianIndex into range, index, and reconstruct it. + R((_getindex(r.parent.indices[1], r.n, r.Δ, i), )) +end + +Base.@propagate_inbounds function _getindex(r::R, n::T, Δ::T, i::Int) where {R, T} + @boundscheck checkbounds(r, i) + start = first(r) + (i-1)*Δ + stop = start + n - _oneunit(eltype(R)) + return start:min(stop, last(r)) +end +Base.@propagate_inbounds function _getindex(r::R, n::T, Δ::T, i::Int) where {R<:StepRange, T} + @boundscheck checkbounds(r, i) + start = first(r) + (i-1)*Δ + stop = start + step(r)*n - _oneunit(eltype(R)) + return start:step(r):min(stop, last(r)) +end + + +""" + FixedTile(sz, Δ; keep_last=true) + +A tile strategy that you can use to construct [`TileIndices`](@ref) using [`FixedTileRange`](@ref). + +# Arguments + +- `sz`: The size of each tile. If keyword `keep_last=true`, the last tile size might be smaller than + `sz`. +- `Δ=sz`: For each dimension `i` and `r = indices[i]`, the sliding stride `Δ[i]` is defined as + `first(r[n]) - first(r[n-1])`. Using a stride `Δ[i] < sz[i]` means there are overlaps between each + adjacent tile along this dimension. +- `keep_last=true` (keyword): this keyword affects the cases when the last tile size is smaller + than `sz`, in which case, `true`/`false` tells `TileIndices` to keep/discard the last tile. + +# Examples + +```jldoctest; setup=:(using TiledIteration) +julia> TileIndices((1:4, 0:5), FixedTile((3, 4), (2, 3))) +2×2 TileIndices{Tuple{UnitRange{Int64},UnitRange{Int64}},2,FixedTileRange{UnitRange{Int64},Int64,UnitRange{Int64}}}: + (1:3, 0:3) (1:3, 3:5) + (3:4, 0:3) (3:4, 3:5) + +julia> TileIndices((1:4, 0:5), FixedTile((3, 4), (2, 3); keep_last=false)) +1×1 TileIndices{Tuple{UnitRange{Int64},UnitRange{Int64}},2,FixedTileRange{UnitRange{Int64},Int64,UnitRange{Int64}}}: + (1:3, 0:3) + +julia> TileIndices((1:2:10, 0:1:5), FixedTile((3, 4), (2, 3))) +3×2 TileIndices{Tuple{StepRange{Int64,Int64},StepRange{Int64,Int64}},2,FixedTileRange{StepRange{Int64,Int64},Int64,StepRange{Int64,Int64}}}: + (1:2:5, 0:1:3) (1:2:5, 3:1:5) + (3:2:7, 0:1:3) (3:2:7, 3:1:5) + (5:2:9, 0:1:3) (5:2:9, 3:1:5) +``` + +When `sz` and `Δ` are scalars, it affects each dimension equivalently. + +```jldoctest; setup=:(using TiledIteration) +julia> TileIndices((1:4, 0:5), FixedTile(3, 2)) +2×3 TileIndices{Tuple{UnitRange{Int64},UnitRange{Int64}},2,FixedTileRange{UnitRange{Int64},Int64,UnitRange{Int64}}}: + (1:3, 0:2) (1:3, 2:4) (1:3, 4:5) + (3:4, 0:2) (3:4, 2:4) (3:4, 4:5) + +julia> TileIndices((1:4, 0:5), FixedTile(3, 2; keep_last=false)) +1×2 TileIndices{Tuple{UnitRange{Int64},UnitRange{Int64}},2,FixedTileRange{UnitRange{Int64},Int64,UnitRange{Int64}}}: + (1:3, 0:2) (1:3, 2:4) +``` +""" +struct FixedTile{N, T} <: AbstractTileStrategy + size::T + Δ::T + keep_last::Bool +end + +FixedTile(sz::T, Δ=sz; keep_last=true) where T<:Integer = FixedTile{0, T}(sz, Δ, keep_last) +FixedTile(sz::T, Δ=sz; keep_last=true) where T = FixedTile{length(sz), T}(sz, Δ, keep_last) + +(S::FixedTile{0})(r::Range1) = FixedTileRange(r, S.size, S.Δ; keep_last=S.keep_last) +(S::FixedTile{0})(r::CartesianIndices{1}) = FixedTileRange(r, S.size, S.Δ; keep_last=S.keep_last) +(S::FixedTile{0})(indices) = map(r->FixedTileRange(r, S.size, S.Δ; keep_last=S.keep_last), indices) + +(S::FixedTile{N})(indices) where N = + map((args...)->FixedTileRange(args...; keep_last=S.keep_last), indices, S.size, S.Δ) +# ambiguity patch +(S::FixedTile{0})(indices::CartesianIndices{N}) where N = + S(map(x->CartesianIndices((x, )), indices.indices)) +(S::FixedTile{N})(indices::CartesianIndices{N}) where N = + S(map(x->CartesianIndices((x, )), indices.indices)) diff --git a/test/compat.jl b/test/compat.jl new file mode 100644 index 0000000..e3587c1 --- /dev/null +++ b/test/compat.jl @@ -0,0 +1,5 @@ +# https://github.com/JuliaLang/julia/pull/29440 +if VERSION < v"1.1.0-DEV.389" + Base.:(:)(I::CartesianIndex{N}, J::CartesianIndex{N}) where N = + CartesianIndices(map((i,j) -> i:j, Tuple(I), Tuple(J))) +end diff --git a/test/runtests.jl b/test/runtests.jl index 6d7701c..a406740 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,7 +3,7 @@ using Test using Documenter using OffsetArrays: IdentityUnitRange -if VERSION < v"1.6-" +if v"1.1" <= VERSION < v"1.6-" Documenter.doctest(TiledIteration) # Version restriction can be lifted, when # filters can be passed to `doctest` @@ -17,6 +17,9 @@ if VERSION < v"1.6-" # Documenter.doctest(TiledIteration, doctestfilters = doctestfilters) end +include("compat.jl") +include("tilerange.jl") +include("tileindices.jl") @testset "TileIterator small examples" begin titr = @inferred TileIterator((1:10,), RelaxLastTile((3,))) diff --git a/test/tileindices.jl b/test/tileindices.jl new file mode 100644 index 0000000..104a598 --- /dev/null +++ b/test/tileindices.jl @@ -0,0 +1,164 @@ +@testset "TileIndices" begin + function test_iteration(R) + for (i, v) in enumerate(R) + @test v == R[i] + end + end + + @testset "FixedTile" begin + @testset "axes" begin + R0 = (1:4, 0:5) + + R = @inferred TileIndices(R0, FixedTile((3, 4), (2, 3))) + @test R == TileIndices(R0, (3, 4), (2, 3)) + @test R isa AbstractArray + @test eltype(R) == Tuple{UnitRange{Int}, UnitRange{Int}} + @test size(R) == (2, 2) + @test R[1] == (1:3, 0:3) + @test R[end] == (3:4, 3:5) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile((3, 4), (2, 3), keep_last=false)) + @test R == TileIndices(R0, (3, 4), (2, 3), keep_last=false) + @test R isa AbstractArray + @test eltype(R) == Tuple{UnitRange{Int}, UnitRange{Int}} + @test size(R) == (1, 1) + @test R[1] == (1:3, 0:3) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile(3, 2)) + @test R == TileIndices(R0, 3, 2) + @test R isa AbstractArray + @test eltype(R) == Tuple{UnitRange{Int}, UnitRange{Int}} + @test size(R) == (2, 3) + @test R[1] == R[1, 1] == (1:3, 0:2) + @test R[3] == R[1, 2] == (1:3, 2:4) + @test R[end] == R[6] == (3:4, 4:5) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile(3, 2, keep_last=false)) + @test R == TileIndices(R0, 3, 2, keep_last=false) + @test R isa AbstractArray + @test eltype(R) == Tuple{UnitRange{Int}, UnitRange{Int}} + @test size(R) == (1, 2) + @test R[1] == R[1, 1] == (1:3, 0:2) + @test R[2] == R[1, 2] == R[end] == (1:3, 2:4) + + # StepRange + R0 = (1:2:8, 0:3:20) + + R = @inferred TileIndices(R0, FixedTile((3, 4), (2, 3))) + @test R == TileIndices(R0, (3, 4), (2, 3)) + @test R isa AbstractArray + @test eltype(R) == Tuple{StepRange{Int, Int}, StepRange{Int, Int}} + @test size(R) == (2, 4) + @test R[1] == (1:2:5, 0:3:9) + @test R[end] == (3:2:7, 9:3:18) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile((3, 4), (2, 3), keep_last=false)) + @test R == TileIndices(R0, (3, 4), (2, 3), keep_last=false) + @test R isa AbstractArray + @test eltype(R) == Tuple{StepRange{Int, Int}, StepRange{Int, Int}} + @test size(R) == (1, 3) + @test R[1] == (1:2:5, 0:3:9) + @test R[end] == (1:2:5, 6:3:15) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile(3, 2)) + @test R == TileIndices(R0, 3, 2) + @test R isa AbstractArray + @test eltype(R) == Tuple{StepRange{Int, Int}, StepRange{Int, Int}} + @test size(R) == (2, 6) + @test R[1] == R[1, 1] == (1:2:5, 0:3:6) + @test R[3] == R[1, 2] == (1:2:5, 2:3:8) + @test R[end] == R[12] == (3:2:7, 10:3:16) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile(3, 2, keep_last=false)) + @test R == TileIndices(R0, 3, 2, keep_last=false) + @test R isa AbstractArray + @test eltype(R) == Tuple{StepRange{Int, Int}, StepRange{Int, Int}} + @test size(R) == (1, 6) + @test R[1] == R[1, 1] == (1:2:5, 0:3:6) + @test R[6] == R[1, 6] == R[end] == (1:2:5, 10:3:16) + end + + @testset "CartesianIndices" begin + R0 = CartesianIndex(1,0):CartesianIndex(4, 5) + + R = @inferred TileIndices(R0, FixedTile((3, 4), (2, 3))) + @test R == TileIndices(R0, (3, 4), (2, 3)) + @test R isa AbstractArray + @test eltype(R) <: Tuple{<:CartesianIndices, <:CartesianIndices} + @test size(R) == (2, 2) + @test R[1] == CartesianIndices((1:3, 0:3)) + @test R[end] == CartesianIndices((3:4, 3:5)) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile((3, 4), (2, 3), keep_last=false)) + @test R == TileIndices(R0, (3, 4), (2, 3), keep_last=false) + @test R isa AbstractArray + @test eltype(R) <: Tuple{<:CartesianIndices, <:CartesianIndices} + @test size(R) == (1, 1) + @test R[1] == CartesianIndices((1:3, 0:3)) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile(3, 2)) + @test R == TileIndices(R0, 3, 2) + @test R isa AbstractArray + @test eltype(R) <: Tuple{<:CartesianIndices, <:CartesianIndices} + @test size(R) == (2, 3) + @test R[1] == R[1, 1] == CartesianIndices((1:3, 0:2)) + @test R[3] == R[1, 2] == CartesianIndices((1:3, 2:4)) + @test R[end] == R[6] == CartesianIndices((3:4, 4:5)) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile(3, 2, keep_last=false)) + @test R == TileIndices(R0, 3, 2, keep_last=false) + @test R isa AbstractArray + @test eltype(R) <: Tuple{<:CartesianIndices, <:CartesianIndices} + @test size(R) == (1, 2) + @test R[1] == R[1, 1] == CartesianIndices((1:3, 0:2)) + @test R[2] == R[1, 2] == R[end] == CartesianIndices((1:3, 2:4)) + + if VERSION >= v"1.6.0-DEV.1174" + # StepRange CartesianIndices + # https://github.com/JuliaLang/julia/pull/37829 + R0 = CartesianIndices((1:2:8, 0:3:20)) + + R = @inferred TileIndices(R0, FixedTile((3, 4), (2, 3))) + @test R == TileIndices(R0, (3, 4), (2, 3)) + @test R isa AbstractArray + @test size(R) == (2, 4) + @test R[1] == CartesianIndices((1:2:5, 0:3:9)) + @test R[end] == CartesianIndices((3:2:7, 9:3:18)) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile((3, 4), (2, 3), keep_last=false)) + @test R == TileIndices(R0, (3, 4), (2, 3), keep_last=false) + @test R isa AbstractArray + @test size(R) == (1, 3) + @test R[1] == CartesianIndices((1:2:5, 0:3:9)) + @test R[end] == CartesianIndices((1:2:5, 6:3:15)) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile(3, 2)) + @test R == TileIndices(R0, 3, 2) + @test R isa AbstractArray + @test size(R) == (2, 6) + @test R[1] == R[1, 1] == CartesianIndices((1:2:5, 0:3:6)) + @test R[3] == R[1, 2] == CartesianIndices((1:2:5, 2:3:8)) + @test R[end] == R[12] == CartesianIndices((3:2:7, 10:3:16)) + test_iteration(R) + + R = @inferred TileIndices(R0, FixedTile(3, 2, keep_last=false)) + @test R == TileIndices(R0, 3, 2, keep_last=false) + @test R isa AbstractArray + @test size(R) == (1, 6) + @test R[1] == R[1, 1] == CartesianIndices((1:2:5, 0:3:6)) + @test R[6] == R[1, 6] == R[end] == CartesianIndices((1:2:5, 10:3:16)) + end + end + end +end diff --git a/test/tilerange.jl b/test/tilerange.jl new file mode 100644 index 0000000..ce3a207 --- /dev/null +++ b/test/tilerange.jl @@ -0,0 +1,305 @@ +@testset "Fixed Tile" begin + ranges = [ + 2:10, + UInt8(2):UInt8(10), + OffsetArrays.IdentityUnitRange(2:10), # Julia 1.0 compat: Base.IdentityUnitRange was Base.Slice + OffsetArrays.IdOffsetRange(2:10), + OffsetArrays.IdOffsetRange(Base.OneTo(9), 1) + ] + + @testset "FixedTileRange" begin + function test_iteration(r) + for (i, v) in enumerate(r) + @test v == r[i] + end + @test collect(r) == r + + rst = zero(first(r.parent)) + for v in r + rst += sum(v) + end + @test mapreduce(sum, +, r) == rst + end + + @testset "ranges" begin + for r0 in ranges + # keep the last tile + r = @inferred FixedTileRange(r0, 4) + @test r == FixedTileRange(r0, 4; keep_last=true) == FixedTileRange(r0, 4, 4) + @test r isa AbstractArray + @test eltype(r) == UnitRange{eltype(r0)} + @test length(r) == 3 + @test first(r) == 2:5 + @test last(r) == 10:10 + test_iteration(r) + @test all(map(x->length(x)==4, r[1:end-1])) + @test length(r[end]) <= 4 + + # different stride + r = @inferred FixedTileRange(r0, 4, 2) + @test r == FixedTileRange(r0, 4, 2; keep_last=true) + @test r isa AbstractArray + @test eltype(r) == UnitRange{eltype(r0)} + @test length(r) == 4 + @test first(r) == 2:5 + @test last(r) == 8:10 + test_iteration(r) + @test all(map(x->length(x)==4, r[1:end-1])) + @test length(r[end]) <= 4 + + # discard the last tile + r = @inferred FixedTileRange(r0, 4, keep_last=false) + @test r isa AbstractArray + @test eltype(r) == UnitRange{eltype(r0)} + @test length(r) == 2 + @test first(r) == 2:5 + @test last(r) == 6:9 + test_iteration(r) + @test all(map(x->length(x)==4, r)) + end + + @testset "StepRange" begin + r0 = 2:3:20 + + # keep the last tile + r = @inferred FixedTileRange(r0, 4) + @test r == FixedTileRange(r0, 4; keep_last=true) == FixedTileRange(r0, 4, 4) + @test r isa AbstractArray + @test eltype(r) == StepRange{eltype(r0), eltype(r0)} + @test length(r) == 3 + @test first(r) == 2:3:11 + @test last(r) == 10:3:19 + test_iteration(r) + @test all(map(x->length(x)==4, r[1:end-1])) + @test length(r[end]) <= 4 + + # different stride + r = @inferred FixedTileRange(r0, 4, 2) + @test r == FixedTileRange(r0, 4, 2; keep_last=true) + @test r isa AbstractArray + @test eltype(r) == StepRange{eltype(r0), eltype(r0)} + @test length(r) == 5 + @test first(r) == 2:3:11 + @test last(r) == 10:3:19 + test_iteration(r) + @test all(map(x->length(x)==4, r[1:end-1])) + @test length(r[end]) <= 4 + + # discard the last tile + r = @inferred FixedTileRange(r0, 4; keep_last=false) + @test r isa AbstractArray + @test eltype(r) == StepRange{eltype(r0), eltype(r0)} + @test length(r) == 2 + @test first(r) == 2:3:11 + @test last(r) == 6:3:15 + test_iteration(r) + @test all(map(x->length(x)==4, r)) + end + + # FixedTileRange only works for 1d case + @test_throws MethodError FixedTileRange((2:10, 2:10), (4, 4), (2, 2)) + end + + @testset "CartesianIndices" begin + r0 = CartesianIndex(2):CartesianIndex(10) + + # keep the last tile + r = @inferred FixedTileRange(r0, 4) + @test r == FixedTileRange(r0, 4; keep_last=true) == FixedTileRange(r0, 4, 4) + @test r isa AbstractArray + @test eltype(r) == typeof(r0) + @test length(r) == 3 + @test first(r) == CartesianIndex(2):CartesianIndex(5) + @test last(r) == CartesianIndex(10):CartesianIndex(10) + test_iteration(r) + @test all(map(x->length(x)==4, r[1:end-1])) + @test length(r[end]) <= 4 + + # `Δ` can also be `CartesianIndex` when `r` is a `CartesianIndices` + @test FixedTileRange(r0, 4, 2) == FixedTileRange(r0, 4, CartesianIndex(2)) + + # different stride + r = @inferred FixedTileRange(r0, 4, 2) + @test r == FixedTileRange(r0, 4, 2; keep_last=true) + @test r isa AbstractArray + @test eltype(r) == typeof(r0) + @test length(r) == 4 + @test first(r) == CartesianIndex(2):CartesianIndex(5) + @test last(r) == CartesianIndex(8):CartesianIndex(10) + test_iteration(r) + @test all(map(x->length(x)==4, r[1:end-1])) + @test length(r[end]) <= 4 + + # discard the last tile + r = @inferred FixedTileRange(r0, 4, keep_last=false) + @test r isa AbstractArray + @test eltype(r) == typeof(r0) + @test length(r) == 2 + @test first(r) == CartesianIndex(2):CartesianIndex(5) + @test last(r) == CartesianIndex(6):CartesianIndex(9) + # test_iteration(r) + @test all(map(x->length(x)==4, r)) + + @testset "StepRange" begin + if VERSION >= v"1.6.0-DEV.1174" + # StepRange CartesianIndices + # https://github.com/JuliaLang/julia/pull/37829 + r0 = CartesianIndex(2):CartesianIndex(3):CartesianIndex(20) + + # keep the last tile + r = @inferred FixedTileRange(r0, 4); + @test r == FixedTileRange(r0, 4; keep_last=true) == FixedTileRange(r0, 4, 4) + @test r isa AbstractArray + @test eltype(r) == CartesianIndices{1, Tuple{StepRange{Int64, Int64}}} + @test length(r) == 3 + @test first(r) == CartesianIndex(2):CartesianIndex(3):CartesianIndex(11) + @test last(r) == CartesianIndex(10):CartesianIndex(3):CartesianIndex(19) + test_iteration(r) + @test all(map(x->length(x)==4, r[1:end-1])) + @test length(r[end]) <= 4 + + # different stride + r = @inferred FixedTileRange(r0, 4, 2); + @test r == FixedTileRange(r0, 4, 2; keep_last=true) + @test r isa AbstractArray + @test eltype(r) == CartesianIndices{1, Tuple{StepRange{Int64, Int64}}} + @test length(r) == 5 + @test first(r) == CartesianIndex(2):CartesianIndex(3):CartesianIndex(11) + @test last(r) == CartesianIndex(10):CartesianIndex(3):CartesianIndex(19) + test_iteration(r) + @test all(map(x->length(x)==4, r[1:end-1])) + @test length(r[end]) <= 4 + + # discard the last tile + r = @inferred FixedTileRange(r0, 4; keep_last=false); + @test r isa AbstractArray + @test eltype(r) == CartesianIndices{1, Tuple{StepRange{Int64, Int64}}} + @test length(r) == 2 + @test first(r) == CartesianIndex(2):CartesianIndex(3):CartesianIndex(11) + @test last(r) == CartesianIndex(6):CartesianIndex(3):CartesianIndex(15) + test_iteration(r) + @test all(map(x->length(x)==4, r)) + end + end + end + + @testset "AbstractVector" begin + # AbstractVector can be terrible for performance if it allocates memory. + + for r0 in [ + collect(2:10), + collect(UInt8(2):UInt8(10)), + # Broken: https://github.com/JuliaArrays/OffsetArrays.jl/issues/171 + # OffsetArray(collect(2:10), -1) + ] + # keep the last tile + r = @inferred FixedTileRange(r0, 4) + @test r == FixedTileRange(r0, 4; keep_last=true) == FixedTileRange(r0, 4, 4) + @test r isa AbstractArray + @test eltype(r) == Vector{eltype(r0)} + @test length(r) == 3 + @test first(r) == collect(2:5) + @test last(r) == collect(10:10) + test_iteration(r) + @test all(map(x->length(x)==4, r[1:end-1])) + @test length(r[end]) <= 4 + + # different stride + r = @inferred FixedTileRange(r0, 4, 2) + @test r == FixedTileRange(r0, 4, 2; keep_last=true) + @test r isa AbstractArray + @test eltype(r) == Vector{eltype(r0)} + @test length(r) == 4 + @test first(r) == collect(2:5) + @test last(r) == collect(8:10) + test_iteration(r) + @test all(map(x->length(x)==4, r[1:end-1])) + @test length(r[end]) <= 4 + + # discard the last tile + r = @inferred FixedTileRange(r0, 4, keep_last=false) + @test r isa AbstractArray + @test eltype(r) == Vector{eltype(r0)} + @test length(r) == 2 + @test first(r) == collect(2:5) + @test last(r) == collect(6:9) + test_iteration(r) + @test all(map(x->length(x)==4, r)) + end + end + + end # FixedTileRange + + @testset "FixedTile" begin + # scalar case + @testset "scalar case" begin + s = FixedTile(4, 2) + @test s == FixedTile(4, 2; keep_last=true) + @test s == FixedTile(4, CartesianIndex(2)) + + for r0 in ranges + s = FixedTile(4, 2) + R = @inferred s(r0) + @test R == FixedTileRange(r0, 4, 2) + + r1 = CartesianIndices((r0, )) + @test s(r1) == FixedTileRange(r1, 4, 2) + end + + # StepRange + r0 = 2:3:20 + s = FixedTile(4, 2) + R = @inferred s(r0) + @test R == FixedTileRange(r0, 4, 2) + if VERSION >= v"1.6.0-DEV.1174" + # StepRange CartesianIndices + # https://github.com/JuliaLang/julia/pull/37829 + r1 = CartesianIndices((r0, )) + R = @inferred s(r1); + @test R == FixedTileRange(r1, 4, 2) + end + end + + # nd case + for n in 2:3 + for (sz, Δ) in [ + (4, 2), + (ntuple(_->4, n), ntuple(_->2, n)), + (fill(4, n), fill(2, n)), + (ntuple(_->4, n), ntuple(_->CartesianIndex(2), n)) + ] + for r0 in ranges + s = FixedTile(sz, Δ) + indices = ntuple(_->r0, n) + R = @inferred s(indices) + @test eltype(R) <: FixedTileRange{<:UnitRange} + @test all(map(==, R, ntuple(_->FixedTileRange(r0, 4, 2), n))) + + indices = CartesianIndices(indices) + r1 = CartesianIndices((r0, )) + R = @inferred s(indices) + @test eltype(R) <: FixedTileRange{<:CartesianIndices} + @test all(map(==, R, ntuple(_->FixedTileRange(r1, 4, 2), n))) + end + + # StepRange + r0 = 2:3:20 + s = FixedTile(sz, Δ) + indices = ntuple(_->r0, n) + R = @inferred s(indices) + @test eltype(R) <: FixedTileRange{<:StepRange} + @test all(map(==, R, ntuple(_->FixedTileRange(r0, 4, 2), n))) + + if VERSION >= v"1.6.0-DEV.1174" + # StepRange CartesianIndices + # https://github.com/JuliaLang/julia/pull/37829 + indices = CartesianIndices(indices) + r1 = CartesianIndices((r0, )) + R = @inferred s(indices) + @test eltype(R) <: FixedTileRange{<:CartesianIndices} + @test all(map(==, R, ntuple(_->FixedTileRange(r1, 4, 2), n))) + end + end + end + end # FixedTile +end