From 3f0724d5ee953f2b843a0ad986be8bb92cdc95a7 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 5 Aug 2026 09:36:55 -0400 Subject: [PATCH 1/5] Flatten BiTuple through generic array machinery Lets an `AbstractArray` whose `axes` return a `BiTuple` flow through generic array machinery. `BiTuple` already acts like its flat tuple for iteration and indexing, so this bridges the remaining Base entry points (index sets, shape and broadcast checks, reshape/permute tuple helpers, `show`) and TensorAlgebra's `matricize` helpers to flatten it. Single-argument `map` preserves the split. --- Project.toml | 2 +- src/bituple.jl | 62 ++++++++++++++++++++++++++++++++++++++++ src/linearbroadcasted.jl | 6 ++-- test/test_bituple.jl | 10 +++++++ 4 files changed, 77 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index db4a3287..d0e9aaf6 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "TensorAlgebra" uuid = "68bd88dc-f39d-4e12-b2ca-f046b68fcc6a" -version = "0.19.0" +version = "0.19.1" authors = ["ITensor developers and contributors"] [workspace] diff --git a/src/bituple.jl b/src/bituple.jl index 6aa1e8f4..78ece2cf 100644 --- a/src/bituple.jl +++ b/src/bituple.jl @@ -30,10 +30,72 @@ function Base.show(io::IO, bt::BiTuple) end Base.:(==)(a::BiTuple, b::BiTuple) = a.t1 == b.t1 && a.t2 == b.t2 +# Compare flat against a plain tuple, so `axes(a) == (r1, r2)` works when `axes` returns a `BiTuple`. +Base.:(==)(bt::BiTuple, t::Tuple) = Tuple(bt) == t +Base.:(==)(t::Tuple, bt::BiTuple) = t == Tuple(bt) Base.hash(bt::BiTuple, h::UInt) = hash(bt.t2, hash(bt.t1, hash(:BiTuple, h))) Base.invperm(bt::BiTuple{N1}) where {N1} = BiTuple(invperm(Tuple(bt)), Val(N1)) +# An `AbstractArray` whose `axes` is a `BiTuple` reaches a range of generic machinery (Base indexing, +# Base broadcasting/shape, and the matricize/permute helpers here) that assumes a flat tuple of axes. +# Index sets, bounds, shapes, and permutations all ignore the codomain/domain split, so flatten the +# `BiTuple` first. This is the same collapse-to-flat behavior mixed operations fall back to. +Base.LinearIndices(bt::BiTuple) = LinearIndices(Tuple(bt)) +Base.CartesianIndices(bt::BiTuple) = CartesianIndices(Tuple(bt)) +function Base.checkbounds_indices(::Type{Bool}, bt::BiTuple, I::Tuple) + return Base.checkbounds_indices(Bool, Tuple(bt), I) +end +Base.promote_shape(a::BiTuple, b::Tuple) = Base.promote_shape(Tuple(a), b) +Base.promote_shape(a::Tuple, b::BiTuple) = Base.promote_shape(a, Tuple(b)) +Base.promote_shape(a::BiTuple, b::BiTuple) = Base.promote_shape(Tuple(a), Tuple(b)) +function Base.Broadcast.broadcast_shape(a::BiTuple, shapes...) + return Base.Broadcast.broadcast_shape(Tuple(a), shapes...) +end +function Base.Broadcast.broadcast_shape(a::Tuple, b::BiTuple, shapes...) + return Base.Broadcast.broadcast_shape(a, Tuple(b), shapes...) +end +function Base.Broadcast.check_broadcast_shape(a::BiTuple, b::Tuple{}) + return Base.Broadcast.check_broadcast_shape(Tuple(a), b) +end +function Base.Broadcast.check_broadcast_shape(a::BiTuple, b::Tuple) + return Base.Broadcast.check_broadcast_shape(Tuple(a), b) +end +function Base.Broadcast.check_broadcast_shape(a, b::BiTuple) + return Base.Broadcast.check_broadcast_shape(a, Tuple(b)) +end +function Base.Broadcast.check_broadcast_shape(a::BiTuple, b::BiTuple) + return Base.Broadcast.check_broadcast_shape(Tuple(a), Tuple(b)) +end +function Base._show_nonempty( + io::IO, + X::AbstractMatrix, + prefix::String, + drop::Bool, + axs::BiTuple + ) + return Base._show_nonempty(io, X, prefix, drop, Tuple(axs)) +end +function Base.PermutedDimsArrays.genperm(bt::BiTuple, perm::NTuple{N, Int}) where {N} + return Base.PermutedDimsArrays.genperm(Tuple(bt), perm) +end +Base.tail(bt::BiTuple) = Base.tail(Tuple(bt)) +Base.front(bt::BiTuple) = Base.front(Tuple(bt)) +Base.lastindex(bt::BiTuple) = Base.lastindex(Tuple(bt)) +# A single-argument `map` preserves the split, mapping each block: with one operand there is no +# ambiguity about which split to keep. (The multi-argument case is the ambiguous one and is left to +# collapse to the flat tuple.) +Base.map(f, bt::BiTuple) = BiTuple(map(f, bt.t1), map(f, bt.t2)) +Base.safe_tail(bt::BiTuple) = Base.safe_tail(Tuple(bt)) +Base.rdims(out::Val{N}, bt::BiTuple) where {N} = Base.rdims(out, Tuple(bt)) +Base.offset_if_vec(i::Integer, axs::BiTuple) = Base.offset_if_vec(i, Tuple(axs)) +Base.Broadcast._axes(bc::Base.Broadcast.Broadcasted, axs::BiTuple) = Tuple(axs) +bipartition_axes(bt::BiTuple, split...) = bipartition_axes(Tuple(bt), split...) +bipartition(bt::BiTuple, length1::Val) = bipartition(Tuple(bt), length1) +function bipartition(bt::BiTuple, group1::Tuple, group2::Tuple) + return bipartition(Tuple(bt), group1, group2) +end + """ bipartition(t::Tuple, length1::Val) -> (t1, t2) bipartition(t::Tuple, group1::Tuple, group2::Tuple) -> (p1, p2) diff --git a/src/linearbroadcasted.jl b/src/linearbroadcasted.jl index 4d113091..ebebf8b2 100644 --- a/src/linearbroadcasted.jl +++ b/src/linearbroadcasted.jl @@ -128,11 +128,13 @@ addends(a::AddBroadcasted) = a.args # differing shapes), so combine by verifying equality through `axes` (TensorAlgebra's, which # works for a non-`AbstractArray` backend like a `TensorMap`) rather than Base's `combine_axes`, # which would call `Base.axes`/`Base.size` on the operands. A mismatch (e.g. a half-conjugated -# `conj.(a) .- b`, whose dualized and non-dualized axes differ) throws here. +# `conj.(a) .- b`, whose dualized and non-dualized axes differ) throws here. Comparison is on the +# flat axes (`Tuple`), so operands that agree on the flat legs but differ in a codomain/domain split +# (`BiTuple`) still combine, matching the split-collapsing result. function Base.axes(a::AddBroadcasted) axs = map(axes, addends(a)) ax = first(axs) - all(x -> x == ax, axs) || + all(x -> Tuple(x) == Tuple(ax), axs) || throw(DimensionMismatch("linear-combination operands have mismatched axes: $axs")) return ax end diff --git a/test/test_bituple.jl b/test/test_bituple.jl index 9cb91b80..5354bcc8 100644 --- a/test/test_bituple.jl +++ b/test/test_bituple.jl @@ -28,6 +28,16 @@ using TestExtras: @constinferred # Equality compares the two blocks. @test BiTuple((1, 2), (3,)) == BiTuple((1, 2), (3,)) @test BiTuple((1, 2), (3,)) != BiTuple((1,), (2, 3)) + + # Equality against a plain tuple compares the flattened form. + @test BiTuple((1, 2), (3,)) == (1, 2, 3) + @test (1, 2, 3) == BiTuple((1, 2), (3,)) + @test BiTuple((1, 2), (3,)) != (1, 2) + # A plain-tuple comparison ignores the split, unlike a `BiTuple`-vs-`BiTuple` comparison. + @test BiTuple((1, 2), (3,)) == (1, 2, 3) == BiTuple((1,), (2, 3)) + + # Single-argument `map` preserves the split (no ambiguity with one operand). + @test (@constinferred map(x -> x + 1, BiTuple((1, 2), (3,)))) == BiTuple((2, 3), (4,)) end @testset "biperm" begin From e5b13358892c4eb098c800a3e85680ff7bc917b6 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 5 Aug 2026 15:21:15 -0400 Subject: [PATCH 2/5] Make BiTuple equality flat and drop the broadcast-shape bridges Equality and hash now ignore the codomain/domain split, matching how array equality ignores block partitioning. Broadcast and array-arithmetic shape combining is owned by the consumer instead, so the `broadcast_shape`, `check_broadcast_shape`, and `promote_shape` bridges are no longer needed. Co-Authored-By: Claude Opus 4.8 --- src/bituple.jl | 47 +++++++++------------------------------- src/linearbroadcasted.jl | 8 +++---- test/test_bituple.jl | 17 +++++++++------ 3 files changed, 24 insertions(+), 48 deletions(-) diff --git a/src/bituple.jl b/src/bituple.jl index 78ece2cf..87005734 100644 --- a/src/bituple.jl +++ b/src/bituple.jl @@ -29,11 +29,14 @@ function Base.show(io::IO, bt::BiTuple) return print(io, "BiTuple(", bt.t1, ", ", bt.t2, ")") end -Base.:(==)(a::BiTuple, b::BiTuple) = a.t1 == b.t1 && a.t2 == b.t2 -# Compare flat against a plain tuple, so `axes(a) == (r1, r2)` works when `axes` returns a `BiTuple`. +# Equality ignores the split: it compares the flattened contents, so a `(2, 0)` and a `(1, 1)` split +# with the same flat entries are equal, and a `BiTuple` equals the plain tuple it flattens to. This +# mirrors how comparing arrays ignores how they are partitioned into blocks. `hash` matches, hashing +# the flat form so the equality relation stays hash-consistent. +Base.:(==)(a::BiTuple, b::BiTuple) = Tuple(a) == Tuple(b) Base.:(==)(bt::BiTuple, t::Tuple) = Tuple(bt) == t Base.:(==)(t::Tuple, bt::BiTuple) = t == Tuple(bt) -Base.hash(bt::BiTuple, h::UInt) = hash(bt.t2, hash(bt.t1, hash(:BiTuple, h))) +Base.hash(bt::BiTuple, h::UInt) = hash(Tuple(bt), h) Base.invperm(bt::BiTuple{N1}) where {N1} = BiTuple(invperm(Tuple(bt)), Val(N1)) @@ -46,50 +49,20 @@ Base.CartesianIndices(bt::BiTuple) = CartesianIndices(Tuple(bt)) function Base.checkbounds_indices(::Type{Bool}, bt::BiTuple, I::Tuple) return Base.checkbounds_indices(Bool, Tuple(bt), I) end -Base.promote_shape(a::BiTuple, b::Tuple) = Base.promote_shape(Tuple(a), b) -Base.promote_shape(a::Tuple, b::BiTuple) = Base.promote_shape(a, Tuple(b)) -Base.promote_shape(a::BiTuple, b::BiTuple) = Base.promote_shape(Tuple(a), Tuple(b)) -function Base.Broadcast.broadcast_shape(a::BiTuple, shapes...) - return Base.Broadcast.broadcast_shape(Tuple(a), shapes...) -end -function Base.Broadcast.broadcast_shape(a::Tuple, b::BiTuple, shapes...) - return Base.Broadcast.broadcast_shape(a, Tuple(b), shapes...) -end -function Base.Broadcast.check_broadcast_shape(a::BiTuple, b::Tuple{}) - return Base.Broadcast.check_broadcast_shape(Tuple(a), b) -end -function Base.Broadcast.check_broadcast_shape(a::BiTuple, b::Tuple) - return Base.Broadcast.check_broadcast_shape(Tuple(a), b) -end -function Base.Broadcast.check_broadcast_shape(a, b::BiTuple) - return Base.Broadcast.check_broadcast_shape(a, Tuple(b)) -end -function Base.Broadcast.check_broadcast_shape(a::BiTuple, b::BiTuple) - return Base.Broadcast.check_broadcast_shape(Tuple(a), Tuple(b)) -end -function Base._show_nonempty( - io::IO, - X::AbstractMatrix, - prefix::String, - drop::Bool, - axs::BiTuple - ) - return Base._show_nonempty(io, X, prefix, drop, Tuple(axs)) -end function Base.PermutedDimsArrays.genperm(bt::BiTuple, perm::NTuple{N, Int}) where {N} return Base.PermutedDimsArrays.genperm(Tuple(bt), perm) end Base.tail(bt::BiTuple) = Base.tail(Tuple(bt)) Base.front(bt::BiTuple) = Base.front(Tuple(bt)) Base.lastindex(bt::BiTuple) = Base.lastindex(Tuple(bt)) +Base.safe_tail(bt::BiTuple) = Base.safe_tail(Tuple(bt)) +# In-place `dest .= src` into a `BiTuple`-axed destination carries those axes on the `Broadcasted`; +# report the flat tuple so the generic element-wise `copyto!` can consume them. +Base.Broadcast._axes(bc::Base.Broadcast.Broadcasted, axs::BiTuple) = Tuple(axs) # A single-argument `map` preserves the split, mapping each block: with one operand there is no # ambiguity about which split to keep. (The multi-argument case is the ambiguous one and is left to # collapse to the flat tuple.) Base.map(f, bt::BiTuple) = BiTuple(map(f, bt.t1), map(f, bt.t2)) -Base.safe_tail(bt::BiTuple) = Base.safe_tail(Tuple(bt)) -Base.rdims(out::Val{N}, bt::BiTuple) where {N} = Base.rdims(out, Tuple(bt)) -Base.offset_if_vec(i::Integer, axs::BiTuple) = Base.offset_if_vec(i, Tuple(axs)) -Base.Broadcast._axes(bc::Base.Broadcast.Broadcasted, axs::BiTuple) = Tuple(axs) bipartition_axes(bt::BiTuple, split...) = bipartition_axes(Tuple(bt), split...) bipartition(bt::BiTuple, length1::Val) = bipartition(Tuple(bt), length1) function bipartition(bt::BiTuple, group1::Tuple, group2::Tuple) diff --git a/src/linearbroadcasted.jl b/src/linearbroadcasted.jl index ebebf8b2..034fcae1 100644 --- a/src/linearbroadcasted.jl +++ b/src/linearbroadcasted.jl @@ -128,13 +128,13 @@ addends(a::AddBroadcasted) = a.args # differing shapes), so combine by verifying equality through `axes` (TensorAlgebra's, which # works for a non-`AbstractArray` backend like a `TensorMap`) rather than Base's `combine_axes`, # which would call `Base.axes`/`Base.size` on the operands. A mismatch (e.g. a half-conjugated -# `conj.(a) .- b`, whose dualized and non-dualized axes differ) throws here. Comparison is on the -# flat axes (`Tuple`), so operands that agree on the flat legs but differ in a codomain/domain split -# (`BiTuple`) still combine, matching the split-collapsing result. +# `conj.(a) .- b`, whose dualized and non-dualized axes differ) throws here. Axis equality ignores +# the codomain/domain split, so operands that agree on the flat legs but differ in split still +# combine, matching the split-collapsing result. function Base.axes(a::AddBroadcasted) axs = map(axes, addends(a)) ax = first(axs) - all(x -> Tuple(x) == Tuple(ax), axs) || + all(==(ax), axs) || throw(DimensionMismatch("linear-combination operands have mismatched axes: $axs")) return ax end diff --git a/test/test_bituple.jl b/test/test_bituple.jl index 5354bcc8..3b44d766 100644 --- a/test/test_bituple.jl +++ b/test/test_bituple.jl @@ -25,19 +25,22 @@ using TestExtras: @constinferred # Split constructor: split a flat tuple at the given codomain length. @test (@constinferred BiTuple((3, 4, 5, 2, 1), Val(3))) == BiTuple((3, 4, 5), (2, 1)) - # Equality compares the two blocks. + # Equality ignores the split, comparing the flattened contents: same flat entries compare equal + # even under a different codomain/domain split, the way array equality ignores block partitioning. @test BiTuple((1, 2), (3,)) == BiTuple((1, 2), (3,)) - @test BiTuple((1, 2), (3,)) != BiTuple((1,), (2, 3)) + @test BiTuple((1, 2), (3,)) == BiTuple((1,), (2, 3)) + @test BiTuple((1, 2), (3,)) != BiTuple((1, 3), (2,)) - # Equality against a plain tuple compares the flattened form. + # A `BiTuple` also equals the plain tuple it flattens to. @test BiTuple((1, 2), (3,)) == (1, 2, 3) @test (1, 2, 3) == BiTuple((1, 2), (3,)) @test BiTuple((1, 2), (3,)) != (1, 2) - # A plain-tuple comparison ignores the split, unlike a `BiTuple`-vs-`BiTuple` comparison. - @test BiTuple((1, 2), (3,)) == (1, 2, 3) == BiTuple((1,), (2, 3)) + # `hash` matches equality (hashes the flat form). + @test hash(BiTuple((1, 2), (3,))) == hash(BiTuple((1,), (2, 3))) == hash((1, 2, 3)) - # Single-argument `map` preserves the split (no ambiguity with one operand). - @test (@constinferred map(x -> x + 1, BiTuple((1, 2), (3,)))) == BiTuple((2, 3), (4,)) + # Single-argument `map` preserves the split (no ambiguity with one operand). Checked with `===` + # since `==` ignores the split and so would not catch a collapse to the flat form. + @test (@constinferred map(x -> x + 1, BiTuple((1, 2), (3,)))) === BiTuple((2, 3), (4,)) end @testset "biperm" begin From d44c7e8b4a3451bb21fc21589dc8c9b92c7906f7 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 5 Aug 2026 16:41:56 -0400 Subject: [PATCH 3/5] Drop the BiTuple indexing and in-place-broadcast bridges `safe_tail`, `tail`, `front`, `lastindex`, and the broadcast `_axes` existed only to flatten a `BiTuple` for Base's index and in-place-broadcast machinery. Their consumers are now handled in the consuming package (block-structure `isdiag`, block-data slicing, dense-into-block writes), so these are no longer reached. Co-Authored-By: Claude Opus 4.8 --- src/bituple.jl | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/bituple.jl b/src/bituple.jl index 87005734..3ac5359e 100644 --- a/src/bituple.jl +++ b/src/bituple.jl @@ -40,10 +40,9 @@ Base.hash(bt::BiTuple, h::UInt) = hash(Tuple(bt), h) Base.invperm(bt::BiTuple{N1}) where {N1} = BiTuple(invperm(Tuple(bt)), Val(N1)) -# An `AbstractArray` whose `axes` is a `BiTuple` reaches a range of generic machinery (Base indexing, -# Base broadcasting/shape, and the matricize/permute helpers here) that assumes a flat tuple of axes. -# Index sets, bounds, shapes, and permutations all ignore the codomain/domain split, so flatten the -# `BiTuple` first. This is the same collapse-to-flat behavior mixed operations fall back to. +# An `AbstractArray` whose `axes` is a `BiTuple` reaches Base indexing and the matricize/permute +# helpers here, which assume a flat tuple of axes. Index sets, bounds, and permutations all ignore the +# codomain/domain split, so flatten the `BiTuple` first. Base.LinearIndices(bt::BiTuple) = LinearIndices(Tuple(bt)) Base.CartesianIndices(bt::BiTuple) = CartesianIndices(Tuple(bt)) function Base.checkbounds_indices(::Type{Bool}, bt::BiTuple, I::Tuple) @@ -52,13 +51,6 @@ end function Base.PermutedDimsArrays.genperm(bt::BiTuple, perm::NTuple{N, Int}) where {N} return Base.PermutedDimsArrays.genperm(Tuple(bt), perm) end -Base.tail(bt::BiTuple) = Base.tail(Tuple(bt)) -Base.front(bt::BiTuple) = Base.front(Tuple(bt)) -Base.lastindex(bt::BiTuple) = Base.lastindex(Tuple(bt)) -Base.safe_tail(bt::BiTuple) = Base.safe_tail(Tuple(bt)) -# In-place `dest .= src` into a `BiTuple`-axed destination carries those axes on the `Broadcasted`; -# report the flat tuple so the generic element-wise `copyto!` can consume them. -Base.Broadcast._axes(bc::Base.Broadcast.Broadcasted, axs::BiTuple) = Tuple(axs) # A single-argument `map` preserves the split, mapping each block: with one operand there is no # ambiguity about which split to keep. (The multi-argument case is the ambiguous one and is left to # collapse to the flat tuple.) From d556d6a9775ad1a52666e4c333bb8e90fdbb3746 Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 5 Aug 2026 17:28:57 -0400 Subject: [PATCH 4/5] Document the checkbounds_indices BiTuple bridge Note that Base dispatches bounds-checking on the axes tuple, so a `BiTuple` never competes with Base's index-type specializations. Co-Authored-By: Claude Opus 4.8 --- src/bituple.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/bituple.jl b/src/bituple.jl index 3ac5359e..e167a9aa 100644 --- a/src/bituple.jl +++ b/src/bituple.jl @@ -45,6 +45,8 @@ Base.invperm(bt::BiTuple{N1}) where {N1} = BiTuple(invperm(Tuple(bt)), Val(N1)) # codomain/domain split, so flatten the `BiTuple` first. Base.LinearIndices(bt::BiTuple) = LinearIndices(Tuple(bt)) Base.CartesianIndices(bt::BiTuple) = CartesianIndices(Tuple(bt)) +# `checkbounds_indices` is Base's extension point for a custom axes container: it dispatches on the +# axes tuple, so a `BiTuple` (not a `Tuple`) never competes with Base's index-type specializations. function Base.checkbounds_indices(::Type{Bool}, bt::BiTuple, I::Tuple) return Base.checkbounds_indices(Bool, Tuple(bt), I) end From 11509c3c9fcf0b4dce35c890b833e7004801275f Mon Sep 17 00:00:00 2001 From: Matthew Fishman Date: Wed, 5 Aug 2026 22:26:25 -0400 Subject: [PATCH 5/5] Make BiTuple a plain value, not an axes container A BiTuple is a bipartition value for matricize, not an array's axes, so it should not stand in as one. Co-Authored-By: Claude Opus 4.8 --- src/bituple.jl | 13 ------------- src/matricize.jl | 4 ++-- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/bituple.jl b/src/bituple.jl index e167a9aa..17acf4cb 100644 --- a/src/bituple.jl +++ b/src/bituple.jl @@ -40,19 +40,6 @@ Base.hash(bt::BiTuple, h::UInt) = hash(Tuple(bt), h) Base.invperm(bt::BiTuple{N1}) where {N1} = BiTuple(invperm(Tuple(bt)), Val(N1)) -# An `AbstractArray` whose `axes` is a `BiTuple` reaches Base indexing and the matricize/permute -# helpers here, which assume a flat tuple of axes. Index sets, bounds, and permutations all ignore the -# codomain/domain split, so flatten the `BiTuple` first. -Base.LinearIndices(bt::BiTuple) = LinearIndices(Tuple(bt)) -Base.CartesianIndices(bt::BiTuple) = CartesianIndices(Tuple(bt)) -# `checkbounds_indices` is Base's extension point for a custom axes container: it dispatches on the -# axes tuple, so a `BiTuple` (not a `Tuple`) never competes with Base's index-type specializations. -function Base.checkbounds_indices(::Type{Bool}, bt::BiTuple, I::Tuple) - return Base.checkbounds_indices(Bool, Tuple(bt), I) -end -function Base.PermutedDimsArrays.genperm(bt::BiTuple, perm::NTuple{N, Int}) where {N} - return Base.PermutedDimsArrays.genperm(Tuple(bt), perm) -end # A single-argument `map` preserves the split, mapping each block: with one operand there is no # ambiguity about which split to keep. (The multi-argument case is the ambiguous one and is left to # collapse to the flat tuple.) diff --git a/src/matricize.jl b/src/matricize.jl index a86bf8fc..5b5a9bde 100644 --- a/src/matricize.jl +++ b/src/matricize.jl @@ -255,7 +255,7 @@ function unmatricizeperm( throw(ArgumentError("axes do not match permutation")) codomain_axes, domain_axes = bipartition_axes(axes_dest, invbiperm) a12 = unmatricize(style, m, codomain_axes, domain_axes) - biperm_dest = BiTuple(Tuple(invperm(invbiperm)), Val(length_codomain(axes_dest))) + biperm_dest = BiTuple(Tuple(invperm(invbiperm)), Val(length_codomain(invbiperm))) return bipermutedims(a12, biperm_dest) end @@ -274,7 +274,7 @@ function unmatricizeperm!( throw(ArgumentError("destination does not match permutation")) codomain_axes, domain_axes = bipartition_axes(axes(a_dest), invbiperm) a_perm = unmatricize(style, m, codomain_axes, domain_axes) - biperm_dest = BiTuple(Tuple(invperm(invbiperm)), Val(length_codomain(axes(a_dest)))) + biperm_dest = BiTuple(Tuple(invperm(invbiperm)), Val(length_codomain(invbiperm))) return bipermutedims!(a_dest, a_perm, biperm_dest) end