Home

TensorNetworkTensors.jl

Tensors for Tensor Network Methods. TensorNetworkTensors.jl overloads methods from TensorOperations and KrylovKit to work with Tensors that might be symmetric under some abelina symmetry.

Usage

Tensors

You can define tensors without symmetry using the DTensor type as

julia> a = DTensor{Complex{Float64}}((2,2));

which will return a 2x2-tensor with elements of type Complex{Float64}. DTensor is just a thin wrapper around Array.

A tensor with a symmetry can be defined by specifying the:

  1. symmetry
  2. possible charge
  3. sizes of the degeneracy spaces of those charge
  4. action of the group acts on an index

The currently implemented Discrete Abelian Symmetries are U1 and ZN symmetries. To define a U1 symmetric rank-3 tensor which, in the particle conservation picture, has two incoming, one outgoing leg and supports carrying between 3 and -3 particles per leg where each charge has a degeneracy-size of 2, we write:

julia> sym = U1()
julia> chs = (U1Charges(-3:3), U1Charges(-3:3), U1Charges(-3:3))
julia> dims = ([2 for ch in U1Charges(-3:3)],
                [2 for ch in U1Charges(-3:3)],
                [2 for ch in U1Charges(-3:3)])
julia> io = InOut(1,1,-1)
julia> a = DASTensor{Float64,3}(sym, chs, dims, io)

where a is a Discrete Abelian Symmetric rank-3 tensor with degeneracy tensors of type Float64. It's first two indices can be read as incoming while the last is outgoing. All indices support charges in U1Charges(-3:3) which can be looked at using

julia> foreach(println, U1Charges(-3:3))
U1Charge(-3)
U1Charge(-2)
U1Charge(-1)
U1Charge(0)
U1Charge(1)
U1Charge(2)
U1Charge(3)

The same for a Z2-symmetric tensor looks like

julia> sym = ZN{2}()
julia> chs = (ZNCharges{2}(), ZNCharges{2}(), ZNCharges{2}())
julia> dims = ([2 for ch in ZNCharges{2}()],
               [2 for ch in ZNCharges{2}()],
               [2 for ch in ZNCharges{2}()])
julia> io = InOut(1,1,-1)
julia> a = DASTensor{Float64,3}(sym, chs, dims, io)

Above we just defined tensors but they are either filled with garbage (DTensor) or empty (DASTensor). To initialize a tensor, use initwithzero! or initwithrand!, e.g.

julia> a = DTensor{Complex{Float64}}((2,2))
DTensor{Complex{Float64},2}Complex{Float64}[6.93789e-310+6.93789e-310im 6.93789e-310+6.93786e-310im; 6.93789e-310+6.93786e-310im 6.93786e-310+6.93789e-310im]
julia> initwithzero!(a)
DTensor{Complex{Float64},2}Complex{Float64}[0.0+0.0im 0.0+0.0im; 0.0+0.0im 0.0+0.0im]

For tensors with symmetry - DASTensor - the same works but the underlying structure is different:

julia> sym = ZN{2}()
julia> chs = (ZNCharges{2}(), ZNCharges{2}(), ZNCharges{2}())
julia> dims = ([2 for ch in ZNCharges{2}()],
               [2 for ch in ZNCharges{2}()],
               [2 for ch in ZNCharges{2}()])
julia> io = InOut(1,1,-1)
julia> a = DASTensor{Float64,3}(sym, chs, dims, io);
julia> initwithzero!(a)
julia> tensor(a)

To look into a DASTensor, you can use tensor to see the underlying dictionary which maps DASSectors, discrete abelian symmetry sectors, to degeneracy tensors.

Dict{DASSector{3,ZNCharge{2}},Array{Float64,3}} with 4 entries:
  DASSector(Z2Charge(0), Z2Charge(0), Z2Charge(0)) => [0.0 0.0; 0.0 0.0]…
  DASSector(Z2Charge(0), Z2Charge(1), Z2Charge(1)) => [0.0 0.0; 0.0 0.0]…
  DASSector(Z2Charge(1), Z2Charge(0), Z2Charge(1)) => [0.0 0.0; 0.0 0.0]…
  DASSector(Z2Charge(1), Z2Charge(1), Z2Charge(0)) => [0.0 0.0; 0.0 0.0]…

You can also directly access a degeneracy tensor like this:

julia> a[DASSector(Z2Charge(0), Z2Charge(0), Z2Charge(0))]
2×2×2 Array{Float64,3}:
[:, :, 1] =
 0.0  0.0
 0.0  0.0

[:, :, 2] =
 0.0  0.0
 0.0  0.0

Specific functions for DASTensors include:

to learn more, use ? as in e.g.

julia>?charge
search: charge charges chargedim chargetype chargesize chargestype chargeindex ZNCharge Z2Charge U1Charge ZNCharges Z2Charges U1Charges DASCharge setcharges! DASCharges NDASCharge NDASCharges splitchargeit SplitChargeIt connectingcharge

  charge(a::DASSector)

  returns the charge which is calculated as the the sum of all charges it contains.

  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  charge(a::DASTensor)

  returns the charge which is calculated as the charge of its non-zero sectors which needs to be unique.

Reshaping

Reshaping can be done by either fusing or splitting legs. Since splitting with symmetries is highly nontrivial, it might only be done after a fusion which provides the necessary information of how to recombine indices and charges.

To fuse indices, we use the function fuselegs:

help?>fuselegs
  fuselegs(A, indexes)

  Fuse the indices in A as given by indexes where indexes is a tuple containing indices either alone or grouped in tuples - the latter will be fused. Returns a tuple of a tensor and the object necessary to undo the fusion.

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> a = DTensor(collect(reshape(1:8,2,2,2))
  DTensor{Int64,3}[1 3; 2 4]
  [5 7; 6 8]
  julia> fuselegs(a, ((1,2),3))
  (DTensor{Int64,2}[1 5; 2 6; 3 7; 4 8], ((2, 2),))

    ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  fuselegs(A, indexes[, io])

  For DASTensors, the directions of the resulting legs might be specified. If io is ommitted, the default is InOut(1,1,1...).

Fusion might also include permutation. Note that fuselegs returns two objects:

  1. A new tensor that corresponds to the input with the fusion applie
  2. An object to undo that fusion.

The latter is achieved using splitlegs:

help?> splitlegs
search: splitlegs splitlegs!

  splitlegs(A, indexes, rs...)

  Split the indices in A as given by indexes and rs. indexes is a tuple of integers and 3-tuple where each 3-tuple (i,j,k) specifies that index i in A is split according to rs[j], index k therein. Returns tensor with fused legs.

  Examples
  ≡≡≡≡≡≡≡≡≡≡

  julia> a = DTensor(collect(reshape(1:8,2,2,2))
  DTensor{Int64,3}[1 3; 2 4]
  [5 7; 6 8]
  julia> af, rs = fuselegs(a, ((1,2),3))
  (DTensor{Int64,2}[1 5; 2 6; 3 7; 4 8], ((2, 2),))
  julia> splitlegs(af, ((1,1,1),(1,1,2),2), rs...)
  DTensor{Int64,3}[1 3; 2 4]
  [5 7; 6 8]

The same is true for the case of DASTensors, although fuselegs returns a more complicated object of the type Reshaper. If there is already a tensor of the correct shape and charges available, both fuselegs and splitlegs can be used as their in-place version fuselegs! and splitlegs! (see help).

Note that fusion is specified with permutation of the indices of a tensor and indices grouped in a tuple are fused, e.g. ((1,2),3) means the first two indices of a rank-3 tensor are grouped whereas (1,(4,3),2) means that the third and fourth index of a rank-4 tensor are grouped and switched with the second index.

For splitting, indices are a list of integers and 3-tuples (i,j,k) where the latter specifies that index i in given tensor is split according to index j in the reshapeing-information and part k of that split is to be at the position of that tuple.

Let's look at an example:

julia> sym = U1()
julia> chs = (U1Charges(-3:3), U1Charges(-3:3), U1Charges(-3:3))
julia> dims = ([2 for ch in U1Charges(-3:3)],
                [2 for ch in U1Charges(-3:3)],
                [2 for ch in U1Charges(-3:3)])
julia> io = InOut(1,1,1)
julia> a = DASTensor{Float64,3}(sym, chs, dims, io)
julia> initwithrand!(a)

First we fuse indices 1 and 3 into (3,1) putting them in the first place and specifying the directions of the legs as both outgoing:

julia> af, rs = fuselegs(a, ((3,1),2),InOut(-1,-1))

To undo that fusion, we need to split the first index of af according to the first reshaper in rs (there's a reshaper for each index in the fused tensor in order). A valid splitting would be:

julia> splitlegs(af,((1,1,1),(1,1,2),2),rs...)

but that would correspond to a permutation (3,1,2) of a. Additionally, the unfused index was also changed by switching its InOut! We thus want to specify both the correct permutation and that index 2 needs to be changed accordingly, arriving at

julia> splitlegs(af, ((1,1,2),(2,2,1),(1,1,1)), rs...) ≈ a
true

Factorizations

So far the factorizations available are tensorsvd which returns the SVD of a tensor, see

help?> tensorsvd
search: tensorsvd tensorsvd! _tensorsvd TensorOperations

  tensorsvd(A::AbstractTensor, indexes; svdtrunc)

  works like tensorsvd except that A can have arbitrary rank.
  indexes specifies which indices the fuse for A to be a rank-2 tensor as in fuselegs.
  The tensor is then fused, tensorsvd applied and split again.

  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  tensorsvd(A::AbstractTensor{T,2}; svdtrunc)

  returns the svd of an AbstractTensor.
  svdtrunc is a function that has the singular values as input and returns a number specifying how many of them to keep.
  The default svdtrunc is svdtrunc_default and keeps all of them.

  Other options are: svdtruncdiscardzero svdtruncmaxχ svdtruncmaxcumerror svdtruncmaxerror

and tensorqr which returns the QR-decomposition of a tensor, see

help?> tensorqr
search: tensorqr tensor tensorsvd tensoradd tensorsvd! tensortrace tensorcopy tensoradd! tensortrace! tensorcopy! tensorproduct tensorproduct! tensorcontract

  tensorqr(A::AbstractTensor)

  returns tensor Q,R such that A = QR and Q is an orthogonal/unitary matrix and R is an upper triangular matrix.

  ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

  tensorqr(A::AbstractTensor, inds)

  returns the tensorqr of A fused according to inds.

KrylovKit

All AbstractTensors work with KrylovKit, a package that combines a number of Krylov-based algorithms including exponentiate.

To use KrylovKit, follow the instruction for installation at the github repository and do

using KrylovKit

As an example, consider eigsolve for a tensor with two independent symmetries:

julia> a = DASTensor{Complex{Float64},2}(
  NDAS(Z2(),U1()),
  (NDASCharges(Z2Charges(), U1Charges(-1:1)), NDASCharges(Z2Charges(), U1Charges(-1:1))),
  (fill(2,6), fill(2,6)),
  InOut(1,-1))
julia> initwithrand!(a)
julia> using TensorOperations
julia> @tensor a[1,2] := a[1,2] + a'[2,1];

Since we are not working with a regular array, we need to provide an initial guess for an eigenvector, e.g.

julia> v0 = DASTensor{Complex{Float64},1}(
         NDAS(Z2(),U1()),
         (NDASCharges(Z2Charges(), U1Charges(-1:1)),),
         (fill(2,6),),
         InOut(1))
julia> initwithrand!(v0)

Then we can define a function for applying a to vectors like v0:

julia> f(v) = @tensor v2[1] :=  a[1,-1] * v[-1]

Then we can use eigsolve with f and v0 to get an eigenvector of a or the map that it describes:

julia> eigsolve(f,v0)
[...]

which returns two eigenvalues, two eigenvectors and an object containing information about convergence.

Similarly we can apply exp(0.1*a) to v0 with exponentiate:

julia> exponentiate(f,0.1,v0, ishermitian=true)
AbstractTensor{T,N}

Abstract supertype for all tensornetwork-tensors.

source
DAS

Abstract supertype of all Discrete Abelian Symmetries

source
DASCharge

Abstract supertype of all Charges of Discrete Abelian Symmetries

source
DASCharges

Abstract supertype of all collections of Charges of Discrete Abelian Symmetries

source
DASSector{N,T}

DASSectors are a configuration of charges that are allowed under a given symmetry and index degeneracy spaces in DASTensors{T,N}.

Example

julia> DASSector(U1Charge(1), U1Charge(2))
DASSector(U1Charge(1), U1Charge(2))
source
DTensor{T,N} <: AbstractTensor{T,N}

wrapper for generic Array{T,N} to represent dense tensors.

source
InOut{N}

InOut describes whether representations of the DAS act on an index of a DASTensor{T,N} directly or via their dual. InOut(1,1,1,-1) can be read as the first three indices corresponding to incoming, the last as an outgoing index w.r.t the group action.

source
NDAS{N,S} <: DAS

wraps multiple independent symmetries to work together.

Example

julia>NDAS(U1(), Z2())
NDAS{2,(U1, Z2)}()
source
NDASCharge{N} <: DASCharge

holds the charge of multiple independent symmetries grouped together.

Example

julia> a = NDASCharge(U1Charge(1), Z2Charge(1));
julia> a ⊕ a
NDASCharge(U1Charge(2), Z2Charge(0))
source
NDASCharges{N,T} <: DASCharges

holds combinations of multiple independent DASCharges that are grouped together. Yields NDASCharge{N,T}s upon iteration.

Example

julia> a = NDASCharges(U1Charges(-1:1), ZNCharges{3}());
julia> foreach(println, a)
NDASCharge(U1Charge(-1), Z3Charge(0))
NDASCharge(U1Charge(0), Z3Charge(0))
NDASCharge(U1Charge(1), Z3Charge(0))
NDASCharge(U1Charge(-1), Z3Charge(1))
NDASCharge(U1Charge(0), Z3Charge(1))
NDASCharge(U1Charge(1), Z3Charge(1))
NDASCharge(U1Charge(-1), Z3Charge(2))
NDASCharge(U1Charge(0), Z3Charge(2))
NDASCharge(U1Charge(1), Z3Charge(2))
source
U1 <: DAS

U1 symmetry singleton-type

source
U1Charge <: DASCharge

holds the charge of a U1 symmetry as an integer.

Example

julia>a = U1Charge(1);
julia>a ⊕ a
U1Charge(2)
source
U1Charges <: DASCharges

type for collection of U1Charge that holds StepRange{Int,Int} which represents the valid values for U1Charge.

Example

julia>a = U1Charges(-1:1);
julia>foreach(println, a)
U1Charge(-1)
U1Charge(0)
U1Charge(1)
source
ZN{N} <: DAS

ZN symmetry singleton type where N is an Int.

source
ZNCharge{N} <: DASCharge

holds the charge of a ZN symmetry as an integer. The integer is taken mod N s.t. the charge is always between 0 and N-1

Example

julia>a = ZNCharge{2}(1);
julia>a ⊕ a
ZNCharge{2}(0)

julia>a = ZNCharge{2}(3) == ZNCharge{2}(1)
true
source
ZNCharges{N} <: DASCharges

singleton-type for collections of ZNCharge{N} objects. Only the type parameter is provided - ZNCharges{N} doesn't have a field. Upon iteration yields ZNCharge{N}(i) for i = 0,...,N-1

Example

julia>a = ZNCharges{2}()
julia>foreach(println, a)
ZNCharge{2}(0)
ZNCharge{2}(1)
source
⊕(a::T, bs::T...) where {T<:Union{DASCharge, DASCharges}}

returns the result of fusing one or more DASCharge or DASCharges together. Fusing DASCharges yields a DASCharges that holds all elements that result from fusing the DASCharges.

source
allsectors(chs)

returns a generator that generates all possible combinations of charges in chs wrapped in a DASSector.

#Example

julia> allsectors((U1Charges(-1:1), U1Charges(4:5))) |> collect
3×2 Array{DASSector{2,U1Charge},2}:
 DASSector(U1Charge(-1), U1Charge(4))  DASSector(U1Charge(-1), U1Charge(5))
 DASSector(U1Charge(0), U1Charge(4))   DASSector(U1Charge(0), U1Charge(5))
 DASSector(U1Charge(1), U1Charge(4))   DASSector(U1Charge(1), U1Charge(5))
source
charge(a::DASSector)

returns the charge which is calculated as minus the sum of all charges it contains.

source
charge(a::DASTensor)

returns the charge of a tensor.

source
chargeindex(ch::DASCharge, chs::DASCharges)

returns the index i of ch in chs

Example

julia> ch = U1Charge(0); chs = U1Charges(-1:1);
julia> chargeindex(ch, chs)
2
julia> chs[2] == ch
true
source
charges(A::DASTensor[,i])

returns the charges of A. If i is specified as either Int or Tuple, returns only the charges of the indices in i.

source
covariantsectors(chs, io[, ch = zero(chs)])

returns all sectors in allsectors(io ⊗ chs) that have total charge ch

source
fuselegs(A, indexes)

Fuse the indices in A as given by indexes where indexes is a tuple containing indices either alone or grouped in tuples - the latter will be fused. Returns a tuple of a tensor and the object necessary to undo the fusion.

Examples

julia> a = DTensor(collect(reshape(1:8,2,2,2))
DTensor{Int64,3}[1 3; 2 4]
[5 7; 6 8]
julia> fuselegs(a, ((1,2),3))
(DTensor{Int64,2}[1 5; 2 6; 3 7; 4 8], ((2, 2),))
source
fuselegs!(AF,A, indexes)

In-place version of fuselegs where AF is a tensor of the correct type and size to hold the fused version of A. See fuselegs

source
fuselegs(A, indexes[, io])

For DASTensors, the directions of the resulting legs might be specified. If io is ommitted, the default is InOut(1,1,1...).

source
gatherby(f, xs)

Like groupby but only returns values, i.e. elements of xs in Vectors such that f applied to an element of a group is the same as for any other element of that Vector.

Example

julia> gatherby(isodd, 1:10)
2-element Array{Array{Int64,1},1}:
 [2, 4, 6, 8, 10]
 [3, 5, 7, 9]
source
groupby(f, xs)

group values x ∈ xs by the result of applying f. Returns a Dict with keys yi and values [xi1, xi2,...] such that f(xij) = yi.

Example

julia> groupby(isodd,1:10)
Dict{Bool,Array{Int64,1}} with 2 entries:
  false => [2, 4, 6, 8, 10]
  true  => [3, 5, 7, 9]
source
in_out(A::DASTensor[,i])

returns the InOut of A which specifies the action of the symmetry group on the corresponding leg. If i is specified as either Int or Tuple, returns only the charges of the indices in i.

source
initwithrand!(A::DASTensor)

construct all valid sectors in A and initialize their degeneracy spaces with rand.

source
initwithzero!(A::DASTensor)

construct all valid sectors in A and initialize their degeneracy spaces with zeros.

source
covariantsectors(chs, io)

returns all sectors in allsectors(io ⊗ chs) that have total charge zero.

source
isinvariant(a::DASTensor)

return true if charge(a) is zero.

source
setcharges!(A::DASTensor, chs)

set the charges of A to be chs where the latter is a tuple of DASCharges.

source
setin_out!(A::DASTensor, io)

set the InOut of A to be io where the latter is a InOut.

source
setsizes!(A::DASTensor, s)

set the sizes of A to be s where the latter is a tuple of Vector{Int}.

source
sizes(A::DASTensor[,i])

returns the sizes of A as a tuple of vectors v such that the degeneracy space associated with a charge ch has size v[chargeindex(ch, chs)] where chs is the DASCharges associated with the specified leg. If i is specified as either Int or Tuple, returns only the charges of the indices in i.

source
splitlegs(A, indexes, rs...)

Split the indices in A as given by indexes and rs. indexes is a tuple of integers and 3-tuple where each 3-tuple (i,j,k) specifies that index i in A is split according to rs[j], index k therein. Returns tensor with fused legs.

Examples

julia> a = DTensor(collect(reshape(1:8,2,2,2))
DTensor{Int64,3}[1 3; 2 4]
[5 7; 6 8]
julia> af, rs = fuselegs(a, ((1,2),3))
(DTensor{Int64,2}[1 5; 2 6; 3 7; 4 8], ((2, 2),))
julia> splitlegs(af, ((1,1,1),(1,1,2),2), rs...)
DTensor{Int64,3}[1 3; 2 4]
[5 7; 6 8]
source
splitlegs!(AS,A, indexes, rs...)

In-place version of splitlegs where AS is a tensor of the correct type and size to hold the split version of A. See splitlegs

source
svdtrunc_discardzero(s)

return the number of non-zero values in s

source
svdtrunc_maxcumerror(ϵ::Real; χ::Int = typemax(Int))

return a function that returns then min of χ and l where l is the number of singular values that need to be kept to have the truncated sum up to not more than ϵ.

source
svdtrunc_maxerror(ϵ::Real; χ::Int = typemax(Int))

return a function that returns then min of χ and l where l is the number of singular values that need to be kept such that the largest discarded singular value is below ϵ.

source
svdtrunc_maxχ(χ)

return a function that given singular values s returns the min of the length of s and χ.

source
symmetry(A::DASTensor)

returns the symmetry of a tensor A

source
tensor(A::DASTensor[,i])

returns a dictionary of DASSectors and their associated degeneracy spaces.

source
tensorqr(A::AbstractTensor)

returns tensor Q,R such that A = QR and Q is an orthogonal/unitary matrix and R is an upper triangular matrix.

source
tensorqr(A::AbstractTensor, inds)

returns the tensorqr of A fused according to inds.

source
tensorrq(A::AbstractTensor)

returns tensor R,Q such that A = RQ and Q is obeys Q*Q' = 1 and R is triangular. This is simply a wrapper for tensorqr, useful for e.g. MPS canonicalization.

source
tensorrq(A::AbstractTensor, inds)

returns the tensorrq of A fused according to inds.

source
tensorsvd(A::AbstractTensor{T,2}; svdtrunc)

returns the svd of an AbstractTensor. svdtrunc is a function that has the singular values as input and returns a number specifying how many of them to keep. The default svdtrunc is svdtrunc_default and keeps all of them.

Other options are: svdtruncdiscardzero svdtruncmaxχ svdtruncmaxcumerror svdtruncmaxerror

source
tensorsvd(A::AbstractTensor, indexes; svdtrunc)

works like tensorsvd except that A can have arbitrary rank. indexes specifies which indices the fuse for A to be a rank-2 tensor as in fuselegs. The tensor is then fused, tensorsvd applied and split again.

source
connectingcharge(A)

where A is a rank-2 DASTensor, returns the charges on the second index that can be realized given the charges on the first index and the charge of A.

source
initwith!(A::DASTensor{T}, fun [,ch])

modifies A such that each sector with charge ch (default=zero) is (independently) set to fun(T, dims...) where dims is the size of the degeneracy space for the sector.

source