From cf10d1f1c3287adc12a219286f81758506192460 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Thu, 2 Jul 2026 23:25:16 +0100 Subject: [PATCH 1/9] Add BlockedUnitRangeFirsts/Lasts to avoid allocating --- Project.toml | 2 +- src/blockaxis.jl | 74 ++++++++++++++++++++--------------------- src/blocks.jl | 2 +- test/test_blockrange.jl | 5 +++ 4 files changed, 44 insertions(+), 39 deletions(-) diff --git a/Project.toml b/Project.toml index dddf335b..e84383e5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "BlockArrays" uuid = "8e7c35d0-a365-5155-bbbb-fb81a777f24e" -version = "1.9.5" +version = "1.10" [deps] diff --git a/src/blockaxis.jl b/src/blockaxis.jl index 94c4d601..6ab27ec1 100644 --- a/src/blockaxis.jl +++ b/src/blockaxis.jl @@ -17,8 +17,8 @@ # handles plain ranges where block(K) is always Block(1). @propagate_inbounds getindex(b::AbstractUnitRange{<:Integer}, K::BlockIndices{1}) = b[block(K)][K.indices...] -function findblockindex(b::AbstractVector, k::Integer) - @boundscheck k in b || throw(BoundsError()) +@propagate_inbounds function findblockindex(b::AbstractVector, k::Integer) + @boundscheck k in b || throw(BoundsError(b, k)) bl = blocklasts(b) blockidx = _searchsortedfirst(bl, k) @assert blockindex != lastindex(bl) + 1 # guaranteed by the @boundscheck above @@ -94,19 +94,6 @@ BlockedUnitRange(::BlockedUnitRange) = throw(ArgumentError("Forbidden due to amb # Use `accumulate` instead of `cumsum` because it preserves the element type of the block lengths _blocklengths2blocklasts(blocks) = accumulate(+, blocks) # extra level to allow changing default accumulate behaviour -@inline blockfirsts(a::AbstractBlockedUnitRange) = [first(a); @views(blocklasts(a)[1:end-1]) .+ oneunit(eltype(a))] - -# optimize common cases -@inline function blockfirsts(a::AbstractBlockedUnitRange{<:Any,<:Union{Vector, RangeCumsum{<:Any, <:UnitRange}}}) - v = Vector{eltype(a)}(undef, length(blocklasts(a))) - v[1] = first(a) - v[2:end] .= @views(blocklasts(a)[oneto(end-1)]) .+ oneunit(eltype(a)) - return v -end -@inline function blockfirsts(a::AbstractBlockedUnitRange{<:Any,<:Tuple}) - return (first(a), (blocklasts(a)[oneto(end-1)] .+ oneunit(eltype(a)))...) -end - function Base.AbstractUnitRange{T}(r::BlockedUnitRange) where {T} return _BlockedUnitRange(convert(T,first(r)), convert.(T,blocklasts(r))) end @@ -218,15 +205,35 @@ julia> blockedrange(2, (1,2)) @inline blockedrange(blocks::Union{Tuple,AbstractVector}) = BlockedOneTo(_blocklengths2blocklasts(blocks)) @inline blockedrange(f::Integer, blocks::Union{Tuple,AbstractVector}) = _BlockedUnitRange(f, f-oneunit(f) .+ _blocklengths2blocklasts(blocks)) -_diff(a::AbstractVector) = diff(a) -_diff(a::Tuple) = diff(collect(a)) -@inline _blocklengths(a, bl, dbl) = isempty(bl) ? [dbl;] : [first(bl)-first(a)+oneunit(eltype(a)); dbl] -@inline function _blocklengths(a::BlockedOneTo, bl::RangeCumsum, ::OrdinalRange) - # the 1:0 is hardcoded here to enable conversions to a Base.OneTo - isempty(bl) ? oftype(bl.range, 1:0) : bl.range + +struct BlockedUnitRangeLengths{T<:Integer, LASTS} <: AbstractVector{T} + offset::T + lasts::LASTS +end + +struct BlockedUnitRangeFirsts{T<:Integer, LASTS} <: AbstractVector{T} + first::T + lasts::LASTS +end + +size(b::Union{BlockedUnitRangeLengths,BlockedUnitRangeFirsts}) = (length(b.lasts),) + +@propagate_inbounds function getindex(b::BlockedUnitRangeLengths, k::Integer) + if isone(k) + first(b.lasts) - b.offset + else + b.lasts[k] - b.lasts[k-1] + end end -@inline _blocklengths(a, bl) = _blocklengths(a, bl, _diff(bl)) -@inline blocklengths(a::AbstractBlockedUnitRange) = _blocklengths(a, blocklasts(a)) + +@propagate_inbounds function getindex(b::BlockedUnitRangeFirsts{T}, k::Integer) where T + if isone(k) + b.first + else + b.lasts[k-1] + one(T) + end +end + length(a::AbstractBlockedUnitRange) = isempty(blocklasts(a)) ? zero(eltype(a)) : Integer(last(blocklasts(a))-first(a)+oneunit(eltype(a))) @@ -624,7 +631,10 @@ julia> blockfirsts(b) 4 ``` """ -blockfirsts(a::AbstractUnitRange{<:Integer}) = Ones{eltype(a)}(1) +@inline blockfirsts(a::AbstractUnitRange{<:Integer}) = Fill(first(a), 1) + +@inline blockfirsts(a::AbstractBlockedUnitRange{<:Integer}) = BlockedUnitRangeFirsts(first(a), blocklasts(a)) + """ blocklasts(a::AbstractUnitRange{<:Integer}) @@ -651,6 +661,7 @@ julia> blocklasts(b) ``` """ blocklasts(a::AbstractUnitRange{<:Integer}) = Fill(eltype(a)(length(a)),1) + """ blocklengths(a::AbstractUnitRange{<:Integer}) @@ -676,7 +687,8 @@ julia> blocklengths(b) 3 ``` """ -blocklengths(a::AbstractUnitRange{<:Integer}) = blocklasts(a) .- blockfirsts(a) .+ oneunit(eltype(a)) +@inline blocklengths(a::AbstractUnitRange{<:Integer}) = blocklasts(a) +@inline blocklengths(a::AbstractBlockedUnitRange{<:Integer}) = BlockedUnitRangeLengths(first(a)-1, blocklasts(a)) Base.summary(io::IO, a::AbstractBlockedUnitRange) = _block_summary(io, a) @@ -719,18 +731,6 @@ function blocklengths(a::AbstractBlockedUnitRange{<:Any,<:Base.OneTo{<:Integer}} first(a) == 1 || error("Offset axes not supported") Ones{eltype(a)}(length(blocklasts(a))) end -function blockfirsts(a::AbstractBlockedUnitRange{<:Any,<:AbstractRange}) - st = step(blocklasts(a)) - first(a) == 1 || error("Offset axes not supported") - @assert first(blocklasts(a))-first(a)+oneunit(eltype(a)) == st - range(oneunit(eltype(a)); step=st, length=eltype(a)(length(blocklasts(a)))) -end -function blocklengths(a::AbstractBlockedUnitRange{<:Any,<:AbstractRange}) - st = step(blocklasts(a)) - first(a) == 1 || error("Offset axes not supported") - @assert first(blocklasts(a))-first(a)+oneunit(eltype(a)) == st - Fill(st,length(blocklasts(a))) -end # TODO: Remove diff --git a/src/blocks.jl b/src/blocks.jl index ee23f227..b3da9361 100644 --- a/src/blocks.jl +++ b/src/blocks.jl @@ -186,7 +186,7 @@ julia> blocklengths(A)[1,2] ``` """ blocklengths(A::AbstractArray) = BlockLengths(A) -blocklengths(A::AbstractVector) = map(length, blocks(A)) +blocklengths(A::AbstractVector) = blocklengths(axes(A,1)) struct BlockLengths{T,N,A<:AbstractArray{<:Any,N}} <: AbstractArray{T,N} array::A diff --git a/test/test_blockrange.jl b/test/test_blockrange.jl index ffb2ba2b..15ec0c80 100644 --- a/test/test_blockrange.jl +++ b/test/test_blockrange.jl @@ -126,6 +126,11 @@ end @test bi == collect(bi) @test size(bi) == (2,2) end + + @testset "BlockedOneTo(::AbstractRange)" begin + @test blocklengths(BlockedOneTo(2:5)) == [2,1,1,1] + @test blockfirsts(BlockedOneTo(2:5)) == [1,3,4,5] + end end end # module From e1455e3fb6bc7abcf598bd640a7d90a84ef6ce11 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Thu, 2 Jul 2026 23:46:54 +0100 Subject: [PATCH 2/9] Support general indexing and tuples --- src/blockaxis.jl | 33 +++++++++++++++++++++------------ test/test_blockindices.jl | 36 ++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/blockaxis.jl b/src/blockaxis.jl index 6ab27ec1..53660243 100644 --- a/src/blockaxis.jl +++ b/src/blockaxis.jl @@ -206,17 +206,17 @@ julia> blockedrange(2, (1,2)) @inline blockedrange(f::Integer, blocks::Union{Tuple,AbstractVector}) = _BlockedUnitRange(f, f-oneunit(f) .+ _blocklengths2blocklasts(blocks)) -struct BlockedUnitRangeLengths{T<:Integer, LASTS} <: AbstractVector{T} +struct BlockedUnitRangeLengths{T<:Integer, LASTS<:AbstractVector{T}} <: AbstractVector{T} offset::T lasts::LASTS end -struct BlockedUnitRangeFirsts{T<:Integer, LASTS} <: AbstractVector{T} +struct BlockedUnitRangeFirsts{T<:Integer, LASTS<:AbstractVector{T}} <: AbstractVector{T} first::T lasts::LASTS end -size(b::Union{BlockedUnitRangeLengths,BlockedUnitRangeFirsts}) = (length(b.lasts),) +size(b::Union{BlockedUnitRangeLengths,BlockedUnitRangeFirsts}) = size(b.lasts) @propagate_inbounds function getindex(b::BlockedUnitRangeLengths, k::Integer) if isone(k) @@ -230,7 +230,7 @@ end if isone(k) b.first else - b.lasts[k-1] + one(T) + b.lasts[k-1] + oneunit(T) end end @@ -607,7 +607,7 @@ function findblock(b::AbstractUnitRange{<:Integer}, k::Integer) end """ - blockfirsts(a::AbstractUnitRange{<:Integer}) + blockfirsts(a::AbstractUnitRange) Return the first index of each block of `a`. @@ -631,12 +631,16 @@ julia> blockfirsts(b) 4 ``` """ -@inline blockfirsts(a::AbstractUnitRange{<:Integer}) = Fill(first(a), 1) +@inline blockfirsts(a::AbstractUnitRange) = Fill(first(a), 1) +@inline blockfirsts(a::AbstractBlockedUnitRange) = BlockedUnitRangeFirsts(first(a), blocklasts(a)) -@inline blockfirsts(a::AbstractBlockedUnitRange{<:Integer}) = BlockedUnitRangeFirsts(first(a), blocklasts(a)) +# special support for tuple indexing +@inline function blockfirsts(a::AbstractBlockedUnitRange{<:Any,<:Tuple}) + return (first(a), (blocklasts(a)[oneto(end-1)] .+ oneunit(eltype(a)))...) +end """ - blocklasts(a::AbstractUnitRange{<:Integer}) + blocklasts(a::AbstractUnitRange) Return the last index of each block of `a`. @@ -660,10 +664,10 @@ julia> blocklasts(b) 6 ``` """ -blocklasts(a::AbstractUnitRange{<:Integer}) = Fill(eltype(a)(length(a)),1) +blocklasts(a::AbstractUnitRange) = Fill(eltype(a)(length(a)),1) """ - blocklengths(a::AbstractUnitRange{<:Integer}) + blocklengths(a::AbstractUnitRange) Return the length of each block of `a`. @@ -687,8 +691,13 @@ julia> blocklengths(b) 3 ``` """ -@inline blocklengths(a::AbstractUnitRange{<:Integer}) = blocklasts(a) -@inline blocklengths(a::AbstractBlockedUnitRange{<:Integer}) = BlockedUnitRangeLengths(first(a)-1, blocklasts(a)) +@inline blocklengths(a::AbstractUnitRange) = blocklasts(a) +@inline blocklengths(a::AbstractBlockedUnitRange{T}) where T = BlockedUnitRangeLengths(first(a)-oneunit(T), blocklasts(a)) +# special support for tuple indexing +@inline function blocklengths(a::AbstractBlockedUnitRange{<:Any,<:Tuple}) + return (first(blocklasts(a)) - first(a) + oneunit(eltype(a)), (blocklasts(a)[2:end] .- blocklasts(a)[1:end-1])...) +end +blocklengths(a::AbstractBlockedUnitRange{<:Any,Tuple{}}) = () Base.summary(io::IO, a::AbstractBlockedUnitRange) = _block_summary(io, a) diff --git a/test/test_blockindices.jl b/test/test_blockindices.jl index 9cfcc32e..c4984496 100644 --- a/test/test_blockindices.jl +++ b/test/test_blockindices.jl @@ -276,9 +276,9 @@ end @test @inferred(blocklengths(o)) == Ones{Int}(10) f = blockedrange(1, Fill(2,5)) - @test @inferred(blockfirsts(f)) ≡ 1:2:9 + @test @inferred(blockfirsts(f)) == 1:2:9 @test @inferred(blocklasts(f)) ≡ StepRangeLen(2,2,5) - @test @inferred(blocklengths(f)) ≡ Fill(2,5) + @test @inferred(blocklengths(f)) == Fill(2,5) f = blockedrange(1, Zeros{Int}(2)) @test @inferred(blockfirsts(f)) == [1,1] @@ -490,7 +490,7 @@ end # we support Tuples in addition to SVectors for InfiniteArrays.jl, which has # infinite block sizes s = blockedrange(1, (5,big(100_000_000)^2)) - @test blocklengths(s) == [5,big(100_000_000)^2] + @test blocklengths(s) == (5,big(100_000_000)^2) @test blockaxes(s) == (Block.(1:2),) @test findblock(s,3) == Block(1) @test findblock(s,big(100_000_000)) == Block(2) @@ -563,9 +563,9 @@ end @test @inferred(blocklengths(o)) == Ones{Int}(10) f = blockedrange(Fill(2,5)) - @test @inferred(blockfirsts(f)) ≡ 1:2:9 + @test @inferred(blockfirsts(f)) == 1:2:9 @test @inferred(blocklasts(f)) ≡ StepRangeLen(2,2,5) - @test @inferred(blocklengths(f)) ≡ Fill(2,5) + @test @inferred(blocklengths(f)) == Fill(2,5) f = blockedrange(Zeros{Int}(2)) @test @inferred(blockfirsts(f)) == [1,1] @@ -764,7 +764,7 @@ end @test eltype(s) === BigInt @test first(s) isa BigInt @test last(s) isa BigInt - @test blocklengths(s) == [5,big(100_000_000)^2] + @test blocklengths(s) == (5,big(100_000_000)^2) @test eltype(blocklengths(s)) === BigInt @test blockaxes(s) == (Block.(1:2),) @test findblock(s,3) == Block(1) @@ -777,7 +777,7 @@ end @test length(r) === 6 @test blockfirsts(r) === (1, 3, 5) @test blocklasts(r) === (2, 4, 6) - @test blocklengths(r) == [2, 2, 2] + @test blocklengths(r) == (2, 2, 2) end @testset "Empty Tuple" begin @@ -788,7 +788,7 @@ end @test length(r) === 0 @test blockfirsts(r) === (1,) @test blocklasts(r) === () - @test blocklengths(r) == [] + @test blocklengths(r) == () end @testset "General element types" begin @@ -815,15 +815,15 @@ end @test eltype(blockedrange(one(elt), Base.OneTo(elt(3)))) === elt @test eltype(blockedrange(one(elt), elt(1):elt(3))) === elt - if VERSION >= v"1.7" - # `cumsum(::Fill)` doesn't preserve element types properly. - # That issue was fixed by this fix to `StepRangeLen`: - # https://github.com/JuliaLang/julia/pull/41619 - # which is only available in Julia v1.7 and higher. - r = blockedrange(one(elt), Fill(elt(2), 3)) - @test r isa BlockedUnitRange{elt,<:StepRangeLen{elt}} - @test eltype(r) === elt - end + + # `cumsum(::Fill)` doesn't preserve element types properly. + # That issue was fixed by this fix to `StepRangeLen`: + # https://github.com/JuliaLang/julia/pull/41619 + # which is only available in Julia v1.7 and higher. + r = blockedrange(one(elt), Fill(elt(2), 3)) + @test r isa BlockedUnitRange{elt,<:StepRangeLen{elt}} + @test eltype(r) === elt + r = blockedrange(one(elt), Ones(elt, 3)) @test r isa BlockedUnitRange{elt} @@ -871,7 +871,7 @@ end @test last(r) === UInt16(5) @test blockfirsts(r) === (UInt16(1), UInt16(3)) @test blocklasts(r) === (UInt16(2), UInt16(5)) - @test blocklengths(r) == [UInt16(2), UInt16(3)] + @test blocklengths(r) === (UInt16(2), UInt16(3)) @test eltype(blocklengths(r)) === UInt16 end From f0b61cf76582c6cd547312b379f1eab39f3d0311 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Thu, 2 Jul 2026 23:57:26 +0100 Subject: [PATCH 3/9] fix docs --- .github/workflows/docs.yml | 2 +- src/BlockArrays.jl | 1 + src/blockaxis.jl | 4 ++-- src/blocks.jl | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index db26dddb..49427cf4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@v7 - uses: julia-actions/setup-julia@v3 with: - version: '1.10' + version: '1' - uses: julia-actions/cache@v3 - name: Install dependencies run: julia --project=docs/ -e 'using Pkg; Pkg.update(); Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' diff --git a/src/BlockArrays.jl b/src/BlockArrays.jl index d824ab04..b9d8e993 100644 --- a/src/BlockArrays.jl +++ b/src/BlockArrays.jl @@ -8,6 +8,7 @@ export blockaxes, blocksize, blocklength, blockcheckbounds, BlockBoundsError, Bl export blocksizes, blocklengths, blocklasts, blockfirsts, blockisequal, blockequals, blockisapprox export eachblockaxes export BlockRange, blockedrange, BlockedUnitRange, BlockedOneTo +export BlockedUnitRangeFirsts, BlockedUnitRangeLengths export BlockArray, BlockMatrix, BlockVector, BlockVecOrMat, mortar export BlockedArray, BlockedMatrix, BlockedVector, BlockedVecOrMat diff --git a/src/blockaxis.jl b/src/blockaxis.jl index 53660243..ca50cdcf 100644 --- a/src/blockaxis.jl +++ b/src/blockaxis.jl @@ -625,7 +625,7 @@ julia> b = blockedrange([1,2,3]) 6 julia> blockfirsts(b) -3-element Vector{Int64}: +3-element BlockedUnitRangeFirsts{Int64, Vector{Int64}}: 1 2 4 @@ -685,7 +685,7 @@ julia> b = blockedrange([1,2,3]) 6 julia> blocklengths(b) -3-element Vector{Int64}: +3-element BlockedUnitRangeLengths{Int64, Vector{Int64}}: 1 2 3 diff --git a/src/blocks.jl b/src/blocks.jl index b3da9361..789a8dac 100644 --- a/src/blocks.jl +++ b/src/blocks.jl @@ -144,7 +144,7 @@ julia> A = BlockArray(ones(3,3),[2,1],[1,1,1]) 1.0 │ 1.0 │ 1.0 julia> blocksizes(A) -2×3 BlockArrays.ProductArray{Tuple{Int64, Int64}, 2, Tuple{Vector{Int64}, Vector{Int64}}}: +2×3 BlockArrays.ProductArray{Tuple{Int64, Int64}, 2, Tuple{BlockedUnitRangeLengths{Int64, Vector{Int64}}, BlockedUnitRangeLengths{Int64, Vector{Int64}}}}: (2, 1) (2, 1) (2, 1) (1, 1) (1, 1) (1, 1) @@ -152,7 +152,7 @@ julia> blocksizes(A)[1,2] (2, 1) julia> blocksizes(A,2) -3-element Vector{Int64}: +3-element BlockedUnitRangeLengths{Int64, Vector{Int64}}: 1 1 1 From 009a74b719905d79509ae7b8795f67fc669dcb84 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Fri, 3 Jul 2026 00:20:22 +0100 Subject: [PATCH 4/9] support cumsum with BlockedUnitRangeLengths --- src/BlockArrays.jl | 2 +- src/blockaxis.jl | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/BlockArrays.jl b/src/BlockArrays.jl index b9d8e993..89f92e3a 100644 --- a/src/BlockArrays.jl +++ b/src/BlockArrays.jl @@ -29,7 +29,7 @@ import Base: @propagate_inbounds, Array, AbstractArray, to_indices, to_index, RangeIndex, Int, Integer, Number, Tuple, +, -, *, /, \, min, max, isless, in, copy, copyto!, axes, @deprecate, BroadcastStyle, checkbounds, checkindex, ensure_indexable, - oneunit, ones, zeros, intersect, Slice, resize! + oneunit, ones, zeros, intersect, Slice, resize!, cumsum using Base: ReshapedArray, LogicalIndex, dataids, oneto diff --git a/src/blockaxis.jl b/src/blockaxis.jl index ca50cdcf..933462a6 100644 --- a/src/blockaxis.jl +++ b/src/blockaxis.jl @@ -234,6 +234,8 @@ end end end +cumsum(b::BlockedUnitRangeLengths) = b.lasts .+ b.offset + length(a::AbstractBlockedUnitRange) = isempty(blocklasts(a)) ? zero(eltype(a)) : Integer(last(blocklasts(a))-first(a)+oneunit(eltype(a))) From def3bb524e193107e1d7b5a4c3dbccab5dbee78d Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Fri, 3 Jul 2026 07:54:02 +0100 Subject: [PATCH 5/9] Overload accumulate(+, ::BlockedUnitRangeLengths) --- src/BlockArrays.jl | 2 +- src/blockaxis.jl | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/BlockArrays.jl b/src/BlockArrays.jl index 89f92e3a..0d93d4af 100644 --- a/src/BlockArrays.jl +++ b/src/BlockArrays.jl @@ -29,7 +29,7 @@ import Base: @propagate_inbounds, Array, AbstractArray, to_indices, to_index, RangeIndex, Int, Integer, Number, Tuple, +, -, *, /, \, min, max, isless, in, copy, copyto!, axes, @deprecate, BroadcastStyle, checkbounds, checkindex, ensure_indexable, - oneunit, ones, zeros, intersect, Slice, resize!, cumsum + oneunit, ones, zeros, intersect, Slice, resize!, accumulate, cumsum using Base: ReshapedArray, LogicalIndex, dataids, oneto diff --git a/src/blockaxis.jl b/src/blockaxis.jl index 933462a6..afea5bc9 100644 --- a/src/blockaxis.jl +++ b/src/blockaxis.jl @@ -234,6 +234,7 @@ end end end +accumulate(::typeof(+), b::BlockedUnitRangeLengths) = cumsum(b) cumsum(b::BlockedUnitRangeLengths) = b.lasts .+ b.offset From fea06ef61603bc9bec40624411f96746cc4be526 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Fri, 3 Jul 2026 09:26:56 +0100 Subject: [PATCH 6/9] Add FillBlockedOneTo --- src/BlockArrays.jl | 2 +- src/blockaxis.jl | 90 ++++++++++++++++++++++++++------ src/blockbroadcast.jl | 2 +- test/test_blockarrayinterface.jl | 6 +-- test/test_blockcholesky.jl | 6 +-- test/test_blockindices.jl | 3 +- 6 files changed, 81 insertions(+), 28 deletions(-) diff --git a/src/BlockArrays.jl b/src/BlockArrays.jl index 0d93d4af..50feb6fe 100644 --- a/src/BlockArrays.jl +++ b/src/BlockArrays.jl @@ -40,7 +40,7 @@ import ArrayLayouts: MatLdivVec, MatLmulVec, MatMulMatAdd, MatMulVecAdd, MemoryL conjlayout, rowsupport, sub_materialize, sub_materialize_axes, sublayout, transposelayout, triangulardata, triangularlayout, zero!, materialize! -import FillArrays: axes_print_matrix_row +import FillArrays: axes_print_matrix_row, AbstractFillVector, ZerosVector import LinearAlgebra: AbstractTriangular, AdjOrTrans, HermOrSym, RealHermSymComplexHerm, StructuredMatrixStyle, lmul!, rmul! diff --git a/src/blockaxis.jl b/src/blockaxis.jl index afea5bc9..826fcbd2 100644 --- a/src/blockaxis.jl +++ b/src/blockaxis.jl @@ -108,6 +108,8 @@ function Base.getindex(r::AbstractUnitRange, s::AbstractBlockedUnitRange{T}) whe return blockedrange(start, lens) end +abstract type AbstractBlockedOneTo{T,CS} <: AbstractBlockedUnitRange{T,CS} end + """ BlockedOneTo{T, <:Union{AbstractVector{T}, NTuple{<:Any,T}}} where {T} @@ -117,7 +119,7 @@ This parallels `Base.OneTo` in that the first value is guaranteed to be `1`. Construction is typically via `blockedrange` which converts -a vector of block lengths to a `BlockedUnitRange`. +a vector of block lengths to a `AbstractBlockedUnitRange`. # Examples ```jldoctest @@ -136,7 +138,7 @@ julia> blockedrange([2,2,3]) # block lengths See also [`BlockedUnitRange`](@ref). """ -struct BlockedOneTo{T<:Integer,CS} <: AbstractBlockedUnitRange{T,CS} +struct BlockedOneTo{T<:Integer,CS} <: AbstractBlockedOneTo{T,CS} lasts::CS # assume that lasts is sorted, no checks carried out here function BlockedOneTo(lasts::CS) where {T<:Integer, CS<:AbstractVector{T}} @@ -154,29 +156,79 @@ end _throw_if_bool(_) = nothing _throw_if_bool(::Type{Bool}) = throw(ArgumentError("a Bool collection is not allowed as blocklasts")) -const DefaultBlockAxis = BlockedOneTo{Int, Vector{Int}} +""" + FillBlockedOneTo{T, <: AbstractRange{T}} where {T} -first(b::BlockedOneTo) = oneunit(eltype(b)) -@inline blocklasts(a::BlockedOneTo) = a.lasts +Define an `AbstractUnitRange{T}` that has been divided +into equally sized blocks, which is used to represent `axes` of block arrays. +This parallels `Base.OneTo` in that the first value is guaranteed +to be `1`. -BlockedOneTo(::BlockedOneTo) = throw(ArgumentError("Forbidden due to ambiguity")) +Construction is typically via `blockedrange` with a `AbstractFill` which converts +a vector of block lengths to a `AbstractBlockedUnitRange`. -axes(b::BlockedOneTo) = (b,) +# Examples +```jldoctest +julia> blockedrange(Fill(2,3)) +3-blocked 6-element BlockArrays.FillBlockedOneTo{Int64, StepRangeLen{Int64, Int64, Int64, Int64}}: + 1 + 2 + ─ + 3 + 4 + ─ + 5 + 6 +``` -function Base.AbstractUnitRange{T}(r::BlockedOneTo) where {T} - return BlockedOneTo(convert.(T,blocklasts(r))) +See also [`BlockedUnitRange`](@ref). +""" +struct FillBlockedOneTo{T<:Integer,CS<:AbstractRange} <: AbstractBlockedOneTo{T,CS} + lasts::CS + # assume that lasts is sorted, no checks carried out here + function FillBlockedOneTo(lasts::CS) where {T<:Integer, CS<:AbstractRange{T}} + _throw_if_bool(T) + Base.require_one_based_indexing(lasts) + isempty(lasts) || first(lasts) >= 0 || throw(ArgumentError("blocklasts must be >= 0")) + first(lasts) == step(lasts) || throw(ArgumentError("first block length must match others")) + new{T,CS}(lasts) + end end -# See: https://github.com/JuliaLang/julia/blob/b06d26075bf7b3f4e7f1b64b120f5665d8ed76f9/base/range.jl#L1006-L1010 -function getindex(r::Base.OneTo{T}, s::BlockedOneTo) where T - @inline - @boundscheck checkbounds(r, s) - return BlockedOneTo(convert(AbstractVector{T}, blocklasts(s))) +const DefaultBlockAxis = BlockedOneTo{Int, Vector{Int}} + +first(b::AbstractBlockedOneTo) = oneunit(eltype(b)) +@inline blocklasts(a::AbstractBlockedOneTo) = a.lasts + +BlockedOneTo(::AbstractBlockedOneTo) = throw(ArgumentError("Forbidden due to ambiguity")) +FillBlockedOneTo(::AbstractBlockedOneTo) = throw(ArgumentError("Forbidden due to ambiguity")) + +# we know if lasts are OneTo its all block-lengths == 1 +blockedoneto(lasts) = BlockedOneTo(lasts) +blockedoneto(lasts::Base.OneTo) = FillBlockedOneTo(lasts) + +axes(b::AbstractBlockedOneTo) = (b,) + +for BlockOne2 in (:BlockedOneTo, :FillBlockedOneTo) + @eval begin + function Base.AbstractUnitRange{T}(r::$BlockOne2) where {T} + return $BlockOne2(convert.(T,blocklasts(r))) + end + # See: https://github.com/JuliaLang/julia/blob/b06d26075bf7b3f4e7f1b64b120f5665d8ed76f9/base/range.jl#L1006-L1010 + function getindex(r::Base.OneTo{T}, s::$BlockOne2) where T + @inline + @boundscheck checkbounds(r, s) + return $BlockOne2(convert(AbstractVector{T}, blocklasts(s))) + end + end end -function getindex(r::BlockedOneTo{T}, s::BlockedOneTo) where T + +function getindex(r::AbstractBlockedOneTo{T}, s::AbstractBlockedOneTo) where T return Base.oneto(r)[s] end + + """ blockedrange(blocklengths::Union{Tuple, AbstractVector}) blockedrange(first::Integer, blocklengths::Union{Tuple, AbstractVector}) @@ -204,7 +256,8 @@ julia> blockedrange(2, (1,2)) """ @inline blockedrange(blocks::Union{Tuple,AbstractVector}) = BlockedOneTo(_blocklengths2blocklasts(blocks)) @inline blockedrange(f::Integer, blocks::Union{Tuple,AbstractVector}) = _BlockedUnitRange(f, f-oneunit(f) .+ _blocklengths2blocklasts(blocks)) - +@inline blockedrange(blocks::AbstractFillVector) = FillBlockedOneTo(_blocklengths2blocklasts(blocks)) +@inline blockedrange(blocks::ZerosVector) = BlockedOneTo(_blocklengths2blocklasts(blocks)) # Zero-steps not supported in FillBlockedOneTo struct BlockedUnitRangeLengths{T<:Integer, LASTS<:AbstractVector{T}} <: AbstractVector{T} offset::T @@ -636,6 +689,7 @@ julia> blockfirsts(b) """ @inline blockfirsts(a::AbstractUnitRange) = Fill(first(a), 1) @inline blockfirsts(a::AbstractBlockedUnitRange) = BlockedUnitRangeFirsts(first(a), blocklasts(a)) +@inline blockfirsts(a::FillBlockedOneTo) = blocklasts(a) .- first(blocklasts(a)) .+ oneunit(eltype(a)) # special support for tuple indexing @inline function blockfirsts(a::AbstractBlockedUnitRange{<:Any,<:Tuple}) @@ -667,7 +721,7 @@ julia> blocklasts(b) 6 ``` """ -blocklasts(a::AbstractUnitRange) = Fill(eltype(a)(length(a)),1) +@inline blocklasts(a::AbstractUnitRange) = Fill(eltype(a)(length(a)),1) """ blocklengths(a::AbstractUnitRange) @@ -701,6 +755,8 @@ julia> blocklengths(b) return (first(blocklasts(a)) - first(a) + oneunit(eltype(a)), (blocklasts(a)[2:end] .- blocklasts(a)[1:end-1])...) end blocklengths(a::AbstractBlockedUnitRange{<:Any,Tuple{}}) = () +@inline blocklengths(a::FillBlockedOneTo) = Fill(step(blocklasts(a)), length(blocklasts(a))) +@inline blocklengths(a::FillBlockedOneTo{T,<:AbstractUnitRange}) where T<:Integer = Ones{T}(length(blocklasts(a))) Base.summary(io::IO, a::AbstractBlockedUnitRange) = _block_summary(io, a) diff --git a/src/blockbroadcast.jl b/src/blockbroadcast.jl index c1180268..2f67b53d 100644 --- a/src/blockbroadcast.jl +++ b/src/blockbroadcast.jl @@ -40,7 +40,7 @@ sortedunion(a,b) = maybeinplacesort!(union(a,b)) sortedunion(a::Base.OneTo, b::Base.OneTo) = Base.OneTo(max(last(a),last(b))) sortedunion(a::AbstractUnitRange, b::AbstractUnitRange) = min(first(a),first(b)):max(last(a),last(b)) combine_blockaxes(a, b) = _BlockedUnitRange(sortedunion(blocklasts(a), blocklasts(b))) -combine_blockaxes(a::BlockedOneTo, b::BlockedOneTo) = BlockedOneTo(sortedunion(blocklasts(a), blocklasts(b))) +combine_blockaxes(a::AbstractBlockedOneTo, b::AbstractBlockedOneTo) = blockedoneto(sortedunion(blocklasts(a), blocklasts(b))) Base.Broadcast.axistype(a::AbstractBlockedUnitRange, b::AbstractBlockedUnitRange) = length(b) == 1 ? a : combine_blockaxes(a, b) Base.Broadcast.axistype(a::AbstractBlockedUnitRange, b) = length(b) == 1 ? a : combine_blockaxes(a, b) diff --git a/test/test_blockarrayinterface.jl b/test/test_blockarrayinterface.jl index 57581398..4e19d43e 100644 --- a/test/test_blockarrayinterface.jl +++ b/test/test_blockarrayinterface.jl @@ -146,14 +146,14 @@ end @test axes(D) isa NTuple{2,BlockedOneTo} @test blockisequal(axes(D, 1), axes(parent(D), 1)) @test D == Diagonal(Vector(parent(D))) - @test MemoryLayout(D) isa BlockArrays.DiagonalLayout{<:BlockArrays.BlockLayout} + @test MemoryLayout(D) isa ArrayLayouts.DiagonalLayout{<:BlockArrays.BlockLayout} end @testset "non-standard block axes" begin A = BlockArray([1 2; 3 4], Fill(1, 2), Fill(1, 2)) - @test A isa BlockMatrix{Int,Matrix{Matrix{Int}},<:NTuple{2,BlockedOneTo{Int,<:AbstractRange}}} + @test A isa BlockMatrix{Int,Matrix{Matrix{Int}},<:NTuple{2,BlockArrays.FillBlockedOneTo{Int,<:AbstractRange}}} A = BlockArray([1 2; 3 4], Fill(1, 2), [1, 1]) - @test A isa BlockMatrix{Int,Matrix{Matrix{Int}},<:Tuple{BlockedOneTo{Int,<:AbstractRange},BlockedOneTo{Int,Vector{Int}}}} + @test A isa BlockMatrix{Int,Matrix{Matrix{Int}},<:Tuple{BlockArrays.FillBlockedOneTo{Int,<:AbstractRange},BlockedOneTo{Int,Vector{Int}}}} end @testset "block Fill" begin diff --git a/test/test_blockcholesky.jl b/test/test_blockcholesky.jl index 0eb5dca3..c3d96099 100644 --- a/test/test_blockcholesky.jl +++ b/test/test_blockcholesky.jl @@ -26,11 +26,7 @@ Random.seed!(0) D_T = Matrix(D) #Test on nonsymmetric matrix - if VERSION < v"1.8-" - @test_throws MethodError cholesky(nsym) - else - @test_throws DimensionMismatch cholesky(nsym) - end + @test_throws DimensionMismatch cholesky(nsym) #Tests on A @test cholesky(A).U ≈ cholesky(A_T).U diff --git a/test/test_blockindices.jl b/test/test_blockindices.jl index c4984496..7643f3c2 100644 --- a/test/test_blockindices.jl +++ b/test/test_blockindices.jl @@ -593,7 +593,8 @@ end b = blockedrange(Fill(2,3)) c = blockedrange([2,2,2]) - @test convert(BlockedOneTo, b) === b + @test convert(BlockArrays.AbstractBlockedOneTo, b) === b + @test convert(BlockedOneTo, b) === BlockedOneTo(b.lasts) @test convert(typeof(b), b) === b @test convert(BlockedOneTo, c) === c @test convert(typeof(c), c) === c From 7963faf369400baeb456f0aaaa73a941631e4de7 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Fri, 3 Jul 2026 13:33:11 +0100 Subject: [PATCH 7/9] Replace FillBlockedOneTo with a new special range FirstStepRangeLen --- ext/BlockArraysAdaptExt.jl | 2 +- src/BlockArrays.jl | 10 ++- src/blockaxis.jl | 89 +++++---------------- src/blockbroadcast.jl | 2 +- src/firststeprangelen.jl | 129 +++++++++++++++++++++++++++++++ test/test_blockarrayinterface.jl | 4 +- test/test_blockindices.jl | 5 +- 7 files changed, 163 insertions(+), 78 deletions(-) create mode 100644 src/firststeprangelen.jl diff --git a/ext/BlockArraysAdaptExt.jl b/ext/BlockArraysAdaptExt.jl index 259b23d4..d8cad922 100644 --- a/ext/BlockArraysAdaptExt.jl +++ b/ext/BlockArraysAdaptExt.jl @@ -2,7 +2,7 @@ module BlockArraysAdaptExt using Adapt using BlockArrays -using BlockArrays: _BlockArray, _BlockedUnitRange +using BlockArrays: _BlockArray, _BlockedUnitRange, BlockedOneTo import Adapt: adapt_structure adapt_structure(to, r::BlockedUnitRange) = _BlockedUnitRange(adapt(to, r.first), map(adapt(to), r.lasts)) diff --git a/src/BlockArrays.jl b/src/BlockArrays.jl index 50feb6fe..780acccd 100644 --- a/src/BlockArrays.jl +++ b/src/BlockArrays.jl @@ -29,9 +29,10 @@ import Base: @propagate_inbounds, Array, AbstractArray, to_indices, to_index, RangeIndex, Int, Integer, Number, Tuple, +, -, *, /, \, min, max, isless, in, copy, copyto!, axes, @deprecate, BroadcastStyle, checkbounds, checkindex, ensure_indexable, - oneunit, ones, zeros, intersect, Slice, resize!, accumulate, cumsum + oneunit, ones, zeros, intersect, Slice, resize!, accumulate, cumsum, + promote_rule -using Base: ReshapedArray, LogicalIndex, dataids, oneto +using Base: ReshapedArray, LogicalIndex, dataids, oneto, OneTo import Base: (:), IteratorSize, iterate, axes1, strides, isempty import Base.Broadcast: broadcasted, DefaultArrayStyle, AbstractArrayStyle, Broadcasted, broadcastable @@ -40,7 +41,7 @@ import ArrayLayouts: MatLdivVec, MatLmulVec, MatMulMatAdd, MatMulVecAdd, MemoryL conjlayout, rowsupport, sub_materialize, sub_materialize_axes, sublayout, transposelayout, triangulardata, triangularlayout, zero!, materialize! -import FillArrays: axes_print_matrix_row, AbstractFillVector, ZerosVector +import FillArrays: axes_print_matrix_row, AbstractFillVector, AbstractZerosVector, AbstractOnesVector, getindex_value import LinearAlgebra: AbstractTriangular, AdjOrTrans, HermOrSym, RealHermSymComplexHerm, StructuredMatrixStyle, lmul!, rmul! @@ -58,6 +59,9 @@ end _maybetail(::Tuple{}) = () _maybetail(t::Tuple) = tail(t) +include("firststeprangelen.jl") +using .FirstStepRange + include("blockindices.jl") include("blockaxis.jl") include("abstractblockarray.jl") diff --git a/src/blockaxis.jl b/src/blockaxis.jl index 826fcbd2..e701ea21 100644 --- a/src/blockaxis.jl +++ b/src/blockaxis.jl @@ -93,6 +93,8 @@ first(b::BlockedUnitRange) = b.first BlockedUnitRange(::BlockedUnitRange) = throw(ArgumentError("Forbidden due to ambiguity")) # Use `accumulate` instead of `cumsum` because it preserves the element type of the block lengths _blocklengths2blocklasts(blocks) = accumulate(+, blocks) # extra level to allow changing default accumulate behaviour +_blocklengths2blocklasts(blocks::AbstractFillVector) = FirstStepRangeLen(getindex_value(blocks), length(blocks)) # encode that the first is the same as step +_blocklengths2blocklasts(blocks::Union{AbstractOnesVector,AbstractZerosVector}) = accumulate(+, blocks) # cumsum of Ones/Zeros gets more precise structure function Base.AbstractUnitRange{T}(r::BlockedUnitRange) where {T} return _BlockedUnitRange(convert(T,first(r)), convert.(T,blocklasts(r))) @@ -108,8 +110,6 @@ function Base.getindex(r::AbstractUnitRange, s::AbstractBlockedUnitRange{T}) whe return blockedrange(start, lens) end -abstract type AbstractBlockedOneTo{T,CS} <: AbstractBlockedUnitRange{T,CS} end - """ BlockedOneTo{T, <:Union{AbstractVector{T}, NTuple{<:Any,T}}} where {T} @@ -138,7 +138,7 @@ julia> blockedrange([2,2,3]) # block lengths See also [`BlockedUnitRange`](@ref). """ -struct BlockedOneTo{T<:Integer,CS} <: AbstractBlockedOneTo{T,CS} +struct BlockedOneTo{T<:Integer,CS} <: AbstractBlockedUnitRange{T,CS} lasts::CS # assume that lasts is sorted, no checks carried out here function BlockedOneTo(lasts::CS) where {T<:Integer, CS<:AbstractVector{T}} @@ -156,74 +156,27 @@ end _throw_if_bool(_) = nothing _throw_if_bool(::Type{Bool}) = throw(ArgumentError("a Bool collection is not allowed as blocklasts")) -""" - FillBlockedOneTo{T, <: AbstractRange{T}} where {T} +const DefaultBlockAxis = BlockedOneTo{Int, Vector{Int}} -Define an `AbstractUnitRange{T}` that has been divided -into equally sized blocks, which is used to represent `axes` of block arrays. -This parallels `Base.OneTo` in that the first value is guaranteed -to be `1`. +first(b::BlockedOneTo) = oneunit(eltype(b)) +@inline blocklasts(a::BlockedOneTo) = a.lasts -Construction is typically via `blockedrange` with a `AbstractFill` which converts -a vector of block lengths to a `AbstractBlockedUnitRange`. +BlockedOneTo(::BlockedOneTo) = throw(ArgumentError("Forbidden due to ambiguity")) -# Examples -```jldoctest -julia> blockedrange(Fill(2,3)) -3-blocked 6-element BlockArrays.FillBlockedOneTo{Int64, StepRangeLen{Int64, Int64, Int64, Int64}}: - 1 - 2 - ─ - 3 - 4 - ─ - 5 - 6 -``` +axes(b::BlockedOneTo) = (b,) -See also [`BlockedUnitRange`](@ref). -""" -struct FillBlockedOneTo{T<:Integer,CS<:AbstractRange} <: AbstractBlockedOneTo{T,CS} - lasts::CS - # assume that lasts is sorted, no checks carried out here - function FillBlockedOneTo(lasts::CS) where {T<:Integer, CS<:AbstractRange{T}} - _throw_if_bool(T) - Base.require_one_based_indexing(lasts) - isempty(lasts) || first(lasts) >= 0 || throw(ArgumentError("blocklasts must be >= 0")) - first(lasts) == step(lasts) || throw(ArgumentError("first block length must match others")) - new{T,CS}(lasts) - end +function Base.AbstractUnitRange{T}(r::BlockedOneTo) where {T} + return BlockedOneTo(convert.(T,blocklasts(r))) end - -const DefaultBlockAxis = BlockedOneTo{Int, Vector{Int}} - -first(b::AbstractBlockedOneTo) = oneunit(eltype(b)) -@inline blocklasts(a::AbstractBlockedOneTo) = a.lasts - -BlockedOneTo(::AbstractBlockedOneTo) = throw(ArgumentError("Forbidden due to ambiguity")) -FillBlockedOneTo(::AbstractBlockedOneTo) = throw(ArgumentError("Forbidden due to ambiguity")) - -# we know if lasts are OneTo its all block-lengths == 1 -blockedoneto(lasts) = BlockedOneTo(lasts) -blockedoneto(lasts::Base.OneTo) = FillBlockedOneTo(lasts) - -axes(b::AbstractBlockedOneTo) = (b,) - -for BlockOne2 in (:BlockedOneTo, :FillBlockedOneTo) - @eval begin - function Base.AbstractUnitRange{T}(r::$BlockOne2) where {T} - return $BlockOne2(convert.(T,blocklasts(r))) - end - # See: https://github.com/JuliaLang/julia/blob/b06d26075bf7b3f4e7f1b64b120f5665d8ed76f9/base/range.jl#L1006-L1010 - function getindex(r::Base.OneTo{T}, s::$BlockOne2) where T - @inline - @boundscheck checkbounds(r, s) - return $BlockOne2(convert(AbstractVector{T}, blocklasts(s))) - end - end +# See: https://github.com/JuliaLang/julia/blob/b06d26075bf7b3f4e7f1b64b120f5665d8ed76f9/base/range.jl#L1006-L1010 +function getindex(r::Base.OneTo{T}, s::BlockedOneTo) where T + @inline + @boundscheck checkbounds(r, s) + return BlockedOneTo(convert(AbstractVector{T}, blocklasts(s))) end -function getindex(r::AbstractBlockedOneTo{T}, s::AbstractBlockedOneTo) where T + +function getindex(r::BlockedOneTo{T}, s::BlockedOneTo) where T return Base.oneto(r)[s] end @@ -256,8 +209,6 @@ julia> blockedrange(2, (1,2)) """ @inline blockedrange(blocks::Union{Tuple,AbstractVector}) = BlockedOneTo(_blocklengths2blocklasts(blocks)) @inline blockedrange(f::Integer, blocks::Union{Tuple,AbstractVector}) = _BlockedUnitRange(f, f-oneunit(f) .+ _blocklengths2blocklasts(blocks)) -@inline blockedrange(blocks::AbstractFillVector) = FillBlockedOneTo(_blocklengths2blocklasts(blocks)) -@inline blockedrange(blocks::ZerosVector) = BlockedOneTo(_blocklengths2blocklasts(blocks)) # Zero-steps not supported in FillBlockedOneTo struct BlockedUnitRangeLengths{T<:Integer, LASTS<:AbstractVector{T}} <: AbstractVector{T} offset::T @@ -689,7 +640,7 @@ julia> blockfirsts(b) """ @inline blockfirsts(a::AbstractUnitRange) = Fill(first(a), 1) @inline blockfirsts(a::AbstractBlockedUnitRange) = BlockedUnitRangeFirsts(first(a), blocklasts(a)) -@inline blockfirsts(a::FillBlockedOneTo) = blocklasts(a) .- first(blocklasts(a)) .+ oneunit(eltype(a)) +@inline blockfirsts(a::BlockedOneTo{<:Any,<:FirstStepRanges}) = blocklasts(a) .- first(blocklasts(a)) .+ oneunit(eltype(a)) # special support for tuple indexing @inline function blockfirsts(a::AbstractBlockedUnitRange{<:Any,<:Tuple}) @@ -755,8 +706,8 @@ julia> blocklengths(b) return (first(blocklasts(a)) - first(a) + oneunit(eltype(a)), (blocklasts(a)[2:end] .- blocklasts(a)[1:end-1])...) end blocklengths(a::AbstractBlockedUnitRange{<:Any,Tuple{}}) = () -@inline blocklengths(a::FillBlockedOneTo) = Fill(step(blocklasts(a)), length(blocklasts(a))) -@inline blocklengths(a::FillBlockedOneTo{T,<:AbstractUnitRange}) where T<:Integer = Ones{T}(length(blocklasts(a))) +@inline blocklengths(a::BlockedOneTo{<:Any, <:FirstStepRangeLen}) = Fill(step(blocklasts(a)), length(blocklasts(a))) +@inline blocklengths(a::BlockedOneTo{T,<:OneTo}) where T<:Integer = Ones{T}(length(blocklasts(a))) Base.summary(io::IO, a::AbstractBlockedUnitRange) = _block_summary(io, a) diff --git a/src/blockbroadcast.jl b/src/blockbroadcast.jl index 2f67b53d..c1180268 100644 --- a/src/blockbroadcast.jl +++ b/src/blockbroadcast.jl @@ -40,7 +40,7 @@ sortedunion(a,b) = maybeinplacesort!(union(a,b)) sortedunion(a::Base.OneTo, b::Base.OneTo) = Base.OneTo(max(last(a),last(b))) sortedunion(a::AbstractUnitRange, b::AbstractUnitRange) = min(first(a),first(b)):max(last(a),last(b)) combine_blockaxes(a, b) = _BlockedUnitRange(sortedunion(blocklasts(a), blocklasts(b))) -combine_blockaxes(a::AbstractBlockedOneTo, b::AbstractBlockedOneTo) = blockedoneto(sortedunion(blocklasts(a), blocklasts(b))) +combine_blockaxes(a::BlockedOneTo, b::BlockedOneTo) = BlockedOneTo(sortedunion(blocklasts(a), blocklasts(b))) Base.Broadcast.axistype(a::AbstractBlockedUnitRange, b::AbstractBlockedUnitRange) = length(b) == 1 ? a : combine_blockaxes(a, b) Base.Broadcast.axistype(a::AbstractBlockedUnitRange, b) = length(b) == 1 ? a : combine_blockaxes(a, b) diff --git a/src/firststeprangelen.jl b/src/firststeprangelen.jl new file mode 100644 index 00000000..5d88bbd7 --- /dev/null +++ b/src/firststeprangelen.jl @@ -0,0 +1,129 @@ +### +# based on julia/base/range.jl +### +module FirstStepRange +using Base + +import Base: el_same, StepRangeLen, step, step_hp, getindex, unsafe_getindex, length, first, last, iterate, OneTo, promote_rule, isempty, show, ==, -, + +import Base: _reverse +import Base: broadcasted +using Base.Broadcast: DefaultArrayStyle + +export FirstStepRangeLen, FirstStepRanges + +""" + FirstStepRangeLen(step::S, len) where S + FirstStepRangeLen{T}(step::S, len) where {S, T} + +A range `r` where `r[i]` produces values of type `T` (in the first +form, `T` is deduced automatically), a `step`, and the `len`gth. The `step` is also the starting +value `r[1]`. This is used to encode block lasts corresponding to a fixed block size. +""" +struct FirstStepRangeLen{T,S,L<:Integer} <: AbstractRange{T} + step::S # step value + len::L # length of the range + + function FirstStepRangeLen{T,S,L}(step::S, len::Integer) where {T,S,L} + if T <: Integer && !isinteger(step + step) + throw(ArgumentError("FirstStepRangeLen{<:Integer} cannot have non-integer step")) + end + len = convert(L, len) + len >= zero(len) || throw(ArgumentError("length cannot be negative, got $len")) + L1 = oneunit(typeof(len)) + return new(step, len) + end +end + +FirstStepRangeLen{T,S}(step::S, len::Integer) where {T,S} = + FirstStepRangeLen{T,S,promote_type(Int,typeof(len))}(step, len) +FirstStepRangeLen(step::S, len::Integer) where {S} = + FirstStepRangeLen{typeof(zero(step)+zero(step)),S,promote_type(Int,typeof(len))}(step, len) +FirstStepRangeLen{T}(step::S, len::Integer) where {T,S} = + FirstStepRangeLen{T,S,promote_type(Int,typeof(len))}(step, len) + +isempty(r::FirstStepRangeLen) = length(r) == 0 + +step(r::FirstStepRangeLen) = r.step +step_hp(r::FirstStepRangeLen) = r.step +length(r::FirstStepRangeLen) = r.len +first(r::FirstStepRangeLen) = unsafe_getindex(r, 1) +last(r::FirstStepRangeLen) = unsafe_getindex(r, length(r)) + +StepRangeLen(r::FirstStepRangeLen{T}) where T = StepRangeLen{T}(first(r), step(r), length(r)) + + +iterate(r::FirstStepRangeLen, i...) = iterate(StepRangeLen(r), i...) +unsafe_getindex(r::FirstStepRangeLen{T}, i::Integer) where T = T(step(r)i) +getindex(r::FirstStepRangeLen{T}, s::OrdinalRange{S}) where {T, S<:Integer} = StepRangeLen(r)[s] + +function show(io::IO, r::FirstStepRangeLen) + if !iszero(step(r)) + print(io, repr(first(r)), ':', repr(step(r)), ':', repr(last(r))) + else + # ugly temporary printing, to avoid 0:0:0 etc. + print(io, "FirstStepRangeLen(", repr(first(r)), ", ", repr(step(r)), ", ", repr(length(r)), ")") + end +end + +==(r::FirstStepRangeLen, s::FirstStepRangeLen) = + (isempty(r) & isempty(s)) | ((length(r) == length(s)) & (last(r) == last(s))) + +==(r::FirstStepRangeLen{T}, s::Union{StepRange{T},StepRangeLen{T,T}}) where {T} = StepRangeLen(r) == s +==(r::Union{StepRange{T},StepRangeLen{T,T}}, s::FirstStepRangeLen{T}) where {T} = r == StepRangeLen(s) + + +-(r::FirstStepRangeLen{T,S,L}) where {T,S,L} = FirstStepRangeLen{T,S,L}(-r.step, r.len) + +function promote_rule(::Type{FirstStepRangeLen{T1,S1,L1}},::Type{FirstStepRangeLen{T2,S2,L2}}) where {T1,T2,S1,S2,L1,L2} + S, L = promote_type(S1, S2), promote_type(L1, L2) + el_same(promote_type(T1, T2), FirstStepRangeLen{T1,S,L}, FirstStepRangeLen{T2,S,L}) +end +FirstStepRangeLen{T,S,L}(r::FirstStepRangeLen{T,S,L}) where {T,S,L} = r +FirstStepRangeLen{T,S,L}(r::FirstStepRangeLen) where {T,S,L} = + FirstStepRangeLen{T,S,L}(convert(S, r.step), convert(L, r.len)) +FirstStepRangeLen{T}(r::FirstStepRangeLen) where {T} = + FirstStepRangeLen(convert(T, r.step), r.len) + +promote_rule(a::Type{FirstStepRangeLen{T,S,L}}, ::Type{OR}) where {T,S,L,OR<:AbstractRange} = + promote_rule(a, FirstStepRangeLen{eltype(OR), eltype(OR), Int}) + +promote_rule(::Type{LinRange{A,L}}, b::Type{FirstStepRangeLen{T2,S2,L2}}) where {A,L,T2,S2,L2} = + promote_rule(FirstStepRangeLen{A,A,L}, b) + + +_reverse(r::FirstStepRangeLen, ::Colon) = typeof(r)(negate(r.step), length(r), offset) + +function +(r1::FirstStepRangeLen{T,S}, r2::FirstStepRangeLen{T,S}) where {T,S} + len = length(r1) + (len == length(r2) || + throw(DimensionMismatch("argument dimensions must match: length of r1 is $len, length of r2 is $(length(r2))"))) + FirstStepRangeLen(step(r1)+step(r2), len) +end + +-(r1::FirstStepRangeLen, r2::FirstStepRangeLen) = +(r1, -r2) + + +const FirstStepRanges = Union{FirstStepRangeLen, OneTo} + + +###### +# from base/broadcast.jl +###### + +broadcasted(::DefaultArrayStyle{1}, ::typeof(-), r::FirstStepRangeLen) = FirstStepRangeLen(negate(r.step), length(r)) +for op in (:+, :-) + @eval begin + broadcasted(::DefaultArrayStyle{1}, ::typeof($op), r::FirstStepRangeLen{T}, x::Number) where T = broadcasted(DefaultArrayStyle{1}(), $op, StepRangeLen(r), x) + broadcasted(::DefaultArrayStyle{1}, ::typeof($op), x::Number, r::FirstStepRangeLen{T}) where T = broadcasted(DefaultArrayStyle{1}(), $op, x, StepRangeLen(r)) + end +end +broadcasted(::DefaultArrayStyle{1}, ::typeof(*), x::Number, r::FirstStepRangeLen{T}) where {T} = + FirstStepRangeLen{typeof(x*T(r.step))}(x*r.step, length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(*), r::FirstStepRangeLen{T}, x::Number) where {T} = + FirstStepRangeLen{typeof(T(r.step)*x)}(r.step*x, length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(/), r::FirstStepRangeLen{T}, x::Number) where {T} = + FirstStepRangeLen{typeof(T(r.step)/x)}(r.step/x, length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(\), x::Number, r::FirstStepRangeLen) = FirstStepRangeLen(x\r.step, length(r)) +broadcasted(::DefaultArrayStyle{1}, ::typeof(big), r::FirstStepRangeLen) = FirstStepRangeLen(big(r.step), length(r)) + +end # module \ No newline at end of file diff --git a/test/test_blockarrayinterface.jl b/test/test_blockarrayinterface.jl index 4e19d43e..60aeaffa 100644 --- a/test/test_blockarrayinterface.jl +++ b/test/test_blockarrayinterface.jl @@ -151,9 +151,9 @@ end @testset "non-standard block axes" begin A = BlockArray([1 2; 3 4], Fill(1, 2), Fill(1, 2)) - @test A isa BlockMatrix{Int,Matrix{Matrix{Int}},<:NTuple{2,BlockArrays.FillBlockedOneTo{Int,<:AbstractRange}}} + @test A isa BlockMatrix{Int,Matrix{Matrix{Int}},<:NTuple{2,BlockedOneTo{Int,<:BlockArrays.FirstStepRangeLen}}} A = BlockArray([1 2; 3 4], Fill(1, 2), [1, 1]) - @test A isa BlockMatrix{Int,Matrix{Matrix{Int}},<:Tuple{BlockArrays.FillBlockedOneTo{Int,<:AbstractRange},BlockedOneTo{Int,Vector{Int}}}} + @test A isa BlockMatrix{Int,Matrix{Matrix{Int}},<:Tuple{BlockedOneTo{Int,<:BlockArrays.FirstStepRangeLen},BlockedOneTo{Int,Vector{Int}}}} end @testset "block Fill" begin diff --git a/test/test_blockindices.jl b/test/test_blockindices.jl index 7643f3c2..eb745555 100644 --- a/test/test_blockindices.jl +++ b/test/test_blockindices.jl @@ -4,6 +4,7 @@ using BlockArrays, FillArrays, Test, StaticArrays, ArrayLayouts using OffsetArrays import BlockArrays: BlockIndex, BlockIndexRange, BlockSlice, NoncontiguousBlockSlice import BlockArrays: split_index, merge_indices +using BlockArrays.FirstStepRange @testset "Blocks" begin @test Int(Block(2)) === Integer(Block(2)) === Number(Block(2)) === 2 @@ -564,7 +565,7 @@ end f = blockedrange(Fill(2,5)) @test @inferred(blockfirsts(f)) == 1:2:9 - @test @inferred(blocklasts(f)) ≡ StepRangeLen(2,2,5) + @test @inferred(blocklasts(f)) ≡ FirstStepRangeLen(2,5) @test @inferred(blocklengths(f)) == Fill(2,5) f = blockedrange(Zeros{Int}(2)) @@ -593,7 +594,7 @@ end b = blockedrange(Fill(2,3)) c = blockedrange([2,2,2]) - @test convert(BlockArrays.AbstractBlockedOneTo, b) === b + @test convert(BlockedOneTo, b) === b @test convert(BlockedOneTo, b) === BlockedOneTo(b.lasts) @test convert(typeof(b), b) === b @test convert(BlockedOneTo, c) === c From 9f7b1f620cef08969114f269e4e9e39982ed4e84 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Fri, 3 Jul 2026 13:49:27 +0100 Subject: [PATCH 8/9] fix dock string --- src/BlockArrays.jl | 2 ++ src/blockproduct.jl | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/BlockArrays.jl b/src/BlockArrays.jl index 780acccd..9f5a6ba2 100644 --- a/src/BlockArrays.jl +++ b/src/BlockArrays.jl @@ -13,6 +13,8 @@ export BlockedUnitRangeFirsts, BlockedUnitRangeLengths export BlockArray, BlockMatrix, BlockVector, BlockVecOrMat, mortar export BlockedArray, BlockedMatrix, BlockedVector, BlockedVecOrMat +export FirstStepRangeLen + export undef_blocks, undef, findblock, findblockindex export khatri_rao, blockkron, BlockKron diff --git a/src/blockproduct.jl b/src/blockproduct.jl index 43fd9887..593c24b9 100644 --- a/src/blockproduct.jl +++ b/src/blockproduct.jl @@ -151,7 +151,7 @@ julia> A = reshape(1:9, 3, 3) 3 6 9 julia> BlockArrays.blockvec(A) -3-blocked 9-element BlockedVector{Int64, UnitRange{Int64}, Tuple{BlockedOneTo{Int64, StepRangeLen{Int64, Int64, Int64, Int64}}}}: +3-blocked 9-element BlockedVector{Int64, UnitRange{Int64}, Tuple{BlockedOneTo{Int64, FirstStepRangeLen{Int64, Int64, Int64}}}}: 1 2 3 From 1bb316d69aa8750c4db05d77f4a5b5e57c698f11 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Fri, 3 Jul 2026 14:24:39 +0100 Subject: [PATCH 9/9] Update downstream.yml --- .github/workflows/downstream.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/downstream.yml b/.github/workflows/downstream.yml index 0bb0812e..9f17d7e6 100644 --- a/.github/workflows/downstream.yml +++ b/.github/workflows/downstream.yml @@ -40,6 +40,7 @@ jobs: - {repo: LazyArrays.jl, group: JuliaArrays} - {repo: BlockBandedMatrices.jl, group: JuliaLinearAlgebra} - {repo: LazyBandedMatrices.jl, group: JuliaLinearAlgebra} + - {repo: InfiniteArrays.jl, group: JuliaArrays} - {repo: InfiniteLinearAlgebra.jl, group: JuliaLinearAlgebra} steps: