From 72bc37e42707759ca6d40aab020aa0fe384f4217 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Tue, 22 Sep 2026 11:35:22 +0100 Subject: [PATCH 1/6] broadcast adjoint should return an adjoint when possible --- src/broadcast.jl | 80 +++++++++++++++++++++++++++++++++++++++ test/test_broadcasting.jl | 45 ++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/src/broadcast.jl b/src/broadcast.jl index cfc54fb..e95fe68 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -32,6 +32,86 @@ BroadcastStyle(::BlockBandedStyle, ::DefaultArrayStyle{2}) = BlockBandedStyle() BroadcastStyle(::DefaultArrayStyle{2}, ::BandedBlockBandedStyle) = BandedBlockBandedStyle() BroadcastStyle(::BandedBlockBandedStyle, ::DefaultArrayStyle{2}) = BandedBlockBandedStyle() +### +# Adjoints and transposes +# +# The adjoint (transpose) of a block-banded matrix is not stored as a block-banded +# matrix, so broadcasting is done on the parent and the result re-wrapped, e.g. +# `α .* A'` returns an `Adjoint` of a block-banded matrix. This is valid whenever the +# broadcasted function commutes with conjugation, as `f.(A', α) == f.(A, conj(α))'`. +### + +""" + AdjTransBlockBandedStyle{Adj,Sty} + +is a `BroadcastStyle` for an `Adjoint` (`Adj == typeof(adjoint)`) or `Transpose` +(`Adj == typeof(transpose)`) of a block-banded matrix whose parent has +`BroadcastStyle` `Sty`. +""" +struct AdjTransBlockBandedStyle{Adj,Sty} <: AbstractArrayStyle{2} end + +AdjTransBlockBandedStyle{Adj,Sty}(::Val{2}) where {Adj,Sty} = AdjTransBlockBandedStyle{Adj,Sty}() +AdjTransBlockBandedStyle{Adj,Sty}(::Val{N}) where {Adj,Sty,N} = DefaultArrayStyle{N}() + +const AdjointBlockBandedStyle{Sty} = AdjTransBlockBandedStyle{typeof(adjoint),Sty} +const TransposeBlockBandedStyle{Sty} = AdjTransBlockBandedStyle{typeof(transpose),Sty} + +_adjtransop(::AdjTransBlockBandedStyle{Adj}) where Adj = Adj.instance +_adjtransparentstyle(::AdjTransBlockBandedStyle{<:Any,Sty}) where Sty = Sty() + +adjtransbroadcaststyle(op, sty::AbstractBlockBandedStyle) = AdjTransBlockBandedStyle{typeof(op),typeof(sty)}() +adjtransbroadcaststyle(op, _) = DefaultArrayStyle{2}() + +BroadcastStyle(::Type{<:Adjoint{<:Any,Mat}}) where Mat<:AbstractBlockBandedMatrix = + adjtransbroadcaststyle(adjoint, BroadcastStyle(Mat)) +BroadcastStyle(::Type{<:Transpose{<:Any,Mat}}) where Mat<:AbstractBlockBandedMatrix = + adjtransbroadcaststyle(transpose, BroadcastStyle(Mat)) + +# combining with any other style drops down to the style of the parents, that is, +# the result is materialised as a block-banded matrix instead of its adjoint. +# Only one order needs to be declared as broadcasting tests both. +BroadcastStyle(S::AdjTransBlockBandedStyle, T::AbstractArrayStyle{2}) = + Base.Broadcast.result_style(_adjtransparentstyle(S), T) +BroadcastStyle(::AdjTransBlockBandedStyle, T::DefaultArrayStyle{2}) = T +BroadcastStyle(S::AdjTransBlockBandedStyle, T::AdjTransBlockBandedStyle) = + Base.Broadcast.result_style(_adjtransparentstyle(S), _adjtransparentstyle(T)) +BroadcastStyle(S::AdjTransBlockBandedStyle{Adj}, T::AdjTransBlockBandedStyle{Adj}) where Adj = + AdjTransBlockBandedStyle{Adj,typeof(Base.Broadcast.result_style(_adjtransparentstyle(S), _adjtransparentstyle(T)))}() + +# functions with real Taylor coefficients satisfy conj(f(x...)) == f(conj.(x)...) +_conjequivariant(_) = false +_conjequivariant(::Union{typeof(+),typeof(-),typeof(*),typeof(/),typeof(\)}) = true + +# an argument can be moved through the adjoint/transpose if it is a scalar or has a +# matching wrapper, where a nested broadcast must itself commute with conjugation +_adjtransable(_, _) = false +_adjtransable(_, ::Number) = true +_adjtransable(::typeof(adjoint), ::Adjoint) = true +_adjtransable(::typeof(transpose), ::Transpose) = true +_adjtransable(op, bc::Broadcasted) = _conjequivariant(bc.f) && all(map(x -> _adjtransable(op, x), bc.args)) + +_adjtransarg(::typeof(adjoint), α::Number) = conj(α) +_adjtransarg(::typeof(transpose), α::Number) = α +_adjtransarg(::typeof(adjoint), A::Adjoint) = parent(A) +_adjtransarg(::typeof(transpose), A::Transpose) = parent(A) +_adjtransarg(op, bc::Broadcasted) = broadcasted(bc.f, map(x -> _adjtransarg(op, x), bc.args)...) + +function copy(bc::Broadcasted{<:AdjTransBlockBandedStyle}) + op = _adjtransop(BroadcastStyle(typeof(bc))) + if _conjequivariant(bc.f) && all(map(x -> _adjtransable(op, x), bc.args)) + op(Base.Broadcast.materialize(broadcasted(bc.f, map(x -> _adjtransarg(op, x), bc.args)...))) + else # e.g. exp.(A') is not block-banded + copy(Broadcasted{DefaultArrayStyle{2}}(bc.f, bc.args, bc.axes)) + end +end + +copyto!(dest::AbstractArray, bc::Broadcasted{<:AdjTransBlockBandedStyle}) = + copyto!(dest, Broadcasted{DefaultArrayStyle{2}}(bc.f, bc.args, bc.axes)) + +similar(bc::Broadcasted{<:AdjTransBlockBandedStyle}, ::Type{T}) where T = + similar(Broadcasted{DefaultArrayStyle{2}}(bc.f, bc.args, bc.axes), T) + + ### # broadcast blockbandwidths ### diff --git a/test/test_broadcasting.jl b/test/test_broadcasting.jl index 06758fb..66bee54 100644 --- a/test/test_broadcasting.jl +++ b/test/test_broadcasting.jl @@ -297,6 +297,51 @@ import Base: oneto bbw = @inferred blockbandwidths(bc) @test bbw == blockbandwidths(BB) end + + @testset "adjoint/transpose" begin + A = BandedBlockBandedMatrix(randn(ComplexF64,10,10), 1:4,1:4, (1,1), (1,1)) + B = BlockBandedMatrix(randn(ComplexF64,10,10), 1:4,1:4, (1,1)) + α = 2+im + + @testset "$op" for op in (adjoint, transpose) + Wrap = op === adjoint ? Adjoint : Transpose + @testset "$(typeof(M).name.name)" for M in (A, B) + W, Wm = op(M), op(Matrix(M)) + for (R,Rm) in ((α .* W, α .* Wm), (W .* α, Wm .* α), (W ./ α, Wm ./ α), + (α .\ W, α .\ Wm), ((-).(W), (-).(Wm)), + (W .+ W, Wm .+ Wm), (W .- W, Wm .- Wm), + (α .* W .+ W, α .* Wm .+ Wm)) + @test R isa Wrap{ComplexF64} + @test parent(R) isa typeof(M).name.wrapper + @test R ≈ Rm + end + end + @test op(A) .+ op(B) isa Wrap{ComplexF64} + @test parent(op(A) .+ op(B)) isa BlockBandedMatrix + @test op(A) .+ op(B) ≈ op(Matrix(A)) .+ op(Matrix(B)) + end + + # the wrapper is dropped when it cannot be moved through the broadcast + @test exp.(A') ≈ exp.(Matrix(A)') + @test !(exp.(A') isa Adjoint) + @test imag.(A') ≈ imag.(Matrix(A)') + @test !(imag.(A') isa Adjoint) + @test A' .+ transpose(A) ≈ Matrix(A)' .+ transpose(Matrix(A)) + @test !(A' .+ transpose(A) isa Adjoint) + @test A' .+ A ≈ Matrix(A)' .+ Matrix(A) + @test A' .* ones(10,10) ≈ Matrix(A)' + + f(α, A) = α .* A' + @test @inferred(f(α, A)) isa Adjoint{ComplexF64,<:BandedBlockBandedMatrix} + @test f(α, A) ≈ α .* Matrix(A)' + + C = BandedBlockBandedMatrix{ComplexF64}(undef, 1:4,1:4, (1,1), (1,1)) + C .= α .* A' + @test C ≈ α .* Matrix(A)' + D = Matrix{ComplexF64}(undef, 10, 10) + D .= α .* A' + @test D ≈ α .* Matrix(A)' + end end end # module From 36abe72d3df89ce0fe2e9d681635735fae3af23f Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Tue, 22 Sep 2026 13:39:17 +0100 Subject: [PATCH 2/6] lower broadcasting to data --- src/BandedBlockBandedMatrix.jl | 43 ++++++++++++++++++++++++++++ src/broadcast.jl | 13 +++++---- test/test_broadcasting.jl | 52 ++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/BandedBlockBandedMatrix.jl b/src/BandedBlockBandedMatrix.jl index 8892187..9c290f8 100644 --- a/src/BandedBlockBandedMatrix.jl +++ b/src/BandedBlockBandedMatrix.jl @@ -616,6 +616,49 @@ rmul!(A::BandedBlockBandedMatrix, x::Number) = (rmul!(A.data, x); A) *(A::BandedBlockBandedMatrix, x::Number) = _BandedBlockBandedMatrix(A.data*x, axes(A,1), blockbandwidths(A), subblockbandwidths(A)) +### +# broadcasting is passed through to the data +# +# The data of a `BandedBlockBandedMatrix` stores every entry inside the bands, so a +# broadcast whose arguments all share the same block structure can be applied to the data +# directly, e.g. `α .* A .+ B` is `α .* A.data .+ B.data` reinterpreted as a +# `BandedBlockBandedMatrix`. This needs the result to vanish outside the bands, where the +# data stores junk that is never read. +### + +# the blocking of the data plays no role in an elementwise operation, so we broadcast over +# the flat array underlying it +_flatdata(A::BandedBlockBandedMatrix) = parent(bandedblockbandeddata(A)) + +_isdatabroadcast(bc::Broadcasted) = _isbroadcastarith(bc.f) && _isweakzero(bc.f, bc.args...) + +_isdataarg(_, _) = false +_isdataarg(_, ::Number) = true +_isdataarg(A, B::BandedBlockBandedMatrix) = bandedblockbandeddata(B) isa BlockedArray && + blockisequal(axes(A), axes(B)) && blockbandwidths(A) == blockbandwidths(B) && + subblockbandwidths(A) == subblockbandwidths(B) +_isdataarg(A, bc::Broadcasted) = _isdatabroadcast(bc) && all(map(x -> _isdataarg(A, x), bc.args)) + +_dataarg(x::Number) = x +_dataarg(A::BandedBlockBandedMatrix) = _flatdata(A) +_dataarg(bc::Broadcasted) = broadcasted(bc.f, map(_dataarg, bc.args)...) + +# the first BandedBlockBandedMatrix in the tree determines the structure of the result +_firstbandedblockbanded() = nothing +_firstbandedblockbanded(A::BandedBlockBandedMatrix, args...) = A +_firstbandedblockbanded(bc::Broadcasted, args...) = _firstbandedblockbanded(bc.args..., args...) +_firstbandedblockbanded(_, args...) = _firstbandedblockbanded(args...) + +function copy(bc::Broadcasted{BandedBlockBandedStyle}) + A = _firstbandedblockbanded(bc) + if !isnothing(A) && _isdataarg(A, bc) + _BandedBlockBandedMatrix(materialize(_dataarg(bc)), axes(A), blockbandwidths(A), subblockbandwidths(A)) + else # e.g. exp.(A) or matrices whose bands do not line up + invoke(copy, Tuple{Broadcasted}, bc) + end +end + + ##### # summary ##### diff --git a/src/broadcast.jl b/src/broadcast.jl index e95fe68..a5ad767 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -78,9 +78,12 @@ BroadcastStyle(S::AdjTransBlockBandedStyle, T::AdjTransBlockBandedStyle) = BroadcastStyle(S::AdjTransBlockBandedStyle{Adj}, T::AdjTransBlockBandedStyle{Adj}) where Adj = AdjTransBlockBandedStyle{Adj,typeof(Base.Broadcast.result_style(_adjtransparentstyle(S), _adjtransparentstyle(T)))}() -# functions with real Taylor coefficients satisfy conj(f(x...)) == f(conj.(x)...) -_conjequivariant(_) = false -_conjequivariant(::Union{typeof(+),typeof(-),typeof(*),typeof(/),typeof(\)}) = true +# The arithmetic operations whose broadcasts can be rewritten in terms of the underlying +# data: they commute with conjugation, so that an `Adjoint` can be moved outside of the +# broadcast, and they map zeros to zeros and the junk stored outside the bands to junk, +# so that they can be applied to the data of a `BandedBlockBandedMatrix` directly. +_isbroadcastarith(_) = false +_isbroadcastarith(::Union{typeof(+),typeof(-),typeof(*),typeof(/),typeof(\)}) = true # an argument can be moved through the adjoint/transpose if it is a scalar or has a # matching wrapper, where a nested broadcast must itself commute with conjugation @@ -88,7 +91,7 @@ _adjtransable(_, _) = false _adjtransable(_, ::Number) = true _adjtransable(::typeof(adjoint), ::Adjoint) = true _adjtransable(::typeof(transpose), ::Transpose) = true -_adjtransable(op, bc::Broadcasted) = _conjequivariant(bc.f) && all(map(x -> _adjtransable(op, x), bc.args)) +_adjtransable(op, bc::Broadcasted) = _isbroadcastarith(bc.f) && all(map(x -> _adjtransable(op, x), bc.args)) _adjtransarg(::typeof(adjoint), α::Number) = conj(α) _adjtransarg(::typeof(transpose), α::Number) = α @@ -98,7 +101,7 @@ _adjtransarg(op, bc::Broadcasted) = broadcasted(bc.f, map(x -> _adjtransarg(op, function copy(bc::Broadcasted{<:AdjTransBlockBandedStyle}) op = _adjtransop(BroadcastStyle(typeof(bc))) - if _conjequivariant(bc.f) && all(map(x -> _adjtransable(op, x), bc.args)) + if _isbroadcastarith(bc.f) && all(map(x -> _adjtransable(op, x), bc.args)) op(Base.Broadcast.materialize(broadcasted(bc.f, map(x -> _adjtransarg(op, x), bc.args)...))) else # e.g. exp.(A') is not block-banded copy(Broadcasted{DefaultArrayStyle{2}}(bc.f, bc.args, bc.axes)) diff --git a/test/test_broadcasting.jl b/test/test_broadcasting.jl index 66bee54..165b33f 100644 --- a/test/test_broadcasting.jl +++ b/test/test_broadcasting.jl @@ -342,6 +342,58 @@ import Base: oneto D .= α .* A' @test D ≈ α .* Matrix(A)' end + + @testset "data" begin + A = BandedBlockBandedMatrix{Float64}(undef, 1:4,1:4, (1,1),(1,1)) + B = BandedBlockBandedMatrix{Float64}(undef, 1:4,1:4, (1,1),(1,1)) + # the data outside the bands is junk that must not leak into the result + A.data .= NaN; B.data .= NaN + for M in (A,B), J = 1:4, K = max(1,J-1):min(4,J+1) + V = view(M, Block(K), Block(J)) + for j = axes(V,2), k = colrange(V,j) + V[k,j] = randn() + end + end + Am, Bm = Matrix(A), Matrix(B) + α = 2.0 + @test !any(isnan, Am) + + for (R,Rm) in ((α .* A, α .* Am), (A .* α, Am .* α), (A ./ α, Am ./ α), (α .\ A, α .\ Am), + ((-).(A), (-).(Am)), (A .+ B, Am .+ Bm), (A .- B, Am .- Bm), + (A .* B, Am .* Bm), (α .* A .+ B, α .* Am .+ Bm)) + @test R isa BandedBlockBandedMatrix + @test blockbandwidths(R) == (1,1) + @test subblockbandwidths(R) == (1,1) + @test Matrix(R) == Rm + end + @test typeof(α .* A) == typeof(similar(A, Float64)) + + # the data is only used when the structures line up + C = BandedBlockBandedMatrix(randn(10,10), 1:4,1:4, (2,1),(1,0)); Cm = Matrix(C) + @test A .+ C ≈ Am .+ Cm + @test blockbandwidths(A .+ C) == (2,1) + @test subblockbandwidths(A .+ C) == (1,1) + @test A .* C ≈ Am .* Cm + @test blockbandwidths(A .* C) == (1,1) + @test subblockbandwidths(A .* C) == (1,0) + @test_throws DimensionMismatch A .+ BandedBlockBandedMatrix(randn(10,14), 1:4,2:5, (1,1),(1,1)) + + # broadcasts that do not preserve the bands are unchanged + @test A .+ 1 ≈ Am .+ 1 + @test exp.(A) ≈ exp.(Am) + @test Diagonal(1:10) .* A ≈ Diagonal(1:10) .* Am + + f(α, A) = α .* A + g(A, B) = A .+ B + h(α, A, B) = α .* A .+ B + @test @inferred(f(α, A)) ≈ α .* Am + @test @inferred(g(A, B)) ≈ Am .+ Bm + @test @inferred(h(α, A, B)) ≈ α .* Am .+ Bm + + # and the data is used through an adjoint as well + @test parent((2+im) .* A') isa BandedBlockBandedMatrix + @test (2+im) .* A' ≈ (2+im) .* Am' + end end end # module From bda91f4f80660f3ba55e8f33d5b275b4b2577660 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Tue, 22 Sep 2026 18:05:30 +0100 Subject: [PATCH 3/6] move some to BandedMatrices.jl --- Project.toml | 2 +- src/BandedBlockBandedMatrix.jl | 5 +- src/BlockBandedMatrices.jl | 5 +- src/broadcast.jl | 84 ++++------------------------------ 4 files changed, 18 insertions(+), 78 deletions(-) diff --git a/Project.toml b/Project.toml index cc18e35..c902b2d 100644 --- a/Project.toml +++ b/Project.toml @@ -19,7 +19,7 @@ BlockBandedMatricesSparseArraysExt = "SparseArrays" [compat] Aqua = "0.8" ArrayLayouts = "1" -BandedMatrices = "1" +BandedMatrices = "1.13" BlockArrays = "1" Documenter = "1" FillArrays = "1" diff --git a/src/BandedBlockBandedMatrix.jl b/src/BandedBlockBandedMatrix.jl index 9c290f8..32b8ccd 100644 --- a/src/BandedBlockBandedMatrix.jl +++ b/src/BandedBlockBandedMatrix.jl @@ -634,7 +634,10 @@ _isdatabroadcast(bc::Broadcasted) = _isbroadcastarith(bc.f) && _isweakzero(bc.f, _isdataarg(_, _) = false _isdataarg(_, ::Number) = true -_isdataarg(A, B::BandedBlockBandedMatrix) = bandedblockbandeddata(B) isa BlockedArray && +# the data must also be safe to read, which it need not be for `undef` entries of a +# non-isbits eltype +_isdataarg(A, B::BandedBlockBandedMatrix) = isbitstype(eltype(B)) && + bandedblockbandeddata(B) isa BlockedArray && blockisequal(axes(A), axes(B)) && blockbandwidths(A) == blockbandwidths(B) && subblockbandwidths(A) == subblockbandwidths(B) _isdataarg(A, bc::Broadcasted) = _isdatabroadcast(bc) && all(map(x -> _isdataarg(A, x), bc.args)) diff --git a/src/BlockBandedMatrices.jl b/src/BlockBandedMatrices.jl index c9f950e..2e41c13 100644 --- a/src/BlockBandedMatrices.jl +++ b/src/BlockBandedMatrices.jl @@ -8,8 +8,9 @@ import ArrayLayouts: AbstractColumnMajor, AdjQRPackedQLayout, ColumnMajor, layout_replace_in_print_matrix, rowsupport, sub_materialize, sublayout, symmetriclayout, transposelayout, zero!, materialize!, materialize -import BandedMatrices: BandedColumns, BandedLayout, BandedStyle, BlasFloat, - _BandedMatrix, banded_getindex, banded_setindex!, +import BandedMatrices: AdjOrTransStyle, BandedColumns, BandedLayout, BandedStyle, BlasFloat, + _BandedMatrix, _isbroadcastarith, _isweakzero, adjortransstyle, + banded_getindex, banded_setindex!, bandedcolumns, bandeddata, bandshift, bandwidth, bandwidths, colrange, inbands_getindex, inbands_setindex!, isbanded diff --git a/src/broadcast.jl b/src/broadcast.jl index a5ad767..099e1e1 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -35,84 +35,22 @@ BroadcastStyle(::BandedBlockBandedStyle, ::DefaultArrayStyle{2}) = BandedBlockBa ### # Adjoints and transposes # -# The adjoint (transpose) of a block-banded matrix is not stored as a block-banded -# matrix, so broadcasting is done on the parent and the result re-wrapped, e.g. -# `α .* A'` returns an `Adjoint` of a block-banded matrix. This is valid whenever the -# broadcasted function commutes with conjugation, as `f.(A', α) == f.(A, conj(α))'`. +# The adjoint (transpose) of a block-banded matrix is not stored as a block-banded matrix, +# so we use the `AdjOrTransStyle` machinery of BandedMatrices.jl to broadcast over the +# parents and re-wrap the result, e.g. `α .* A'` returns an `Adjoint` of a block-banded +# matrix instead of a dense matrix. ### -""" - AdjTransBlockBandedStyle{Adj,Sty} - -is a `BroadcastStyle` for an `Adjoint` (`Adj == typeof(adjoint)`) or `Transpose` -(`Adj == typeof(transpose)`) of a block-banded matrix whose parent has -`BroadcastStyle` `Sty`. -""" -struct AdjTransBlockBandedStyle{Adj,Sty} <: AbstractArrayStyle{2} end - -AdjTransBlockBandedStyle{Adj,Sty}(::Val{2}) where {Adj,Sty} = AdjTransBlockBandedStyle{Adj,Sty}() -AdjTransBlockBandedStyle{Adj,Sty}(::Val{N}) where {Adj,Sty,N} = DefaultArrayStyle{N}() - -const AdjointBlockBandedStyle{Sty} = AdjTransBlockBandedStyle{typeof(adjoint),Sty} -const TransposeBlockBandedStyle{Sty} = AdjTransBlockBandedStyle{typeof(transpose),Sty} - -_adjtransop(::AdjTransBlockBandedStyle{Adj}) where Adj = Adj.instance -_adjtransparentstyle(::AdjTransBlockBandedStyle{<:Any,Sty}) where Sty = Sty() - -adjtransbroadcaststyle(op, sty::AbstractBlockBandedStyle) = AdjTransBlockBandedStyle{typeof(op),typeof(sty)}() -adjtransbroadcaststyle(op, _) = DefaultArrayStyle{2}() +adjortransstyle(op, sty::AbstractBlockBandedStyle) = AdjOrTransStyle{typeof(op),typeof(sty)}() BroadcastStyle(::Type{<:Adjoint{<:Any,Mat}}) where Mat<:AbstractBlockBandedMatrix = - adjtransbroadcaststyle(adjoint, BroadcastStyle(Mat)) + adjortransstyle(adjoint, BroadcastStyle(Mat)) BroadcastStyle(::Type{<:Transpose{<:Any,Mat}}) where Mat<:AbstractBlockBandedMatrix = - adjtransbroadcaststyle(transpose, BroadcastStyle(Mat)) - -# combining with any other style drops down to the style of the parents, that is, -# the result is materialised as a block-banded matrix instead of its adjoint. -# Only one order needs to be declared as broadcasting tests both. -BroadcastStyle(S::AdjTransBlockBandedStyle, T::AbstractArrayStyle{2}) = - Base.Broadcast.result_style(_adjtransparentstyle(S), T) -BroadcastStyle(::AdjTransBlockBandedStyle, T::DefaultArrayStyle{2}) = T -BroadcastStyle(S::AdjTransBlockBandedStyle, T::AdjTransBlockBandedStyle) = - Base.Broadcast.result_style(_adjtransparentstyle(S), _adjtransparentstyle(T)) -BroadcastStyle(S::AdjTransBlockBandedStyle{Adj}, T::AdjTransBlockBandedStyle{Adj}) where Adj = - AdjTransBlockBandedStyle{Adj,typeof(Base.Broadcast.result_style(_adjtransparentstyle(S), _adjtransparentstyle(T)))}() - -# The arithmetic operations whose broadcasts can be rewritten in terms of the underlying -# data: they commute with conjugation, so that an `Adjoint` can be moved outside of the -# broadcast, and they map zeros to zeros and the junk stored outside the bands to junk, -# so that they can be applied to the data of a `BandedBlockBandedMatrix` directly. -_isbroadcastarith(_) = false -_isbroadcastarith(::Union{typeof(+),typeof(-),typeof(*),typeof(/),typeof(\)}) = true - -# an argument can be moved through the adjoint/transpose if it is a scalar or has a -# matching wrapper, where a nested broadcast must itself commute with conjugation -_adjtransable(_, _) = false -_adjtransable(_, ::Number) = true -_adjtransable(::typeof(adjoint), ::Adjoint) = true -_adjtransable(::typeof(transpose), ::Transpose) = true -_adjtransable(op, bc::Broadcasted) = _isbroadcastarith(bc.f) && all(map(x -> _adjtransable(op, x), bc.args)) - -_adjtransarg(::typeof(adjoint), α::Number) = conj(α) -_adjtransarg(::typeof(transpose), α::Number) = α -_adjtransarg(::typeof(adjoint), A::Adjoint) = parent(A) -_adjtransarg(::typeof(transpose), A::Transpose) = parent(A) -_adjtransarg(op, bc::Broadcasted) = broadcasted(bc.f, map(x -> _adjtransarg(op, x), bc.args)...) - -function copy(bc::Broadcasted{<:AdjTransBlockBandedStyle}) - op = _adjtransop(BroadcastStyle(typeof(bc))) - if _isbroadcastarith(bc.f) && all(map(x -> _adjtransable(op, x), bc.args)) - op(Base.Broadcast.materialize(broadcasted(bc.f, map(x -> _adjtransarg(op, x), bc.args)...))) - else # e.g. exp.(A') is not block-banded - copy(Broadcasted{DefaultArrayStyle{2}}(bc.f, bc.args, bc.axes)) - end -end + adjortransstyle(transpose, BroadcastStyle(Mat)) -copyto!(dest::AbstractArray, bc::Broadcasted{<:AdjTransBlockBandedStyle}) = - copyto!(dest, Broadcasted{DefaultArrayStyle{2}}(bc.f, bc.args, bc.axes)) - -similar(bc::Broadcasted{<:AdjTransBlockBandedStyle}, ::Type{T}) where T = - similar(Broadcasted{DefaultArrayStyle{2}}(bc.f, bc.args, bc.axes), T) +# mixing with a dense array is not supported by the block-banded machinery, as the block +# bandwidths of an unblocked array are not known, so we materialise densely as before +BroadcastStyle(::AdjOrTransStyle{<:Any,<:AbstractBlockBandedStyle}, T::DefaultArrayStyle{2}) = T ### @@ -190,8 +128,6 @@ end # zero is preserved. Take the maximum bandwidth -import BandedMatrices: _isweakzero - function blockbandwidths(bc::Broadcasted) (a,b) = size(bc) bnds = (a-1,b-1) From 3e04bbeaf69e8a3a528a4358e33e1522022af6fc Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Tue, 22 Sep 2026 18:48:38 +0100 Subject: [PATCH 4/6] simplify broadcasting lowering to data --- src/BandedBlockBandedMatrix.jl | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/BandedBlockBandedMatrix.jl b/src/BandedBlockBandedMatrix.jl index 32b8ccd..fc367d0 100644 --- a/src/BandedBlockBandedMatrix.jl +++ b/src/BandedBlockBandedMatrix.jl @@ -626,9 +626,6 @@ rmul!(A::BandedBlockBandedMatrix, x::Number) = (rmul!(A.data, x); A) # data stores junk that is never read. ### -# the blocking of the data plays no role in an elementwise operation, so we broadcast over -# the flat array underlying it -_flatdata(A::BandedBlockBandedMatrix) = parent(bandedblockbandeddata(A)) _isdatabroadcast(bc::Broadcasted) = _isbroadcastarith(bc.f) && _isweakzero(bc.f, bc.args...) @@ -637,13 +634,12 @@ _isdataarg(_, ::Number) = true # the data must also be safe to read, which it need not be for `undef` entries of a # non-isbits eltype _isdataarg(A, B::BandedBlockBandedMatrix) = isbitstype(eltype(B)) && - bandedblockbandeddata(B) isa BlockedArray && blockisequal(axes(A), axes(B)) && blockbandwidths(A) == blockbandwidths(B) && subblockbandwidths(A) == subblockbandwidths(B) _isdataarg(A, bc::Broadcasted) = _isdatabroadcast(bc) && all(map(x -> _isdataarg(A, x), bc.args)) _dataarg(x::Number) = x -_dataarg(A::BandedBlockBandedMatrix) = _flatdata(A) +_dataarg(A::BandedBlockBandedMatrix) = bandedblockbandeddata(A) _dataarg(bc::Broadcasted) = broadcasted(bc.f, map(_dataarg, bc.args)...) # the first BandedBlockBandedMatrix in the tree determines the structure of the result From 88145a06fb348e88eac87974cd46dff3feca614d Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Tue, 22 Sep 2026 20:26:29 +0100 Subject: [PATCH 5/6] Update test_broadcasting.jl --- test/test_broadcasting.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/test_broadcasting.jl b/test/test_broadcasting.jl index 165b33f..296b83b 100644 --- a/test/test_broadcasting.jl +++ b/test/test_broadcasting.jl @@ -387,8 +387,11 @@ import Base: oneto g(A, B) = A .+ B h(α, A, B) = α .* A .+ B @test @inferred(f(α, A)) ≈ α .* Am - @test @inferred(g(A, B)) ≈ Am .+ Bm - @test @inferred(h(α, A, B)) ≈ α .* Am .+ Bm + # Following can't be inferred because of issues in type-inferrence + # see Base.Broadcast.axistype overload in + # BlockArrays/src/blockbroadcast.jl:45 + @test g(A, B) ≈ Am .+ Bm + @test h(α, A, B) ≈ α .* Am .+ Bm # and the data is used through an adjoint as well @test parent((2+im) .* A') isa BandedBlockBandedMatrix From acd3ee1609a3ea7d839fd4dd54a28f668a79ad66 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Tue, 22 Sep 2026 20:39:54 +0100 Subject: [PATCH 6/6] v0.13.6 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index c902b2d..55911dd 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "BlockBandedMatrices" uuid = "ffab5731-97b5-5995-9138-79e8c1846df0" -version = "0.13.5" +version = "0.13.6" [deps] ArrayLayouts = "4c555306-a7a7-4459-81d9-ec55ddd5c99a"