Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[Sectors] Non-abelian fusion #1363

Draft
wants to merge 96 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
882617d
clean start for non-abelian on main
ogauthe Mar 25, 2024
11121c9
split fusion rules test
ogauthe Mar 25, 2024
703c2f1
rename chain axis_cat
ogauthe Mar 25, 2024
9b5eb7e
rename dimension quantum_dimension
ogauthe Mar 25, 2024
d4ef71a
rename test_category.jl test_simple_categories.jl
ogauthe Mar 25, 2024
1e402c6
Merge branch 'ITensor:main' into non-abelian_fusion
ogauthe Mar 25, 2024
11a762a
test inferred quantum_dimension
ogauthe Mar 25, 2024
10ff71b
Remove isempty, length, and getindex for CategoryProduct
emstoudenmire Mar 26, 2024
c64af63
rm GradedAxesSectorsExt
ogauthe Mar 25, 2024
e6c5c49
fix category name
ogauthe Mar 25, 2024
57738e6
remove ⊕
ogauthe Mar 26, 2024
33c87e7
Merge branch 'ITensor:main' into non-abelian_fusion
ogauthe Mar 27, 2024
8bdfd31
Merge branch 'ITensor:main' into non-abelian_fusion
ogauthe Mar 28, 2024
af6b20f
tensor_product for GradedUnitRange
ogauthe Mar 27, 2024
b84772f
fix pairing
ogauthe Mar 28, 2024
9f86bf5
typo
ogauthe Mar 28, 2024
256f275
do not define methods for unused Vector{<:AbstractCategory}
ogauthe Mar 28, 2024
7c575e0
test different categories cannot be fused
ogauthe Mar 28, 2024
f695c5f
define SymmetryStyle and fusion of CategoryProduct{Tuple}
ogauthe Mar 29, 2024
c7b8b50
use SymmetryStyle
ogauthe Mar 29, 2024
2508c61
fusion rules for NamedTuple
ogauthe Mar 30, 2024
e234a75
further investigate quantum_dimension type stability
ogauthe Apr 1, 2024
be3f351
improve type stability
ogauthe Apr 2, 2024
bb0d757
fix type stability for quantum_dimension
ogauthe Apr 2, 2024
6e6b10c
fix product of singlet
ogauthe Apr 2, 2024
667ea0f
pass Named Category Products, broken Ordered Products
ogauthe Apr 2, 2024
11434d6
trickier tests
ogauthe Apr 3, 2024
1830781
all test passing
ogauthe Apr 3, 2024
3ae8e46
def and use gradedisequal
ogauthe Apr 3, 2024
fc96238
test mixed GradedUnitRange - Category
ogauthe Apr 3, 2024
b71f9b2
delete files not supposed to be under git
ogauthe Apr 3, 2024
56de936
dispatch instead of test type
ogauthe Apr 4, 2024
34dc147
define fusion_product for CategoryProduct
ogauthe Apr 4, 2024
551db7a
rename label -> category_label
ogauthe Apr 5, 2024
7f54001
split tensor_product and fusion_product
ogauthe Apr 5, 2024
06febc1
simplify quantum_dimensions
ogauthe Apr 8, 2024
2396e5e
blockmergesortperm for dual
ogauthe Apr 8, 2024
dfa478d
remove unused functions
ogauthe Apr 8, 2024
0000e5c
add more comments
ogauthe Apr 9, 2024
57b3fc8
support julia 1.6
ogauthe Apr 10, 2024
c6f24f8
Swap Ordered Products and Named Category Products
ogauthe Apr 11, 2024
c0e8e8d
inline pack_named_tuple, adjust tests
ogauthe Apr 15, 2024
0e1869f
add comment on label type
ogauthe Apr 15, 2024
9b3419c
parametric U1 and SU{N}, remove SU2
ogauthe Apr 23, 2024
b0e71fe
typo
ogauthe Apr 23, 2024
20b80a5
Merge branch 'main' into non-abelian_fusion
ogauthe Apr 23, 2024
68662a5
improve display
ogauthe Apr 24, 2024
298338d
remove unused abstractgradedunitrange.jl
ogauthe Apr 24, 2024
598cd42
define blocklabels and gradedisequal for UnitRangeDual
ogauthe Apr 24, 2024
bc10ca0
def trivial(::CategoryProduct)
ogauthe Apr 25, 2024
60a5410
simplify SU{N}
ogauthe Apr 25, 2024
de95045
SU2 as a concrete type
ogauthe Apr 25, 2024
043d106
trivial(::AbstractCategory)
ogauthe May 1, 2024
63ab67e
avoid confusion between integer and tuple interfaces
ogauthe May 1, 2024
c4f10e0
define trivial(::AbstractUnitRange)
ogauthe May 2, 2024
d3e0f34
def isdual
ogauthe May 17, 2024
bc5a2b7
define block_boundaries
ogauthe May 22, 2024
5ddf8ea
Merge branch 'main' into non-abelian_fusion
ogauthe May 28, 2024
96e9992
define fusion_product(g1,g2,g3)
ogauthe May 29, 2024
ba863f1
remove unused dual(type)
ogauthe May 29, 2024
e618ed0
rename block_boundaries into block_dimensions
ogauthe May 29, 2024
27b03d9
use block_dimensions in quantum_dimension. Set quantum_dimension(Empt…
ogauthe May 29, 2024
c36654e
simplify istrivial
ogauthe May 29, 2024
7683df0
use iterator
ogauthe May 30, 2024
be768ab
single EmptyCategory type, act as trivial
ogauthe May 30, 2024
326ec5f
define categories_trivial
ogauthe May 30, 2024
d3cced6
categories_isequal
ogauthe May 30, 2024
00b2172
test sector() acts as trivial
ogauthe May 30, 2024
d30c320
fix new line in printing
ogauthe May 30, 2024
e738a11
Merge branch 'main' into non-abelian_fusion
ogauthe Jun 3, 2024
fc9bacb
sector with Type arg
ogauthe Jun 3, 2024
3bcde67
reorder file
ogauthe Jun 3, 2024
9c968b0
use @inferred_latest
ogauthe Jun 4, 2024
bf31a2e
clean categories_fusion_rule
ogauthe Jun 4, 2024
7d7e57d
dual blocklabels
ogauthe Jun 5, 2024
4dae197
fuse EmptyCategory
ogauthe Jun 6, 2024
aff0006
reorder file
ogauthe Jun 6, 2024
1aa1317
replace for loops with Iterators
ogauthe Jun 6, 2024
18763f6
used labelled
ogauthe Jun 6, 2024
951f08e
use mapreduce
ogauthe Jun 6, 2024
bd8e6e7
define fusion_product for non-abelian groups
ogauthe Jun 7, 2024
f366af7
add tests
ogauthe Jun 7, 2024
cbda58b
support OneToOne
ogauthe Jun 7, 2024
cdc4030
share more implementation
ogauthe Jun 7, 2024
1ef04c1
reorder file
ogauthe Jun 7, 2024
2bfe8dd
rigorous comparisons
ogauthe Jun 7, 2024
19df9ea
Merge branch 'main' into non-abelian_fusion
ogauthe Jun 10, 2024
a86e67e
Merge branch 'main' into non-abelian_fusion
ogauthe Jun 14, 2024
3b4e3cb
fix tests
ogauthe Jun 14, 2024
d42c017
define flip
ogauthe Jun 14, 2024
45e8cc6
show(::UnitRangeDual)
ogauthe Jun 14, 2024
43c8b4d
fix tensor_product(::dual)
ogauthe Jun 14, 2024
fb59499
adapt to BlockArrays 1.1
ogauthe Jun 18, 2024
b47fded
fix Vararg
ogauthe Jun 18, 2024
2193a93
Merge branch 'main' into non-abelian_fusion
ogauthe Jun 18, 2024
9a04046
remove unneeded method
ogauthe Jun 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion NDTensors/src/imports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ for lib in [
:MetalExtensions,
:BroadcastMapConversion,
:RankFactorization,
:Sectors,
:LabelledNumbers,
:GradedAxes,
:Sectors,
:TensorAlgebra,
:SparseArrayInterface,
:SparseArrayDOKs,
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

3 changes: 1 addition & 2 deletions NDTensors/src/lib/GradedAxes/src/GradedAxes.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
module GradedAxes
include("blockedunitrange.jl")
include("gradedunitrange.jl")
include("fusion.jl")
include("dual.jl")
include("unitrangedual.jl")
include("../ext/GradedAxesSectorsExt/src/GradedAxesSectorsExt.jl")
include("fusion.jl")
end
2 changes: 2 additions & 0 deletions NDTensors/src/lib/GradedAxes/src/dual.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ using NDTensors.LabelledNumbers:
label_dual(x) = label_dual(LabelledStyle(x), x)
label_dual(::NotLabelled, x) = x
label_dual(::IsLabelled, x) = labelled(unlabel(x), dual(label(x)))

flip(g::AbstractGradedUnitRange) = dual(gradedrange(label_dual.(blocklengths(g))))
86 changes: 62 additions & 24 deletions NDTensors/src/lib/GradedAxes/src/fusion.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ OneToOne() = OneToOne{Bool}()
Base.first(a::OneToOne) = one(eltype(a))
Base.last(a::OneToOne) = one(eltype(a))

gradedisequal(::AbstractUnitRange, ::OneToOne) = false
gradedisequal(::OneToOne, ::AbstractUnitRange) = false
gradedisequal(::OneToOne, ::OneToOne) = true

# https://github.com/ITensor/ITensors.jl/blob/v0.3.57/NDTensors/src/lib/GradedAxes/src/tensor_product.jl
# https://en.wikipedia.org/wiki/Tensor_product
# https://github.com/KeitaNakamura/Tensorial.jl
Expand All @@ -18,26 +22,39 @@ function tensor_product(
return foldl(tensor_product, (a1, a2, a3, a_rest...))
end

function tensor_product(a1::AbstractUnitRange, a2::AbstractUnitRange)
function tensor_product(::AbstractUnitRange, ::AbstractUnitRange)
return error("Not implemented yet.")
end

function tensor_product(a1::Base.OneTo, a2::Base.OneTo)
return Base.OneTo(length(a1) * length(a2))
end

function tensor_product(a1::OneToOne, a2::AbstractUnitRange)
function tensor_product(::OneToOne, a2::AbstractUnitRange)
return a2
end

function tensor_product(a1::AbstractUnitRange, a2::OneToOne)
function tensor_product(a1::AbstractUnitRange, ::OneToOne)
return a1
end

function tensor_product(a1::OneToOne, a2::OneToOne)
function tensor_product(::OneToOne, ::OneToOne)
return OneToOne()
end

# Handle dual. Always return a non-dual GradedUnitRange.
function tensor_product(a1::AbstractUnitRange, a2::UnitRangeDual)
return tensor_product(a1, flip(a2))
end

function tensor_product(a1::UnitRangeDual, a2::AbstractUnitRange)
return tensor_product(flip(a1), a2)
end

function tensor_product(a1::UnitRangeDual, a2::UnitRangeDual)
return tensor_product(flip(a1), flip(a2))
end

function fuse_labels(x, y)
return error(
"`fuse_labels` not implemented for object of type `$(typeof(x))` and `$(typeof(y))`."
Expand All @@ -53,21 +70,28 @@ function fuse_blocklengths(x::LabelledInteger, y::LabelledInteger)
return labelled(unlabel(x) * unlabel(y), fuse_labels(label(x), label(y)))
end

flatten_maybe_nested(v::Vector{<:Integer}) = v
flatten_maybe_nested(v::Vector{<:AbstractGradedUnitRange}) = reduce(vcat, blocklengths.(v))

using BlockArrays: blockedrange, blocks
function tensor_product(a1::AbstractBlockedUnitRange, a2::AbstractBlockedUnitRange)
blocklengths = map(vec(collect(Iterators.product(blocks(a1), blocks(a2))))) do x
return mapreduce(length, fuse_blocklengths, x)
end
maybe_nested = map(
it -> mapreduce(length, fuse_blocklengths, it),
Iterators.flatten((Iterators.product(blocks(a1), blocks(a2)),)),
)
blocklengths = flatten_maybe_nested(maybe_nested)
return blockedrange(blocklengths)
end

function blocksortperm(a::AbstractBlockedUnitRange)
# TODO: Figure out how to deal with dual sectors.
# TODO: `rev=isdual(a)` may not be correct for symmetries beyond `U(1)`.
## return Block.(sortperm(nondual_sectors(a); rev=isdual(a)))
return Block.(sortperm(blocklabels(a)))
end

# convention: sort UnitRangeDual according to nondual blocks
function blocksortperm(a::UnitRangeDual)
return Block.(sortperm(blocklabels(nondual(a))))
end

using BlockArrays: Block, BlockVector
using SplitApplyCombine: groupcount
# Get the permutation for sorting, then group by common elements.
Expand All @@ -83,24 +107,38 @@ end
# Get the permutation for sorting, then group by common elements.
# groupsortperm([2, 1, 2, 3]) == [[2], [1, 3], [4]]
function blockmergesortperm(a::AbstractBlockedUnitRange)
# If it is dual, reverse the sorting so the sectors
# end up sorted in the same way whether or not the space
# is dual.
# TODO: Figure out how to deal with dual sectors.
# TODO: `rev=isdual(a)` may not be correct for symmetries beyond `U(1)`.
## return Block.(groupsortperm(nondual_sectors(a); rev=isdual(a)))
return Block.(groupsortperm(blocklabels(a)))
end

# Used by `TensorAlgebra.splitdims` in `BlockSparseArraysGradedAxesExt`.
invblockperm(a::Vector{<:Block{1}}) = Block.(invperm(Int.(a)))

# Used by `TensorAlgebra.fusedims` in `BlockSparseArraysGradedAxesExt`.
function blockmergesortperm(a::GradedUnitRange)
# If it is dual, reverse the sorting so the sectors
# end up sorted in the same way whether or not the space
# is dual.
# TODO: Figure out how to deal with dual sectors.
# TODO: `rev=isdual(a)` may not be correct for symmetries beyond `U(1)`.
return Block.(groupsortperm(blocklabels(a)))
function blockmergesortperm(a::UnitRangeDual)
return Block.(groupsortperm(blocklabels(nondual(a))))
end

function blockmergesort(g::AbstractGradedUnitRange)
glabels = blocklabels(g)
gblocklengths = blocklengths(g)
new_blocklengths = map(
la -> labelled(sum(gblocklengths[findall(==(la), glabels)]; init=0), la),
sort(unique(glabels)),
)
return GradedAxes.gradedrange(new_blocklengths)
end

blockmergesort(g::UnitRangeDual) = dual(blockmergesort(flip(g)))
blockmergesort(g::OneToOne) = g

# fusion_product produces a sorted, non-dual GradedUnitRange
function fusion_product(g1, g2)
return blockmergesort(tensor_product(g1, g2))
end

fusion_product(g::AbstractUnitRange) = blockmergesort(g)
fusion_product(g::UnitRangeDual) = fusion_product(flip(g))

# recursive fusion_product. Simpler than reduce + fix type stability issues with reduce
function fusion_product(g1, g2, g3...)
return fusion_product(fusion_product(g1, g2), g3...)
end
8 changes: 4 additions & 4 deletions NDTensors/src/lib/GradedAxes/src/gradedunitrange.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ using BlockArrays:
blockedrange,
BlockIndexRange,
blockfirsts,
blocklasts,
blockisequal,
blocklength,
blocklengths,
findblock,
Expand All @@ -35,9 +35,9 @@ function Base.OrdinalRange{T,T}(a::GradedOneTo{<:LabelledInteger{T}}) where {T}
return unlabel_blocks(a)
end

# TODO: See if this is needed.
function Base.AbstractUnitRange{T}(a::GradedOneTo{<:LabelledInteger{T}}) where {T}
return unlabel_blocks(a)
# == is just a range comparison that ignores labels. Need dedicated function to check equality.
function gradedisequal(a1::AbstractUnitRange, a2::AbstractUnitRange)
return blockisequal(a1, a2) && (blocklabels(a1) == blocklabels(a2))
end

# TODO: Use `TypeParameterAccessors`.
Expand Down
21 changes: 21 additions & 0 deletions NDTensors/src/lib/GradedAxes/src/unitrangedual.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ UnitRangeDual(a::AbstractUnitRange) = UnitRangeDual{eltype(a),typeof(a)}(a)
dual(a::AbstractUnitRange) = UnitRangeDual(a)
nondual(a::UnitRangeDual) = a.nondual_unitrange
dual(a::UnitRangeDual) = nondual(a)
flip(a::UnitRangeDual) = dual(flip(nondual(a)))
nondual(a::AbstractUnitRange) = a
isdual(::AbstractGradedUnitRange) = false
isdual(::UnitRangeDual) = true
## TODO: Define this to instantiate a dual unit range.
## materialize_dual(a::UnitRangeDual) = materialize_dual(nondual(a))

Expand All @@ -16,6 +19,16 @@ Base.step(a::UnitRangeDual) = label_dual(step(nondual(a)))

Base.view(a::UnitRangeDual, index::Block{1}) = a[index]

function Base.show(io::IO, a::UnitRangeDual)
return print(io, UnitRangeDual, "(", blocklasts(a), ")")
end

function Base.show(io::IO, mimetype::MIME"text/plain", a::UnitRangeDual)
return Base.invoke(
show, Tuple{typeof(io),MIME"text/plain",AbstractArray}, io, mimetype, a
)
end

function Base.getindex(a::UnitRangeDual, indices::AbstractUnitRange{<:Integer})
return dual(getindex(nondual(a), indices))
end
Expand Down Expand Up @@ -73,6 +86,14 @@ BlockArrays.blockaxes(a::UnitRangeDual) = blockaxes(nondual(a))
BlockArrays.blockfirsts(a::UnitRangeDual) = label_dual.(blockfirsts(nondual(a)))
BlockArrays.blocklasts(a::UnitRangeDual) = label_dual.(blocklasts(nondual(a)))
BlockArrays.findblock(a::UnitRangeDual, index::Integer) = findblock(nondual(a), index)

blocklabels(a::UnitRangeDual) = dual.(blocklabels(nondual(a)))

gradedisequal(::UnitRangeDual, ::AbstractGradedUnitRange) = false
gradedisequal(::AbstractGradedUnitRange, ::UnitRangeDual) = false
function gradedisequal(a1::UnitRangeDual, a2::UnitRangeDual)
return gradedisequal(nondual(a1), nondual(a2))
end
function BlockArrays.combine_blockaxes(a1::UnitRangeDual, a2::UnitRangeDual)
return dual(combine_blockaxes(dual(a1), dual(a2)))
end
Expand Down
2 changes: 1 addition & 1 deletion NDTensors/src/lib/GradedAxes/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
using Test: @testset
@testset "GradedAxes" begin
include("test_basics.jl")
include("test_tensor_product.jl")
include("test_dual.jl")
include("test_tensor_product.jl")
end
end
4 changes: 3 additions & 1 deletion NDTensors/src/lib/GradedAxes/test/test_basics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ using BlockArrays:
blocklengths,
blocks
using NDTensors.BlockSparseArrays: BlockSparseVector
using NDTensors.GradedAxes: GradedOneTo, GradedUnitRange, blocklabels, gradedrange
using NDTensors.GradedAxes:
GradedOneTo, GradedUnitRange, blocklabels, gradedisequal, gradedrange
using NDTensors.LabelledNumbers: LabelledUnitRange, islabelled, label, labelled, unlabel
using Test: @test, @test_broken, @testset
@testset "GradedAxes basics" begin
Expand Down Expand Up @@ -41,6 +42,7 @@ using Test: @test, @test_broken, @testset
@test label(x) == "y"
end
@test isnothing(iterate(a, labelled(5, "y")))
@test gradedisequal(a, a)
@test length(a) == 5
@test step(a) == 1
@test !islabelled(step(a))
Expand Down
62 changes: 57 additions & 5 deletions NDTensors/src/lib/GradedAxes/test/test_dual.jl
Original file line number Diff line number Diff line change
@@ -1,19 +1,40 @@
@eval module $(gensym())
using BlockArrays: Block, blockaxes, blockfirsts, blocklasts, blocks, findblock
using NDTensors.GradedAxes: GradedAxes, UnitRangeDual, dual, gradedrange, nondual
using BlockArrays:
Block, blockaxes, blockfirsts, blocklasts, blocklength, blocklengths, blocks, findblock
using NDTensors.GradedAxes:
GradedAxes,
UnitRangeDual,
blocklabels,
blockmergesortperm,
blocksortperm,
dual,
flip,
gradedisequal,
gradedrange,
isdual,
nondual
using NDTensors.LabelledNumbers: LabelledInteger, label, labelled
using Test: @test, @test_broken, @testset
struct U1
n::Int
end
GradedAxes.dual(c::U1) = U1(-c.n)
Base.isless(c1::U1, c2::U1) = c1.n < c2.n
@testset "dual" begin
a = gradedrange([U1(0) => 2, U1(1) => 3])
ad = dual(a)
@test eltype(ad) == LabelledInteger{Int,U1}
@test dual(ad) == a
@test nondual(ad) == a
@test nondual(a) == a

@test gradedisequal(dual(ad), a)
@test gradedisequal(nondual(ad), a)
@test gradedisequal(nondual(a), a)
@test gradedisequal(ad, ad)
@test !gradedisequal(a, ad)
@test !gradedisequal(ad, a)

@test isdual(ad)
@test !isdual(a)

@test blockfirsts(ad) == [labelled(1, U1(0)), labelled(3, U1(-1))]
@test blocklasts(ad) == [labelled(2, U1(0)), labelled(5, U1(-1))]
@test findblock(ad, 4) == Block(2)
Expand All @@ -34,5 +55,36 @@ GradedAxes.dual(c::U1) = U1(-c.n)
@test label(ad[[Block(2), Block(1)]][Block(1)]) == U1(-1)
@test ad[[Block(2)[1:2], Block(1)[1:2]]][Block(1)] == 3:4
@test label(ad[[Block(2)[1:2], Block(1)[1:2]]][Block(1)]) == U1(-1)
@test blocksortperm(a) == [Block(1), Block(2)]
@test blocksortperm(ad) == [Block(1), Block(2)]
@test blocklength(blockmergesortperm(a)) == 2
@test blocklength(blockmergesortperm(ad)) == 2
@test blockmergesortperm(a) == [Block(1), Block(2)]
@test blockmergesortperm(ad) == [Block(1), Block(2)]
end

@testset "flip" begin
a = gradedrange([U1(0) => 2, U1(1) => 3])
ad = dual(a)
@test gradedisequal(flip(a), dual(gradedrange([U1(0) => 2, U1(-1) => 3])))
@test gradedisequal(flip(ad), gradedrange([U1(0) => 2, U1(-1) => 3]))

@test blocklabels(a) == [U1(0), U1(1)]
@test blocklabels(dual(a)) == [U1(0), U1(-1)]
@test blocklabels(flip(a)) == [U1(0), U1(1)]
@test blocklabels(flip(dual(a))) == [U1(0), U1(-1)]
@test blocklabels(dual(flip(a))) == [U1(0), U1(-1)]

@test blocklengths(a) == [2, 3]
@test blocklengths(dual(a)) == [2, 3]
@test blocklengths(flip(a)) == [2, 3]
@test blocklengths(flip(dual(a))) == [2, 3]
@test blocklengths(dual(flip(a))) == [2, 3]

@test !isdual(a)
@test isdual(dual(a))
@test isdual(flip(a))
@test !isdual(flip(dual(a)))
@test !isdual(dual(flip(a)))
end
end