diff --git a/dev/.documenter-siteinfo.json b/dev/.documenter-siteinfo.json index 5ee8442c..8454beeb 100644 --- a/dev/.documenter-siteinfo.json +++ b/dev/.documenter-siteinfo.json @@ -1 +1 @@ -{"documenter":{"julia_version":"1.11.2","generation_timestamp":"2024-12-08T20:07:32","documenter_version":"1.8.0"}} \ No newline at end of file +{"documenter":{"julia_version":"1.11.2","generation_timestamp":"2024-12-13T16:26:04","documenter_version":"1.8.0"}} \ No newline at end of file diff --git a/dev/index.html b/dev/index.html index f9b68d99..815275f5 100644 --- a/dev/index.html +++ b/dev/index.html @@ -1,2 +1,2 @@ -Home · TensorKit.jl

TensorKit.jl

A Julia package for large-scale tensor computations, with a hint of category theory.

Package summary

TensorKit.jl aims to be a generic package for working with tensors as they appear throughout the physical sciences. TensorKit implements a parametric type Tensor (which is actually a specific case of the type TensorMap) and defines for these types a number of vector space operations (scalar multiplication, addition, norms and inner products), index operations (permutations) and linear algebra operations (multiplication, factorizations). Finally, tensor contractions can be performed using the @tensor macro from TensorOperations.jl.

Currently, most effort is oriented towards tensors as they appear in the context of quantum many body physics and in particular the field of tensor networks. Such tensors often have large dimensions and take on a specific structure when symmetries are present. To deal with generic symmetries, we employ notations and concepts from category theory all the way down to the definition of a tensor.

At the same time, TensorKit.jl focusses on computational efficiency and performance. The underlying storage of a tensor's data can be any DenseArray. Currently, certain operations are already multithreaded, either by distributing the different blocks in case of a structured tensor (i.e. with symmetries) or by using multithreading provided by the package Strided.jl. In the future, we also plan to investigate using CuArrays as underlying storage for the tensors data, so as to leverage GPUs for the different operations defined on tensors.

Contents of the manual

Library outline

+Home · TensorKit.jl

TensorKit.jl

A Julia package for large-scale tensor computations, with a hint of category theory.

Package summary

TensorKit.jl aims to be a generic package for working with tensors as they appear throughout the physical sciences. TensorKit implements a parametric type Tensor (which is actually a specific case of the type TensorMap) and defines for these types a number of vector space operations (scalar multiplication, addition, norms and inner products), index operations (permutations) and linear algebra operations (multiplication, factorizations). Finally, tensor contractions can be performed using the @tensor macro from TensorOperations.jl.

Currently, most effort is oriented towards tensors as they appear in the context of quantum many body physics and in particular the field of tensor networks. Such tensors often have large dimensions and take on a specific structure when symmetries are present. To deal with generic symmetries, we employ notations and concepts from category theory all the way down to the definition of a tensor.

At the same time, TensorKit.jl focusses on computational efficiency and performance. The underlying storage of a tensor's data can be any DenseArray. Currently, certain operations are already multithreaded, either by distributing the different blocks in case of a structured tensor (i.e. with symmetries) or by using multithreading provided by the package Strided.jl. In the future, we also plan to investigate using CuArrays as underlying storage for the tensors data, so as to leverage GPUs for the different operations defined on tensors.

Contents of the manual

Library outline

diff --git a/dev/index/index.html b/dev/index/index.html index 66728a44..3729ffe7 100644 --- a/dev/index/index.html +++ b/dev/index/index.html @@ -1,2 +1,2 @@ -Index · TensorKit.jl

Index

+Index · TensorKit.jl

Index

diff --git a/dev/lib/sectors/index.html b/dev/lib/sectors/index.html index 5e2e5fd8..44f3b494 100644 --- a/dev/lib/sectors/index.html +++ b/dev/lib/sectors/index.html @@ -34,26 +34,26 @@ SU{N} where N const SU₂ = SU{2} ProductGroup

Specific methods:

TensorKitSectors.:×Function
×(G::Vararg{Type{<:Group}}) -> ProductGroup{Tuple{G...}}
-times(G::Vararg{Type{<:Group}}) -> ProductGroup{Tuple{G...}}

Construct the direct product of a (list of) groups.

source

Methods for defining and generating fusion trees

TensorKit.FusionTreeType
struct FusionTree{I, N, M, L}

Represents a fusion tree of sectors of type I<:Sector, fusing (or splitting) N uncoupled sectors to a coupled sector. It actually represents a splitting tree, but fusion tree is a more common term.

Fields

  • uncoupled::NTuple{N,I}: the uncoupled sectors coming out of the splitting tree, before the possible 𝑍 isomorphism (see isdual).
  • coupled::I: the coupled sector.
  • isdual::NTuple{N,Bool}: indicates whether a 𝑍 isomorphism is present (true) or not (false) for each uncoupled sector.
  • innerlines::NTuple{M,I}: the labels of the M=max(0, N-2) inner lines of the splitting tree.
  • vertices::NTuple{L,Int}: the integer values of the L=max(0, N-1) vertices of the splitting tree. If FusionStyle(I) isa MultiplicityFreeFusion, then vertices is simply equal to the constant value ntuple(n->1, L).
source
TensorKit.fusiontreesMethod
fusiontrees(uncoupled::NTuple{N,I}[,
+times(G::Vararg{Type{<:Group}}) -> ProductGroup{Tuple{G...}}

Construct the direct product of a (list of) groups.

source

Methods for defining and generating fusion trees

TensorKit.FusionTreeType
struct FusionTree{I, N, M, L}

Represents a fusion tree of sectors of type I<:Sector, fusing (or splitting) N uncoupled sectors to a coupled sector. It actually represents a splitting tree, but fusion tree is a more common term.

Fields

  • uncoupled::NTuple{N,I}: the uncoupled sectors coming out of the splitting tree, before the possible 𝑍 isomorphism (see isdual).
  • coupled::I: the coupled sector.
  • isdual::NTuple{N,Bool}: indicates whether a 𝑍 isomorphism is present (true) or not (false) for each uncoupled sector.
  • innerlines::NTuple{M,I}: the labels of the M=max(0, N-2) inner lines of the splitting tree.
  • vertices::NTuple{L,Int}: the integer values of the L=max(0, N-1) vertices of the splitting tree. If FusionStyle(I) isa MultiplicityFreeFusion, then vertices is simply equal to the constant value ntuple(n->1, L).
source
TensorKit.fusiontreesMethod
fusiontrees(uncoupled::NTuple{N,I}[,
     coupled::I=one(I)[, isdual::NTuple{N,Bool}=ntuple(n -> false, length(uncoupled))]])
-    where {N,I<:Sector} -> FusionTreeIterator{I,N,I}

Return an iterator over all fusion trees with a given coupled sector label coupled and uncoupled sector labels and isomorphisms uncoupled and isdual respectively.

source

Methods for manipulating fusion trees

For manipulating single fusion trees, the following internal methods are defined:

TensorKit.insertatFunction
insertat(f::FusionTree{I, N₁}, i::Int, f₂::FusionTree{I, N₂})
--> <:AbstractDict{<:FusionTree{I, N₁+N₂-1}, <:Number}

Attach a fusion tree f₂ to the uncoupled leg i of the fusion tree f₁ and bring it into a linear combination of fusion trees in standard form. This requires that f₂.coupled == f₁.uncoupled[i] and f₁.isdual[i] == false.

source
TensorKit.splitFunction
split(f::FusionTree{I, N}, M::Int)
--> (::FusionTree{I, M}, ::FusionTree{I, N-M+1})

Split a fusion tree into two. The first tree has as uncoupled sectors the first M uncoupled sectors of the input tree f, whereas its coupled sector corresponds to the internal sector between uncoupled sectors M and M+1 of the original tree f. The second tree has as first uncoupled sector that same internal sector of f, followed by remaining N-M uncoupled sectors of f. It couples to the same sector as f. This operation is the inverse of insertat in the sense that if f₁, f₂ = split(t, M) ⇒ f == insertat(f₂, 1, f₁).

source
TensorKit.mergeFunction
merge(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, c::I, μ = 1)
--> <:AbstractDict{<:FusionTree{I, N₁+N₂}, <:Number}

Merge two fusion trees together to a linear combination of fusion trees whose uncoupled sectors are those of f₁ followed by those of f₂, and where the two coupled sectors of f₁ and f₂ are further fused to c. In case of FusionStyle(I) == GenericFusion(), also a degeneracy label μ for the fusion of the coupled sectors of f₁ and f₂ to c needs to be specified.

source
TensorKit.elementary_traceFunction
elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} -> <:AbstractDict{FusionTree{I,N-2}, <:Number}

Perform an elementary trace of neighbouring uncoupled indices i and i+1 on a fusion tree f, and returns the result as a dictionary of output trees and corresponding coefficients.

source
TensorKit.planar_traceMethod
planar_trace(f::FusionTree{I,N}, q1::IndexTuple{N₃}, q2::IndexTuple{N₃}) where {I<:Sector,N,N₃}
-    -> <:AbstractDict{FusionTree{I,N-2*N₃}, <:Number}

Perform a planar trace of the uncoupled indices of the fusion tree f at q1 with those at q2, where q1[i] is connected to q2[i] for all i. The result is returned as a dictionary of output trees and corresponding coefficients.

source
TensorKit.artin_braidFunction
artin_braid(f::FusionTree, i; inv::Bool = false) -> <:AbstractDict{typeof(f), <:Number}

Perform an elementary braid (Artin generator) of neighbouring uncoupled indices i and i+1 on a fusion tree f, and returns the result as a dictionary of output trees and corresponding coefficients.

The keyword inv determines whether index i will braid above or below index i+1, i.e. applying artin_braid(f′, i; inv = true) to all the outputs f′ of artin_braid(f, i; inv = false) and collecting the results should yield a single fusion tree with non-zero coefficient, namely f with coefficient 1. This keyword has no effect if BraidingStyle(sectortype(f)) isa SymmetricBraiding.

source
TensorKit.braidMethod
braid(f::FusionTree{<:Sector, N}, levels::NTuple{N, Int}, p::NTuple{N, Int})
--> <:AbstractDict{typeof(t), <:Number}

Perform a braiding of the uncoupled indices of the fusion tree f and return the result as a <:AbstractDict of output trees and corresponding coefficients. The braiding is determined by specifying that the new sector at position k corresponds to the sector that was originally at the position i = p[k], and assigning to every index i of the original fusion tree a distinct level or depth levels[i]. This permutation is then decomposed into elementary swaps between neighbouring indices, where the swaps are applied as braids such that if i and j cross, $τ_{i,j}$ is applied if levels[i] < levels[j] and $τ_{j,i}^{-1}$ if levels[i] > levels[j]. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations.

source
TensorKit.permuteMethod
permute(f::FusionTree, p::NTuple{N, Int}) -> <:AbstractDict{typeof(t), <:Number}

Perform a permutation of the uncoupled indices of the fusion tree f and returns the result as a <:AbstractDict of output trees and corresponding coefficients; this requires that BraidingStyle(sectortype(f)) isa SymmetricBraiding.

source

These can be composed to implement elementary manipulations of fusion-splitting tree pairs, according to the following methods

# TODO: add documentation for the following methods
+    where {N,I<:Sector} -> FusionTreeIterator{I,N,I}

Return an iterator over all fusion trees with a given coupled sector label coupled and uncoupled sector labels and isomorphisms uncoupled and isdual respectively.

source

Methods for manipulating fusion trees

For manipulating single fusion trees, the following internal methods are defined:

TensorKit.insertatFunction
insertat(f::FusionTree{I, N₁}, i::Int, f₂::FusionTree{I, N₂})
+-> <:AbstractDict{<:FusionTree{I, N₁+N₂-1}, <:Number}

Attach a fusion tree f₂ to the uncoupled leg i of the fusion tree f₁ and bring it into a linear combination of fusion trees in standard form. This requires that f₂.coupled == f₁.uncoupled[i] and f₁.isdual[i] == false.

source
TensorKit.splitFunction
split(f::FusionTree{I, N}, M::Int)
+-> (::FusionTree{I, M}, ::FusionTree{I, N-M+1})

Split a fusion tree into two. The first tree has as uncoupled sectors the first M uncoupled sectors of the input tree f, whereas its coupled sector corresponds to the internal sector between uncoupled sectors M and M+1 of the original tree f. The second tree has as first uncoupled sector that same internal sector of f, followed by remaining N-M uncoupled sectors of f. It couples to the same sector as f. This operation is the inverse of insertat in the sense that if f₁, f₂ = split(t, M) ⇒ f == insertat(f₂, 1, f₁).

source
TensorKit.mergeFunction
merge(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, c::I, μ = 1)
+-> <:AbstractDict{<:FusionTree{I, N₁+N₂}, <:Number}

Merge two fusion trees together to a linear combination of fusion trees whose uncoupled sectors are those of f₁ followed by those of f₂, and where the two coupled sectors of f₁ and f₂ are further fused to c. In case of FusionStyle(I) == GenericFusion(), also a degeneracy label μ for the fusion of the coupled sectors of f₁ and f₂ to c needs to be specified.

source
TensorKit.elementary_traceFunction
elementary_trace(f::FusionTree{I,N}, i) where {I<:Sector,N} -> <:AbstractDict{FusionTree{I,N-2}, <:Number}

Perform an elementary trace of neighbouring uncoupled indices i and i+1 on a fusion tree f, and returns the result as a dictionary of output trees and corresponding coefficients.

source
TensorKit.planar_traceMethod
planar_trace(f::FusionTree{I,N}, q1::IndexTuple{N₃}, q2::IndexTuple{N₃}) where {I<:Sector,N,N₃}
+    -> <:AbstractDict{FusionTree{I,N-2*N₃}, <:Number}

Perform a planar trace of the uncoupled indices of the fusion tree f at q1 with those at q2, where q1[i] is connected to q2[i] for all i. The result is returned as a dictionary of output trees and corresponding coefficients.

source
TensorKit.artin_braidFunction
artin_braid(f::FusionTree, i; inv::Bool = false) -> <:AbstractDict{typeof(f), <:Number}

Perform an elementary braid (Artin generator) of neighbouring uncoupled indices i and i+1 on a fusion tree f, and returns the result as a dictionary of output trees and corresponding coefficients.

The keyword inv determines whether index i will braid above or below index i+1, i.e. applying artin_braid(f′, i; inv = true) to all the outputs f′ of artin_braid(f, i; inv = false) and collecting the results should yield a single fusion tree with non-zero coefficient, namely f with coefficient 1. This keyword has no effect if BraidingStyle(sectortype(f)) isa SymmetricBraiding.

source
TensorKit.braidMethod
braid(f::FusionTree{<:Sector, N}, levels::NTuple{N, Int}, p::NTuple{N, Int})
+-> <:AbstractDict{typeof(t), <:Number}

Perform a braiding of the uncoupled indices of the fusion tree f and return the result as a <:AbstractDict of output trees and corresponding coefficients. The braiding is determined by specifying that the new sector at position k corresponds to the sector that was originally at the position i = p[k], and assigning to every index i of the original fusion tree a distinct level or depth levels[i]. This permutation is then decomposed into elementary swaps between neighbouring indices, where the swaps are applied as braids such that if i and j cross, $τ_{i,j}$ is applied if levels[i] < levels[j] and $τ_{j,i}^{-1}$ if levels[i] > levels[j]. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations.

source
TensorKit.permuteMethod
permute(f::FusionTree, p::NTuple{N, Int}) -> <:AbstractDict{typeof(t), <:Number}

Perform a permutation of the uncoupled indices of the fusion tree f and returns the result as a <:AbstractDict of output trees and corresponding coefficients; this requires that BraidingStyle(sectortype(f)) isa SymmetricBraiding.

source

These can be composed to implement elementary manipulations of fusion-splitting tree pairs, according to the following methods

# TODO: add documentation for the following methods
 TensorKit.bendright
 TensorKit.bendleft
 TensorKit.foldright
 TensorKit.foldleft
 TensorKit.cycleclockwise
 TensorKit.cycleanticlockwise

Finally, these are used to define large manipulations of fusion-splitting tree pairs, which are then used in the index manipulation of AbstractTensorMap objects. The following methods defined on fusion splitting tree pairs have an associated definition for tensors.

TensorKit.repartitionFunction
repartition(f₁::FusionTree{I, N₁}, f₂::FusionTree{I, N₂}, N::Int) where {I, N₁, N₂}
--> <:AbstractDict{Tuple{FusionTree{I, N}, FusionTree{I, N₁+N₂-N}}, <:Number}

Input is a double fusion tree that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the individual trees of outgoing (f₁) and incoming sectors (f₂) respectively (with identical coupled sector f₁.coupled == f₂.coupled). Computes new trees and corresponding coefficients obtained from repartitioning the tree by bending incoming to outgoing sectors (or vice versa) in order to have N outgoing sectors.

source
repartition(tsrc::AbstractTensorMap{S}, N₁::Int, N₂::Int; copy::Bool=false) where {S}
-    -> tdst::AbstractTensorMap{S,N₁,N₂}

Return tensor tdst obtained by repartitioning the indices of t. The codomain and domain of tdst correspond to the first N₁ and last N₂ spaces of t, respectively.

If copy=false, tdst might share data with tsrc whenever possible. Otherwise, a copy is always made.

To repartition into an existing destination, see repartition!.

source
Base.transposeMethod
transpose(f₁::FusionTree{I}, f₂::FusionTree{I},
+-> <:AbstractDict{Tuple{FusionTree{I, N}, FusionTree{I, N₁+N₂-N}}, <:Number}

Input is a double fusion tree that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the individual trees of outgoing (f₁) and incoming sectors (f₂) respectively (with identical coupled sector f₁.coupled == f₂.coupled). Computes new trees and corresponding coefficients obtained from repartitioning the tree by bending incoming to outgoing sectors (or vice versa) in order to have N outgoing sectors.

source
repartition(tsrc::AbstractTensorMap{S}, N₁::Int, N₂::Int; copy::Bool=false) where {S}
+    -> tdst::AbstractTensorMap{S,N₁,N₂}

Return tensor tdst obtained by repartitioning the indices of t. The codomain and domain of tdst correspond to the first N₁ and last N₂ spaces of t, respectively.

If copy=false, tdst might share data with tsrc whenever possible. Otherwise, a copy is always made.

To repartition into an existing destination, see repartition!.

source
Base.transposeMethod
transpose(f₁::FusionTree{I}, f₂::FusionTree{I},
         p1::NTuple{N₁, Int}, p2::NTuple{N₂, Int}) where {I, N₁, N₂}
--> <:AbstractDict{Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}}, <:Number}

Input is a double fusion tree that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the individual trees of outgoing (t1) and incoming sectors (t2) respectively (with identical coupled sector t1.coupled == t2.coupled). Computes new trees and corresponding coefficients obtained from repartitioning and permuting the tree such that sectors p1 become outgoing and sectors p2 become incoming.

source
TensorKit.braidMethod
braid(f₁::FusionTree{I}, f₂::FusionTree{I},
+-> <:AbstractDict{Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}}, <:Number}

Input is a double fusion tree that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the individual trees of outgoing (t1) and incoming sectors (t2) respectively (with identical coupled sector t1.coupled == t2.coupled). Computes new trees and corresponding coefficients obtained from repartitioning and permuting the tree such that sectors p1 become outgoing and sectors p2 become incoming.

source
TensorKit.braidMethod
braid(f₁::FusionTree{I}, f₂::FusionTree{I},
         levels1::IndexTuple, levels2::IndexTuple,
         p1::IndexTuple{N₁}, p2::IndexTuple{N₂}) where {I<:Sector, N₁, N₂}
--> <:AbstractDict{Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}}, <:Number}

Input is a fusion-splitting tree pair that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the splitting tree f₁ and fusion tree f₂, such that the incoming sectors f₂.uncoupled are fused to f₁.coupled == f₂.coupled and then to the outgoing sectors f₁.uncoupled. Compute new trees and corresponding coefficients obtained from repartitioning and braiding the tree such that sectors p1 become outgoing and sectors p2 become incoming. The uncoupled indices in splitting tree f₁ and fusion tree f₂ have levels (or depths) levels1 and levels2 respectively, which determines how indices braid. In particular, if i and j cross, $τ_{i,j}$ is applied if levels[i] < levels[j] and $τ_{j,i}^{-1}$ if levels[i] > levels[j]. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations.

source
TensorKit.permuteMethod
permute(f₁::FusionTree{I}, f₂::FusionTree{I},
+-> <:AbstractDict{Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}}, <:Number}

Input is a fusion-splitting tree pair that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the splitting tree f₁ and fusion tree f₂, such that the incoming sectors f₂.uncoupled are fused to f₁.coupled == f₂.coupled and then to the outgoing sectors f₁.uncoupled. Compute new trees and corresponding coefficients obtained from repartitioning and braiding the tree such that sectors p1 become outgoing and sectors p2 become incoming. The uncoupled indices in splitting tree f₁ and fusion tree f₂ have levels (or depths) levels1 and levels2 respectively, which determines how indices braid. In particular, if i and j cross, $τ_{i,j}$ is applied if levels[i] < levels[j] and $τ_{j,i}^{-1}$ if levels[i] > levels[j]. This does not allow to encode the most general braid, but a general braid can be obtained by combining such operations.

source
TensorKit.permuteMethod
permute(f₁::FusionTree{I}, f₂::FusionTree{I},
         p1::NTuple{N₁, Int}, p2::NTuple{N₂, Int}) where {I, N₁, N₂}
--> <:AbstractDict{Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}}, <:Number}

Input is a double fusion tree that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the individual trees of outgoing (t1) and incoming sectors (t2) respectively (with identical coupled sector t1.coupled == t2.coupled). Computes new trees and corresponding coefficients obtained from repartitioning and permuting the tree such that sectors p1 become outgoing and sectors p2 become incoming.

source
+-> <:AbstractDict{Tuple{FusionTree{I, N₁}, FusionTree{I, N₂}}, <:Number}

Input is a double fusion tree that describes the fusion of a set of incoming uncoupled sectors to a set of outgoing uncoupled sectors, represented using the individual trees of outgoing (t1) and incoming sectors (t2) respectively (with identical coupled sector t1.coupled == t2.coupled). Computes new trees and corresponding coefficients obtained from repartitioning and permuting the tree such that sectors p1 become outgoing and sectors p2 become incoming.

source diff --git a/dev/lib/spaces/index.html b/dev/lib/spaces/index.html index cf7e9d72..cce5247b 100644 --- a/dev/lib/spaces/index.html +++ b/dev/lib/spaces/index.html @@ -1,12 +1,12 @@ -Vector spaces · TensorKit.jl

Vector spaces

Type hierarchy

The following types are defined to characterise vector spaces and their properties:

TensorKit.FieldType
abstract type Field end

Abstract type at the top of the type hierarchy for denoting fields over which vector spaces (or more generally, linear categories) can be defined. Two common fields are and , representing the field of real or complex numbers respectively.

source
TensorKit.VectorSpaceType
abstract type VectorSpace end

Abstract type at the top of the type hierarchy for denoting vector spaces, or, more accurately, 𝕜-linear categories. All instances of subtypes of VectorSpace will represent objects in 𝕜-linear monoidal categories.

source
TensorKit.ElementarySpaceType
abstract type ElementarySpace <: VectorSpace end

Elementary finite-dimensional vector space over a field that can be used as the index space corresponding to the indices of a tensor. ElementarySpace is a supertype for all vector spaces (objects) that can be associated with the individual indices of a tensor, as hinted to by its alias IndexSpace.

Every elementary vector space should respond to the methods conj and dual, returning the complex conjugate space and the dual space respectively. The complex conjugate of the dual space is obtained as dual(conj(V)) === conj(dual(V)). These different spaces should be of the same type, so that a tensor can be defined as an element of a homogeneous tensor product of these spaces.

source
TensorKit.GeneralSpaceType
struct GeneralSpace{𝔽} <: ElementarySpace

A finite-dimensional space over an arbitrary field 𝔽 without additional structure. It is thus characterized by its dimension, and whether or not it is the dual and/or conjugate space. For a real field 𝔽, the space and its conjugate are the same.

source
TensorKit.CartesianSpaceType
struct CartesianSpace <: ElementarySpace

A real Euclidean space ℝ^d, which is therefore self-dual. CartesianSpace has no additonal structure and is completely characterised by its dimension d. This is the vector space that is implicitly assumed in most of matrix algebra.

source
TensorKit.ComplexSpaceType
struct ComplexSpace <: ElementarySpace

A standard complex vector space ℂ^d with Euclidean inner product and no additional structure. It is completely characterised by its dimension and whether its the normal space or its dual (which is canonically isomorphic to the conjugate space).

source
TensorKit.GradedSpaceType
struct GradedSpace{I<:Sector, D} <: ElementarySpace
+Vector spaces · TensorKit.jl

Vector spaces

Type hierarchy

The following types are defined to characterise vector spaces and their properties:

TensorKit.FieldType
abstract type Field end

Abstract type at the top of the type hierarchy for denoting fields over which vector spaces (or more generally, linear categories) can be defined. Two common fields are and , representing the field of real or complex numbers respectively.

source
TensorKit.VectorSpaceType
abstract type VectorSpace end

Abstract type at the top of the type hierarchy for denoting vector spaces, or, more accurately, 𝕜-linear categories. All instances of subtypes of VectorSpace will represent objects in 𝕜-linear monoidal categories.

source
TensorKit.ElementarySpaceType
abstract type ElementarySpace <: VectorSpace end

Elementary finite-dimensional vector space over a field that can be used as the index space corresponding to the indices of a tensor. ElementarySpace is a supertype for all vector spaces (objects) that can be associated with the individual indices of a tensor, as hinted to by its alias IndexSpace.

Every elementary vector space should respond to the methods conj and dual, returning the complex conjugate space and the dual space respectively. The complex conjugate of the dual space is obtained as dual(conj(V)) === conj(dual(V)). These different spaces should be of the same type, so that a tensor can be defined as an element of a homogeneous tensor product of these spaces.

source
TensorKit.GeneralSpaceType
struct GeneralSpace{𝔽} <: ElementarySpace

A finite-dimensional space over an arbitrary field 𝔽 without additional structure. It is thus characterized by its dimension, and whether or not it is the dual and/or conjugate space. For a real field 𝔽, the space and its conjugate are the same.

source
TensorKit.CartesianSpaceType
struct CartesianSpace <: ElementarySpace

A real Euclidean space ℝ^d, which is therefore self-dual. CartesianSpace has no additonal structure and is completely characterised by its dimension d. This is the vector space that is implicitly assumed in most of matrix algebra.

source
TensorKit.ComplexSpaceType
struct ComplexSpace <: ElementarySpace

A standard complex vector space ℂ^d with Euclidean inner product and no additional structure. It is completely characterised by its dimension and whether its the normal space or its dual (which is canonically isomorphic to the conjugate space).

source
TensorKit.GradedSpaceType
struct GradedSpace{I<:Sector, D} <: ElementarySpace
     dims::D
     dual::Bool
-end

A complex Euclidean space with a direct sum structure corresponding to labels in a set I, the objects of which have the structure of a monoid with respect to a monoidal product . In practice, we restrict the label set to be a set of superselection sectors of type I<:Sector, e.g. the set of distinct irreps of a finite or compact group, or the isomorphism classes of simple objects of a unitary and pivotal (pre-)fusion category.

Here dims represents the degeneracy or multiplicity of every sector.

The data structure D of dims will depend on the result Base.IteratorElsize(values(I)); if the result is of type HasLength or HasShape, dims will be stored in a NTuple{N,Int} with N = length(values(I)). This requires that a sector s::I can be transformed into an index via s == getindex(values(I), i) and i == findindex(values(I), s). If Base.IteratorElsize(values(I)) results IsInfinite() or SizeUnknown(), a SectorDict{I,Int} is used to store the non-zero degeneracy dimensions with the corresponding sector as key. The parameter D is hidden from the user and should typically be of no concern.

The concrete type GradedSpace{I,D} with correct D can be obtained as Vect[I], or if I == Irrep[G] for some G<:Group, as Rep[G].

source
TensorKit.CompositeSpaceType
abstract type CompositeSpace{S<:ElementarySpace} <: VectorSpace end

Abstract type for composite spaces that are defined in terms of a number of elementary vector spaces of a homogeneous type S<:ElementarySpace.

source
TensorKit.ProductSpaceType
struct ProductSpace{S<:ElementarySpace, N} <: CompositeSpace{S}

A ProductSpace is a tensor product space of N vector spaces of type S<:ElementarySpace. Only tensor products between ElementarySpace objects of the same type are allowed.

source
TensorKit.HomSpaceType
struct HomSpace{S<:ElementarySpace, P1<:CompositeSpace{S}, P2<:CompositeSpace{S}}
+end

A complex Euclidean space with a direct sum structure corresponding to labels in a set I, the objects of which have the structure of a monoid with respect to a monoidal product . In practice, we restrict the label set to be a set of superselection sectors of type I<:Sector, e.g. the set of distinct irreps of a finite or compact group, or the isomorphism classes of simple objects of a unitary and pivotal (pre-)fusion category.

Here dims represents the degeneracy or multiplicity of every sector.

The data structure D of dims will depend on the result Base.IteratorElsize(values(I)); if the result is of type HasLength or HasShape, dims will be stored in a NTuple{N,Int} with N = length(values(I)). This requires that a sector s::I can be transformed into an index via s == getindex(values(I), i) and i == findindex(values(I), s). If Base.IteratorElsize(values(I)) results IsInfinite() or SizeUnknown(), a SectorDict{I,Int} is used to store the non-zero degeneracy dimensions with the corresponding sector as key. The parameter D is hidden from the user and should typically be of no concern.

The concrete type GradedSpace{I,D} with correct D can be obtained as Vect[I], or if I == Irrep[G] for some G<:Group, as Rep[G].

source
TensorKit.CompositeSpaceType
abstract type CompositeSpace{S<:ElementarySpace} <: VectorSpace end

Abstract type for composite spaces that are defined in terms of a number of elementary vector spaces of a homogeneous type S<:ElementarySpace.

source
TensorKit.ProductSpaceType
struct ProductSpace{S<:ElementarySpace, N} <: CompositeSpace{S}

A ProductSpace is a tensor product space of N vector spaces of type S<:ElementarySpace. Only tensor products between ElementarySpace objects of the same type are allowed.

source
TensorKit.HomSpaceType
struct HomSpace{S<:ElementarySpace, P1<:CompositeSpace{S}, P2<:CompositeSpace{S}}
     codomain::P1
     domain::P2
-end

Represents the linear space of morphisms with codomain of type P1 and domain of type P2. Note that HomSpace is not a subtype of VectorSpace, i.e. we restrict the latter to denote certain categories and their objects, and keep HomSpace distinct.

source

together with the following specific types for encoding the inner product structure of a space:

TensorKit.InnerProductStyleType
InnerProductStyle(V::VectorSpace) -> ::InnerProductStyle
-InnerProductStyle(S::Type{<:VectorSpace}) -> ::InnerProductStyle

Return the type of inner product for vector spaces, which can be either

  • NoInnerProduct(): no mapping from dual(V) to conj(V), i.e. no metric
  • subtype of HasInnerProduct: a metric exists, but no further support is implemented.
  • EuclideanInnerProduct(): the metric is the identity, such that dual and conjugate spaces are isomorphic.
source

Useful constants

The following constants are defined to easily create the concrete type of GradedSpace associated with a given type of sector.

TensorKit.VectConstant
const Vect

A constant of a singleton type used as Vect[I] with I<:Sector a type of sector, to construct or obtain the concrete type GradedSpace{I,D} instances without having to specify D.

source
TensorKit.RepConstant
const Rep

A constant of a singleton type used as Rep[G] with G<:Group a type of group, to construct or obtain the concrete type GradedSpace{Irrep[G],D} instances without having to specify D. Note that Rep[G] == Vect[Irrep[G]].

See also Irrep and Vect.

source

In this respect, there are also a number of type aliases for the GradedSpace types associated with the most common sectors, namely

const ZNSpace{N} = Vect[ZNIrrep{N}]
+end

Represents the linear space of morphisms with codomain of type P1 and domain of type P2. Note that HomSpace is not a subtype of VectorSpace, i.e. we restrict the latter to denote certain categories and their objects, and keep HomSpace distinct.

source

together with the following specific types for encoding the inner product structure of a space:

TensorKit.InnerProductStyleType
InnerProductStyle(V::VectorSpace) -> ::InnerProductStyle
+InnerProductStyle(S::Type{<:VectorSpace}) -> ::InnerProductStyle

Return the type of inner product for vector spaces, which can be either

  • NoInnerProduct(): no mapping from dual(V) to conj(V), i.e. no metric
  • subtype of HasInnerProduct: a metric exists, but no further support is implemented.
  • EuclideanInnerProduct(): the metric is the identity, such that dual and conjugate spaces are isomorphic.
source

Useful constants

The following constants are defined to easily create the concrete type of GradedSpace associated with a given type of sector.

TensorKit.VectConstant
const Vect

A constant of a singleton type used as Vect[I] with I<:Sector a type of sector, to construct or obtain the concrete type GradedSpace{I,D} instances without having to specify D.

source
TensorKit.RepConstant
const Rep

A constant of a singleton type used as Rep[G] with G<:Group a type of group, to construct or obtain the concrete type GradedSpace{Irrep[G],D} instances without having to specify D. Note that Rep[G] == Vect[Irrep[G]].

See also Irrep and Vect.

source

In this respect, there are also a number of type aliases for the GradedSpace types associated with the most common sectors, namely

const ZNSpace{N} = Vect[ZNIrrep{N}]
 const Z2Space = ZNSpace{2}
 const Z3Space = ZNSpace{3}
 const Z4Space = ZNSpace{4}
@@ -20,13 +20,13 @@
 const ℤ₄Space = Z4Space
 const U₁Space = U1Space
 const CU₁Space = CU1Space
-const SU₂Space = SU2Space

Methods

Methods often apply similar to e.g. spaces and corresponding tensors or tensor maps, e.g.:

TensorKit.fieldFunction
field(V::VectorSpace) -> Field

Return the field type over which a vector space is defined.

source
TensorKit.sectortypeFunction
sectortype(a) -> Type{<:Sector}

Return the type of sector over which object a (e.g. a representation space or a tensor) is defined. Also works in type domain.

source
sectortype(::AbstractTensorMap) -> Type{I<:Sector}
-sectortype(::Type{<:AbstractTensorMap}) -> Type{I<:Sector}

Return the type of sector I of a tensor.

source
TensorKit.hassectorFunction
hassector(V::VectorSpace, a::Sector) -> Bool

Return whether a vector space V has a subspace corresponding to sector a with non-zero dimension, i.e. dim(V, a) > 0.

source
TensorKitSectors.dimMethod
dim(V::ElementarySpace, s::Sector) -> Int

Return the degeneracy dimension corresponding to the sector s of the vector space V.

source
TensorKit.reduceddimFunction
reduceddim(V::ElementarySpace) -> Int

Return the sum of all degeneracy dimensions of the vector space V.

source
TensorKitSectors.dimMethod
dim(P::ProductSpace{S, N}, s::NTuple{N, sectortype(S)}) where {S<:ElementarySpace}
--> Int

Return the total degeneracy dimension corresponding to a tuple of sectors for each of the spaces in the tensor product, obtained as prod(dims(P, s))`.

source
TensorKitSectors.dimMethod
dim(W::HomSpace)

Return the total dimension of a HomSpace, i.e. the number of linearly independent morphisms that can be constructed within this space.

source
TensorKit.dimsFunction
dims(::ProductSpace{S, N}) -> Dims{N} = NTuple{N, Int}

Return the dimensions of the spaces in the tensor product space as a tuple of integers.

source
dims(P::ProductSpace{S, N}, s::NTuple{N, sectortype(S)}) where {S<:ElementarySpace}
--> Dims{N} = NTuple{N, Int}

Return the degeneracy dimensions corresponding to a tuple of sectors s for each of the spaces in the tensor product P.

source
TensorKit.blocksectorsMethod
blocksectors(P::ProductSpace)

Return an iterator over the different unique coupled sector labels, i.e. the different fusion outputs that can be obtained by fusing the sectors present in the different spaces that make up the ProductSpace instance.

source
TensorKit.blocksectorsMethod
blocksectors(W::HomSpace)

Return an iterator over the different unique coupled sector labels, i.e. the intersection of the different fusion outputs that can be obtained by fusing the sectors present in the domain, as well as from the codomain.

See also hasblock.

source
TensorKit.hasblockFunction
hasblock(P::ProductSpace, c::Sector)

Query whether a coupled sector c appears with nonzero dimension in P, i.e. whether blockdim(P, c) > 0.

See also blockdim and blocksectors.

source
hasblock(W::HomSpace, c::Sector)

Query whether a coupled sector c appears in both the codomain and domain of W.

See also blocksectors.

source
hasblock(t::AbstractTensorMap, c::Sector) -> Bool

Verify whether a tensor has a block corresponding to a coupled sector c.

source
TensorKit.blockdimFunction
blockdim(P::ProductSpace, c::Sector)

Return the total dimension of a coupled sector c in the product space, by summing over all dim(P, s) for all tuples of sectors s::NTuple{N, <:Sector} that can fuse to c, counted with the correct multiplicity (i.e. number of ways in which s can fuse to c).

See also hasblock and blocksectors.

source
TensorKit.fusiontreesMethod
fusiontrees(P::ProductSpace, blocksector::Sector)

Return an iterator over all fusion trees that can be formed by fusing the sectors present in the different spaces that make up the ProductSpace instance into the coupled sector blocksector.

source

The following methods act specifically on ElementarySpace spaces:

TensorKit.isdualFunction
isdual(V::ElementarySpace) -> Bool

Return wether an ElementarySpace V is normal or rather a dual space. Always returns false for spaces where V == dual(V).

source
TensorKitSectors.dualFunction
dual(V::VectorSpace) -> VectorSpace

Return the dual space of V; also obtained via V'. This should satisfy dual(dual(V)) == V. It is assumed that typeof(V) == typeof(V').

source
Base.conjFunction
conj(V::S) where {S<:ElementarySpace} -> S

Return the conjugate space of V. This should satisfy conj(conj(V)) == V.

For field(V)==ℝ, conj(V) == V. It is assumed that typeof(V) == typeof(conj(V)).

source
TensorKit.flipFunction
flip(V::S) where {S<:ElementarySpace} -> S

Return a single vector space of type S that has the same value of isdual as dual(V), but yet is isomorphic to V rather than to dual(V). The spaces flip(V) and dual(V) only differ in the case of GradedSpace{I}.

source
TensorKit.:⊕Function
⊕(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} -> S
-oplus(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} -> S

Return the corresponding vector space of type S that represents the direct sum sum of the spaces V₁, V₂, ... Note that all the individual spaces should have the same value for isdual, as otherwise the direct sum is not defined.

source
Base.zeroMethod
zero(V::S) where {S<:ElementarySpace} -> S

Return the corresponding vector space of type S that represents the zero-dimensional or empty space. This is, with a slight abuse of notation, the zero element of the direct sum of vector spaces.

source
Base.oneunitFunction
oneunit(V::S) where {S<:ElementarySpace} -> S

Return the corresponding vector space of type S that represents the trivial one-dimensional space, i.e. the space that is isomorphic to the corresponding field. Note that this is different from one(V::S), which returns the empty product space ProductSpace{S,0}(()).

source
TensorKit.supremumFunction
supremum(V₁::ElementarySpace, V₂::ElementarySpace, V₃::ElementarySpace...)

Return the supremum of a number of elementary spaces, i.e. an instance V::ElementarySpace such that V ≿ V₁, V ≿ V₂, ... and no other W ≺ V has this property. This requires that all arguments have the same value of isdual( ), and also the return value V will have the same value.

source
TensorKit.infimumFunction
infimum(V₁::ElementarySpace, V₂::ElementarySpace, V₃::ElementarySpace...)

Return the infimum of a number of elementary spaces, i.e. an instance V::ElementarySpace such that V ≾ V₁, V ≾ V₂, ... and no other W ≻ V has this property. This requires that all arguments have the same value of isdual( ), and also the return value V will have the same value.

source

while the following also work on both ElementarySpace and ProductSpace

Base.oneMethod
one(::S) where {S<:ElementarySpace} -> ProductSpace{S, 0}
-one(::ProductSpace{S}) where {S<:ElementarySpace} -> ProductSpace{S, 0}

Return a tensor product of zero spaces of type S, i.e. this is the unit object under the tensor product operation, such that V ⊗ one(V) == V.

source
TensorKit.fuseFunction
fuse(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} -> S
-fuse(P::ProductSpace{S}) where {S<:ElementarySpace} -> S

Return a single vector space of type S that is isomorphic to the fusion product of the individual spaces V₁, V₂, ..., or the spaces contained in P.

source
TensorKitSectors.:⊗Method
⊗(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} -> S

Create a ProductSpace{S}(V₁, V₂, V₃...) representing the tensor product of several elementary vector spaces. For convience, Julia's regular multiplication operator * applied to vector spaces has the same effect.

The tensor product structure is preserved, see fuse for returning a single elementary space of type S that is isomorphic to this tensor product.

source
TensorKitSectors.:⊠Method
⊠(V₁::VectorSpace, V₂::VectorSpace)

Given two vector spaces V₁ and V₂ (ElementarySpace or ProductSpace), or thus, objects of corresponding fusion categories $C₁$ and $C₂$, $V₁ ⊠ V₂$ constructs the Deligne tensor product, an object in $C₁ ⊠ C₂$ which is the natural tensor product of those categories. In particular, the corresponding type of sectors (simple objects) is given by sectortype(V₁ ⊠ V₂) == sectortype(V₁) ⊠ sectortype(V₂) and can be thought of as a tuple of the individual sectors.

The Deligne tensor product also works in the type domain and for sectors and tensors. For group representations, we have Rep[G₁] ⊠ Rep[G₂] == Rep[G₁ × G₂], i.e. these are the natural representation spaces of the direct product of two groups.

source
TensorKit.ismonomorphicFunction
ismonomorphic(V₁::VectorSpace, V₂::VectorSpace)
-V₁ ≾ V₂

Return whether there exist monomorphisms from V₁ to V₂, i.e. 'injective' morphisms with left inverses.

source
TensorKit.isepimorphicFunction
isepimorphic(V₁::VectorSpace, V₂::VectorSpace)
-V₁ ≿ V₂

Return whether there exist epimorphisms from V₁ to V₂, i.e. 'surjective' morphisms with right inverses.

source
TensorKit.isisomorphicFunction
isisomorphic(V₁::VectorSpace, V₂::VectorSpace)
-V₁ ≅ V₂

Return if V₁ and V₂ are isomorphic, meaning that there exists isomorphisms from V₁ to V₂, i.e. morphisms with left and right inverses.

source
TensorKit.insertunitFunction
insertunit(P::ProductSpace, i::Int = length(P)+1; dual = false, conj = false)

For P::ProductSpace{S,N}, this adds an extra tensor product factor at position 1 <= i <= N+1 (last position by default) which is just the S-equivalent of the underlying field of scalars, i.e. oneunit(S). With the keyword arguments, one can choose to insert the conjugated or dual space instead, which are all isomorphic to the field of scalars.

source

There are also specific methods for HomSpace instances, that are used in determining the resuling HomSpace after applying certain tensor operations.

TensorKit.permuteMethod
permute(W::HomSpace, (p₁, p₂)::Index2Tuple{N₁,N₂})

Return the HomSpace obtained by permuting the indices of the domain and codomain of W according to the permutation p₁ and p₂ respectively.

source
TensorKit.selectMethod
select(W::HomSpace, (p₁, p₂)::Index2Tuple{N₁,N₂})

Return the HomSpace obtained by a selection from the domain and codomain of W according to the indices in p₁ and p₂ respectively.

source
TensorKit.composeMethod
compose(W::HomSpace, V::HomSpace)

Obtain the HomSpace that is obtained from composing the morphisms in W and V. For this to be possible, the domain of W must match the codomain of V.

source
+const SU₂Space = SU2Space

Methods

Methods often apply similar to e.g. spaces and corresponding tensors or tensor maps, e.g.:

TensorKit.fieldFunction
field(V::VectorSpace) -> Field

Return the field type over which a vector space is defined.

source
TensorKit.sectortypeFunction
sectortype(a) -> Type{<:Sector}

Return the type of sector over which object a (e.g. a representation space or a tensor) is defined. Also works in type domain.

source
sectortype(::AbstractTensorMap) -> Type{I<:Sector}
+sectortype(::Type{<:AbstractTensorMap}) -> Type{I<:Sector}

Return the type of sector I of a tensor.

source
TensorKit.sectorsFunction
sectors(V::ElementarySpace)

Return an iterator over the different sectors of V.

source
TensorKit.hassectorFunction
hassector(V::VectorSpace, a::Sector) -> Bool

Return whether a vector space V has a subspace corresponding to sector a with non-zero dimension, i.e. dim(V, a) > 0.

source
TensorKitSectors.dimMethod
dim(V::VectorSpace) -> Int

Return the total dimension of the vector space V as an Int.

source
TensorKitSectors.dimMethod
dim(V::ElementarySpace, s::Sector) -> Int

Return the degeneracy dimension corresponding to the sector s of the vector space V.

source
TensorKit.reduceddimFunction
reduceddim(V::ElementarySpace) -> Int

Return the sum of all degeneracy dimensions of the vector space V.

source
TensorKitSectors.dimMethod
dim(P::ProductSpace{S, N}, s::NTuple{N, sectortype(S)}) where {S<:ElementarySpace}
+-> Int

Return the total degeneracy dimension corresponding to a tuple of sectors for each of the spaces in the tensor product, obtained as prod(dims(P, s))`.

source
TensorKitSectors.dimMethod
dim(W::HomSpace)

Return the total dimension of a HomSpace, i.e. the number of linearly independent morphisms that can be constructed within this space.

source
TensorKit.dimsFunction
dims(::ProductSpace{S, N}) -> Dims{N} = NTuple{N, Int}

Return the dimensions of the spaces in the tensor product space as a tuple of integers.

source
dims(P::ProductSpace{S, N}, s::NTuple{N, sectortype(S)}) where {S<:ElementarySpace}
+-> Dims{N} = NTuple{N, Int}

Return the degeneracy dimensions corresponding to a tuple of sectors s for each of the spaces in the tensor product P.

source
TensorKit.blocksectorsMethod
blocksectors(P::ProductSpace)

Return an iterator over the different unique coupled sector labels, i.e. the different fusion outputs that can be obtained by fusing the sectors present in the different spaces that make up the ProductSpace instance.

source
TensorKit.blocksectorsMethod
blocksectors(W::HomSpace)

Return an iterator over the different unique coupled sector labels, i.e. the intersection of the different fusion outputs that can be obtained by fusing the sectors present in the domain, as well as from the codomain.

See also hasblock.

source
TensorKit.hasblockFunction
hasblock(P::ProductSpace, c::Sector)

Query whether a coupled sector c appears with nonzero dimension in P, i.e. whether blockdim(P, c) > 0.

See also blockdim and blocksectors.

source
hasblock(W::HomSpace, c::Sector)

Query whether a coupled sector c appears in both the codomain and domain of W.

See also blocksectors.

source
hasblock(t::AbstractTensorMap, c::Sector) -> Bool

Verify whether a tensor has a block corresponding to a coupled sector c.

source
TensorKit.blockdimFunction
blockdim(P::ProductSpace, c::Sector)

Return the total dimension of a coupled sector c in the product space, by summing over all dim(P, s) for all tuples of sectors s::NTuple{N, <:Sector} that can fuse to c, counted with the correct multiplicity (i.e. number of ways in which s can fuse to c).

See also hasblock and blocksectors.

source
TensorKit.fusiontreesMethod
fusiontrees(P::ProductSpace, blocksector::Sector)

Return an iterator over all fusion trees that can be formed by fusing the sectors present in the different spaces that make up the ProductSpace instance into the coupled sector blocksector.

source
TensorKit.spaceFunction
space(a) -> VectorSpace

Return the vector space associated to object a.

source

The following methods act specifically on ElementarySpace spaces:

TensorKit.isdualFunction
isdual(V::ElementarySpace) -> Bool

Return wether an ElementarySpace V is normal or rather a dual space. Always returns false for spaces where V == dual(V).

source
TensorKitSectors.dualFunction
dual(V::VectorSpace) -> VectorSpace

Return the dual space of V; also obtained via V'. This should satisfy dual(dual(V)) == V. It is assumed that typeof(V) == typeof(V').

source
Base.conjFunction
conj(V::S) where {S<:ElementarySpace} -> S

Return the conjugate space of V. This should satisfy conj(conj(V)) == V.

For field(V)==ℝ, conj(V) == V. It is assumed that typeof(V) == typeof(conj(V)).

source
TensorKit.flipFunction
flip(V::S) where {S<:ElementarySpace} -> S

Return a single vector space of type S that has the same value of isdual as dual(V), but yet is isomorphic to V rather than to dual(V). The spaces flip(V) and dual(V) only differ in the case of GradedSpace{I}.

source
TensorKit.:⊕Function
⊕(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} -> S
+oplus(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} -> S

Return the corresponding vector space of type S that represents the direct sum sum of the spaces V₁, V₂, ... Note that all the individual spaces should have the same value for isdual, as otherwise the direct sum is not defined.

source
Base.zeroMethod
zero(V::S) where {S<:ElementarySpace} -> S

Return the corresponding vector space of type S that represents the zero-dimensional or empty space. This is, with a slight abuse of notation, the zero element of the direct sum of vector spaces.

source
Base.oneunitFunction
oneunit(V::S) where {S<:ElementarySpace} -> S

Return the corresponding vector space of type S that represents the trivial one-dimensional space, i.e. the space that is isomorphic to the corresponding field. Note that this is different from one(V::S), which returns the empty product space ProductSpace{S,0}(()).

source
TensorKit.supremumFunction
supremum(V₁::ElementarySpace, V₂::ElementarySpace, V₃::ElementarySpace...)

Return the supremum of a number of elementary spaces, i.e. an instance V::ElementarySpace such that V ≿ V₁, V ≿ V₂, ... and no other W ≺ V has this property. This requires that all arguments have the same value of isdual( ), and also the return value V will have the same value.

source
TensorKit.infimumFunction
infimum(V₁::ElementarySpace, V₂::ElementarySpace, V₃::ElementarySpace...)

Return the infimum of a number of elementary spaces, i.e. an instance V::ElementarySpace such that V ≾ V₁, V ≾ V₂, ... and no other W ≻ V has this property. This requires that all arguments have the same value of isdual( ), and also the return value V will have the same value.

source

while the following also work on both ElementarySpace and ProductSpace

Base.oneMethod
one(::S) where {S<:ElementarySpace} -> ProductSpace{S, 0}
+one(::ProductSpace{S}) where {S<:ElementarySpace} -> ProductSpace{S, 0}

Return a tensor product of zero spaces of type S, i.e. this is the unit object under the tensor product operation, such that V ⊗ one(V) == V.

source
TensorKit.fuseFunction
fuse(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} -> S
+fuse(P::ProductSpace{S}) where {S<:ElementarySpace} -> S

Return a single vector space of type S that is isomorphic to the fusion product of the individual spaces V₁, V₂, ..., or the spaces contained in P.

source
TensorKitSectors.:⊗Method
⊗(V₁::S, V₂::S, V₃::S...) where {S<:ElementarySpace} -> S

Create a ProductSpace{S}(V₁, V₂, V₃...) representing the tensor product of several elementary vector spaces. For convience, Julia's regular multiplication operator * applied to vector spaces has the same effect.

The tensor product structure is preserved, see fuse for returning a single elementary space of type S that is isomorphic to this tensor product.

source
TensorKitSectors.:⊠Method
⊠(V₁::VectorSpace, V₂::VectorSpace)

Given two vector spaces V₁ and V₂ (ElementarySpace or ProductSpace), or thus, objects of corresponding fusion categories $C₁$ and $C₂$, $V₁ ⊠ V₂$ constructs the Deligne tensor product, an object in $C₁ ⊠ C₂$ which is the natural tensor product of those categories. In particular, the corresponding type of sectors (simple objects) is given by sectortype(V₁ ⊠ V₂) == sectortype(V₁) ⊠ sectortype(V₂) and can be thought of as a tuple of the individual sectors.

The Deligne tensor product also works in the type domain and for sectors and tensors. For group representations, we have Rep[G₁] ⊠ Rep[G₂] == Rep[G₁ × G₂], i.e. these are the natural representation spaces of the direct product of two groups.

source
TensorKit.ismonomorphicFunction
ismonomorphic(V₁::VectorSpace, V₂::VectorSpace)
+V₁ ≾ V₂

Return whether there exist monomorphisms from V₁ to V₂, i.e. 'injective' morphisms with left inverses.

source
TensorKit.isepimorphicFunction
isepimorphic(V₁::VectorSpace, V₂::VectorSpace)
+V₁ ≿ V₂

Return whether there exist epimorphisms from V₁ to V₂, i.e. 'surjective' morphisms with right inverses.

source
TensorKit.isisomorphicFunction
isisomorphic(V₁::VectorSpace, V₂::VectorSpace)
+V₁ ≅ V₂

Return if V₁ and V₂ are isomorphic, meaning that there exists isomorphisms from V₁ to V₂, i.e. morphisms with left and right inverses.

source
TensorKit.insertunitFunction
insertunit(P::ProductSpace, i::Int = length(P)+1; dual = false, conj = false)

For P::ProductSpace{S,N}, this adds an extra tensor product factor at position 1 <= i <= N+1 (last position by default) which is just the S-equivalent of the underlying field of scalars, i.e. oneunit(S). With the keyword arguments, one can choose to insert the conjugated or dual space instead, which are all isomorphic to the field of scalars.

source

There are also specific methods for HomSpace instances, that are used in determining the resuling HomSpace after applying certain tensor operations.

TensorKit.permuteMethod
permute(W::HomSpace, (p₁, p₂)::Index2Tuple{N₁,N₂})

Return the HomSpace obtained by permuting the indices of the domain and codomain of W according to the permutation p₁ and p₂ respectively.

source
TensorKit.selectMethod
select(W::HomSpace, (p₁, p₂)::Index2Tuple{N₁,N₂})

Return the HomSpace obtained by a selection from the domain and codomain of W according to the indices in p₁ and p₂ respectively.

source
TensorKit.composeMethod
compose(W::HomSpace, V::HomSpace)

Obtain the HomSpace that is obtained from composing the morphisms in W and V. For this to be possible, the domain of W must match the codomain of V.

source
diff --git a/dev/lib/tensors/index.html b/dev/lib/tensors/index.html index 737f36df..e129fde3 100644 --- a/dev/lib/tensors/index.html +++ b/dev/lib/tensors/index.html @@ -1,87 +1,87 @@ -Tensors · TensorKit.jl

Tensors

Type hierarchy

The abstract supertype of all tensors in TensorKit is given by AbstractTensorMap:

TensorKit.AbstractTensorMapType
abstract type AbstractTensorMap{T<:Number, S<:IndexSpace, N₁, N₂} end

Abstract supertype of all tensor maps, i.e. linear maps between tensor products of vector spaces of type S<:IndexSpace, with element type T. An AbstractTensorMap maps from an input space of type ProductSpace{S, N₂} to an output space of type ProductSpace{S, N₁}.

source

The following concrete subtypes are provided within the TensorKit library:

TensorKit.TensorMapType
struct TensorMap{T, S<:IndexSpace, N₁, N₂, A<:DenseVector{T}} <: AbstractTensorMap{T, S, N₁, N₂}

Specific subtype of AbstractTensorMap for representing tensor maps (morphisms in a tensor category), where the data is stored in a dense vector.

source
TensorKit.DiagonalTensorMapType
DiagonalTensorMap{T}(undef, domain::S) where {T,S<:IndexSpace}
+Tensors · TensorKit.jl

Tensors

Type hierarchy

The abstract supertype of all tensors in TensorKit is given by AbstractTensorMap:

TensorKit.AbstractTensorMapType
abstract type AbstractTensorMap{T<:Number, S<:IndexSpace, N₁, N₂} end

Abstract supertype of all tensor maps, i.e. linear maps between tensor products of vector spaces of type S<:IndexSpace, with element type T. An AbstractTensorMap maps from an input space of type ProductSpace{S, N₂} to an output space of type ProductSpace{S, N₁}.

source

The following concrete subtypes are provided within the TensorKit library:

TensorKit.TensorMapType
struct TensorMap{T, S<:IndexSpace, N₁, N₂, A<:DenseVector{T}} <: AbstractTensorMap{T, S, N₁, N₂}

Specific subtype of AbstractTensorMap for representing tensor maps (morphisms in a tensor category), where the data is stored in a dense vector.

source
TensorKit.DiagonalTensorMapType
DiagonalTensorMap{T}(undef, domain::S) where {T,S<:IndexSpace}
 # expert mode: select storage type `A`
-DiagonalTensorMap{T,S,A}(undef, domain::S) where {T,S<:IndexSpace,A<:DenseVector{T}}

Construct a DiagonalTensorMap with uninitialized data.

source
TensorKit.BraidingTensorType
struct BraidingTensor{T,S<:IndexSpace} <: AbstractTensorMap{T, S, 2, 2}
-BraidingTensor(V1::S, V2::S, adjoint::Bool=false) where {S<:IndexSpace}

Specific subtype of AbstractTensorMap for representing the braiding tensor that braids the first input over the second input; its inverse can be obtained as the adjoint.

It holds that domain(BraidingTensor(V1, V2)) == V1 ⊗ V2 and codomain(BraidingTensor(V1, V2)) == V2 ⊗ V1.

source

Of those, TensorMap provides the generic instantiation of our tensor concept. It supports various constructors, which are discussed in the next subsection.

Furthermore, some aliases are provided for convenience:

TensorKit.AbstractTensorType
AbstractTensor{T,S,N} = AbstractTensorMap{T,S,N,0}

Abstract supertype of all tensors, i.e. elements in the tensor product space of type ProductSpace{S, N}, with element type T.

An AbstractTensor{T, S, N} is actually a special case AbstractTensorMap{T, S, N, 0}, i.e. a tensor map with only non-trivial output spaces.

source
TensorKit.TensorType
Tensor{T, S, N, A<:DenseVector{T}} = TensorMap{T, S, N, 0, A}

Specific subtype of AbstractTensor for representing tensors whose data is stored in a dense vector.

A Tensor{T, S, N, A} is actually a special case TensorMap{T, S, N, 0, A}, i.e. a tensor map with only a non-trivial output space.

source

TensorMap constructors

General constructors

A TensorMap with undefined data can be constructed by specifying its domain and codomain:

TensorKit.TensorMapMethod
TensorMap{T}(undef, codomain::ProductSpace{S,N₁}, domain::ProductSpace{S,N₂})
+DiagonalTensorMap{T,S,A}(undef, domain::S) where {T,S<:IndexSpace,A<:DenseVector{T}}

Construct a DiagonalTensorMap with uninitialized data.

source
TensorKit.BraidingTensorType
struct BraidingTensor{T,S<:IndexSpace} <: AbstractTensorMap{T, S, 2, 2}
+BraidingTensor(V1::S, V2::S, adjoint::Bool=false) where {S<:IndexSpace}

Specific subtype of AbstractTensorMap for representing the braiding tensor that braids the first input over the second input; its inverse can be obtained as the adjoint.

It holds that domain(BraidingTensor(V1, V2)) == V1 ⊗ V2 and codomain(BraidingTensor(V1, V2)) == V2 ⊗ V1.

source

Of those, TensorMap provides the generic instantiation of our tensor concept. It supports various constructors, which are discussed in the next subsection.

Furthermore, some aliases are provided for convenience:

TensorKit.AbstractTensorType
AbstractTensor{T,S,N} = AbstractTensorMap{T,S,N,0}

Abstract supertype of all tensors, i.e. elements in the tensor product space of type ProductSpace{S, N}, with element type T.

An AbstractTensor{T, S, N} is actually a special case AbstractTensorMap{T, S, N, 0}, i.e. a tensor map with only non-trivial output spaces.

source
TensorKit.TensorType
Tensor{T, S, N, A<:DenseVector{T}} = TensorMap{T, S, N, 0, A}

Specific subtype of AbstractTensor for representing tensors whose data is stored in a dense vector.

A Tensor{T, S, N, A} is actually a special case TensorMap{T, S, N, 0, A}, i.e. a tensor map with only a non-trivial output space.

source

TensorMap constructors

General constructors

A TensorMap with undefined data can be constructed by specifying its domain and codomain:

TensorKit.TensorMapMethod
TensorMap{T}(undef, codomain::ProductSpace{S,N₁}, domain::ProductSpace{S,N₂})
             where {T,S,N₁,N₂}
 TensorMap{T}(undef, codomain ← domain)
 TensorMap{T}(undef, domain → codomain)
 # expert mode: select storage type `A`
 TensorMap{T,S,N₁,N₂,A}(undef, codomain ← domain)
-TensorMap{T,S,N₁,N₂,A}(undef, domain → domain)

Construct a TensorMap with uninitialized data.

source

The resulting object can then be filled with data using the setindex! method as discussed below, using functions such as VectorInterface.zerovector!, rand! or fill!, or it can be used as an output argument in one of the many methods that accept output arguments, or in an @tensor output[...] = ... expression.

Alternatively, a TensorMap can be constructed by specifying its data, codmain and domain in one of the following ways:

TensorKit.TensorMapMethod
TensorMap(data::AbstractDict{<:Sector,<:AbstractMatrix}, codomain::ProductSpace{S,N₁},
+TensorMap{T,S,N₁,N₂,A}(undef, domain → domain)

Construct a TensorMap with uninitialized data.

source

The resulting object can then be filled with data using the setindex! method as discussed below, using functions such as VectorInterface.zerovector!, rand! or fill!, or it can be used as an output argument in one of the many methods that accept output arguments, or in an @tensor output[...] = ... expression.

Alternatively, a TensorMap can be constructed by specifying its data, codmain and domain in one of the following ways:

TensorKit.TensorMapMethod
TensorMap(data::AbstractDict{<:Sector,<:AbstractMatrix}, codomain::ProductSpace{S,N₁},
             domain::ProductSpace{S,N₂}) where {S<:ElementarySpace,N₁,N₂}
 TensorMap(data, codomain ← domain)
-TensorMap(data, domain → codomain)

Construct a TensorMap by explicitly specifying its block data.

Arguments

  • data::AbstractDict{<:Sector,<:AbstractMatrix}: dictionary containing the block data for each coupled sector c as a matrix of size (blockdim(codomain, c), blockdim(domain, c)).
  • codomain::ProductSpace{S,N₁}: the codomain as a ProductSpace of N₁ spaces of type S<:ElementarySpace.
  • domain::ProductSpace{S,N₂}: the domain as a ProductSpace of N₂ spaces of type S<:ElementarySpace.

Alternatively, the domain and codomain can be specified by passing a HomSpace using the syntax codomain ← domain or domain → codomain.

source
TensorKit.TensorMapMethod
TensorMap(data::AbstractArray, codomain::ProductSpace{S,N₁}, domain::ProductSpace{S,N₂};
+TensorMap(data, domain → codomain)

Construct a TensorMap by explicitly specifying its block data.

Arguments

  • data::AbstractDict{<:Sector,<:AbstractMatrix}: dictionary containing the block data for each coupled sector c as a matrix of size (blockdim(codomain, c), blockdim(domain, c)).
  • codomain::ProductSpace{S,N₁}: the codomain as a ProductSpace of N₁ spaces of type S<:ElementarySpace.
  • domain::ProductSpace{S,N₂}: the domain as a ProductSpace of N₂ spaces of type S<:ElementarySpace.

Alternatively, the domain and codomain can be specified by passing a HomSpace using the syntax codomain ← domain or domain → codomain.

source
TensorKit.TensorMapMethod
TensorMap(data::AbstractArray, codomain::ProductSpace{S,N₁}, domain::ProductSpace{S,N₂};
                 tol=sqrt(eps(real(float(eltype(data)))))) where {S<:ElementarySpace,N₁,N₂}
 TensorMap(data, codomain ← domain; tol=sqrt(eps(real(float(eltype(data))))))
-TensorMap(data, domain → codomain; tol=sqrt(eps(real(float(eltype(data))))))

Construct a TensorMap from a plain multidimensional array.

Arguments

  • data::DenseArray: tensor data as a plain array.
  • codomain::ProductSpace{S,N₁}: the codomain as a ProductSpace of N₁ spaces of type S<:ElementarySpace.
  • domain::ProductSpace{S,N₂}: the domain as a ProductSpace of N₂ spaces of type S<:ElementarySpace.
  • tol=sqrt(eps(real(float(eltype(data)))))::Float64:

Here, data can be specified in three ways:

  1. data can be a DenseVector of length dim(codomain ← domain); in that case it represents the actual independent entries of the tensor map. An instance will be created that directly references data.
  2. data can be an AbstractMatrix of size (dim(codomain), dim(domain))
  3. data can be an AbstractArray of rank N₁ + N₂ with a size matching that of the domain and codomain spaces, i.e. size(data) == (dims(codomain)..., dims(domain)...)

In case 2 and 3, the TensorMap constructor will reconstruct the tensor data such that the resulting tensor t satisfies data == convert(Array, t), up to an error specified by tol. For the case where sectortype(S) == Trivial and data isa DenseArray, the data array is simply reshaped into a vector and used as in case 1 so that the memory will still be shared. In other cases, new memory will be allocated.

Note that in the case of N₁ + N₂ = 1, case 3 also amounts to data being a vector, whereas when N₁ + N₂ == 2, case 2 and case 3 both require data to be a matrix. Such ambiguous cases are resolved by checking the size of data in an attempt to support all possible cases.

Note

This constructor for case 2 and 3 only works for sectortype values for which conversion to a plain array is possible, and only in the case where the data actually respects the specified symmetry structure, up to a tolerance tol.

source

Finally, we also support the following Array-like constructors

Base.zerosMethod
zeros([T=Float64,], codomain::ProductSpace{S,N₁}, domain::ProductSpace{S,N₂}) where {S,N₁,N₂,T}
-zeros([T=Float64,], codomain ← domain)

Create a TensorMap with element type T, of all zeros with spaces specified by codomain and domain.

source
Base.onesMethod
ones([T=Float64,], codomain::ProductSpace{S,N₁}, domain::ProductSpace{S,N₂}) where {S,N₁,N₂,T}
-ones([T=Float64,], codomain ← domain)

Create a TensorMap with element type T, of all ones with spaces specified by codomain and domain.

source
Base.randMethod
rand([rng=default_rng()], [T=Float64], codomain::ProductSpace{S,N₁},
+TensorMap(data, domain → codomain; tol=sqrt(eps(real(float(eltype(data))))))

Construct a TensorMap from a plain multidimensional array.

Arguments

  • data::DenseArray: tensor data as a plain array.
  • codomain::ProductSpace{S,N₁}: the codomain as a ProductSpace of N₁ spaces of type S<:ElementarySpace.
  • domain::ProductSpace{S,N₂}: the domain as a ProductSpace of N₂ spaces of type S<:ElementarySpace.
  • tol=sqrt(eps(real(float(eltype(data)))))::Float64:

Here, data can be specified in three ways:

  1. data can be a DenseVector of length dim(codomain ← domain); in that case it represents the actual independent entries of the tensor map. An instance will be created that directly references data.
  2. data can be an AbstractMatrix of size (dim(codomain), dim(domain))
  3. data can be an AbstractArray of rank N₁ + N₂ with a size matching that of the domain and codomain spaces, i.e. size(data) == (dims(codomain)..., dims(domain)...)

In case 2 and 3, the TensorMap constructor will reconstruct the tensor data such that the resulting tensor t satisfies data == convert(Array, t), up to an error specified by tol. For the case where sectortype(S) == Trivial and data isa DenseArray, the data array is simply reshaped into a vector and used as in case 1 so that the memory will still be shared. In other cases, new memory will be allocated.

Note that in the case of N₁ + N₂ = 1, case 3 also amounts to data being a vector, whereas when N₁ + N₂ == 2, case 2 and case 3 both require data to be a matrix. Such ambiguous cases are resolved by checking the size of data in an attempt to support all possible cases.

Note

This constructor for case 2 and 3 only works for sectortype values for which conversion to a plain array is possible, and only in the case where the data actually respects the specified symmetry structure, up to a tolerance tol.

source

Finally, we also support the following Array-like constructors

Base.zerosMethod
zeros([T=Float64,], codomain::ProductSpace{S,N₁}, domain::ProductSpace{S,N₂}) where {S,N₁,N₂,T}
+zeros([T=Float64,], codomain ← domain)

Create a TensorMap with element type T, of all zeros with spaces specified by codomain and domain.

source
Base.onesMethod
ones([T=Float64,], codomain::ProductSpace{S,N₁}, domain::ProductSpace{S,N₂}) where {S,N₁,N₂,T}
+ones([T=Float64,], codomain ← domain)

Create a TensorMap with element type T, of all ones with spaces specified by codomain and domain.

source
Base.randMethod
rand([rng=default_rng()], [T=Float64], codomain::ProductSpace{S,N₁},
              domain::ProductSpace{S,N₂}) where {S,N₁,N₂,T} -> t
-rand([rng=default_rng()], [T=Float64], codomain ← domain) -> t

Generate a tensor t with entries generated by rand.

See also (rand)!.

source
Base.randnMethod
randn([rng=default_rng()], [T=Float64], codomain::ProductSpace{S,N₁},
+rand([rng=default_rng()], [T=Float64], codomain ← domain) -> t

Generate a tensor t with entries generated by rand.

See also (rand)!.

source
Base.randnMethod
randn([rng=default_rng()], [T=Float64], codomain::ProductSpace{S,N₁},
              domain::ProductSpace{S,N₂}) where {S,N₁,N₂,T} -> t
-randn([rng=default_rng()], [T=Float64], codomain ← domain) -> t

Generate a tensor t with entries generated by randn.

See also (randn)!.

source
Random.randexpMethod
randexp([rng=default_rng()], [T=Float64], codomain::ProductSpace{S,N₁},
+randn([rng=default_rng()], [T=Float64], codomain ← domain) -> t

Generate a tensor t with entries generated by randn.

See also (randn)!.

source
Random.randexpMethod
randexp([rng=default_rng()], [T=Float64], codomain::ProductSpace{S,N₁},
              domain::ProductSpace{S,N₂}) where {S,N₁,N₂,T} -> t
-randexp([rng=default_rng()], [T=Float64], codomain ← domain) -> t

Generate a tensor t with entries generated by randexp.

See also (randexp)!.

source

as well as a similar constructor

Base.similarMethod
similar(t::AbstractTensorMap, [AorT=storagetype(t)], [V=space(t)])
-similar(t::AbstractTensorMap, [AorT=storagetype(t)], codomain, domain)

Creates an uninitialized mutable tensor with the given scalar or storagetype AorT and structure V or codomain ← domain, based on the source tensormap. The second and third arguments are both optional, defaulting to the given tensor's storagetype and space. The structure may be specified either as a single HomSpace argument or as codomain and domain.

By default, this will result in TensorMap{T}(undef, V) when custom objects do not specialize this method.

source

Specific constructors

Additionally, the following methods can be used to construct specific TensorMap instances.

TensorKit.idFunction
id([T::Type=Float64,] V::TensorSpace) -> TensorMap

Construct the identity endomorphism on space V, i.e. return a t::TensorMap with domain(t) == codomain(t) == V, where either scalartype(t) = T if T is a Number type or storagetype(t) = T if T is a DenseVector type.

source
TensorKit.isomorphismFunction
isomorphism([T::Type=Float64,] codomain::TensorSpace, domain::TensorSpace) -> TensorMap
+randexp([rng=default_rng()], [T=Float64], codomain ← domain) -> t

Generate a tensor t with entries generated by randexp.

See also (randexp)!.

source

as well as a similar constructor

Base.similarMethod
similar(t::AbstractTensorMap, [AorT=storagetype(t)], [V=space(t)])
+similar(t::AbstractTensorMap, [AorT=storagetype(t)], codomain, domain)

Creates an uninitialized mutable tensor with the given scalar or storagetype AorT and structure V or codomain ← domain, based on the source tensormap. The second and third arguments are both optional, defaulting to the given tensor's storagetype and space. The structure may be specified either as a single HomSpace argument or as codomain and domain.

By default, this will result in TensorMap{T}(undef, V) when custom objects do not specialize this method.

source

Specific constructors

Additionally, the following methods can be used to construct specific TensorMap instances.

TensorKit.idFunction
id([T::Type=Float64,] V::TensorSpace) -> TensorMap

Construct the identity endomorphism on space V, i.e. return a t::TensorMap with domain(t) == codomain(t) == V, where either scalartype(t) = T if T is a Number type or storagetype(t) = T if T is a DenseVector type.

source
TensorKit.isomorphismFunction
isomorphism([T::Type=Float64,] codomain::TensorSpace, domain::TensorSpace) -> TensorMap
 isomorphism([T::Type=Float64,] codomain ← domain) -> TensorMap
-isomorphism([T::Type=Float64,] domain → codomain) -> TensorMap

Construct a specific isomorphism between the codomain and the domain, i.e. return a t::TensorMap where either scalartype(t) = T if T is a Number type or storagetype(t) = T if T is a DenseVector type. If the spaces are not isomorphic, an error will be thrown.

Note

There is no canonical choice for a specific isomorphism, but the current choice is such that isomorphism(cod, dom) == inv(isomorphism(dom, cod)).

See also unitary when InnerProductStyle(cod) === EuclideanInnerProduct().

source
TensorKit.unitaryFunction
unitary([T::Type=Float64,] codomain::TensorSpace, domain::TensorSpace) -> TensorMap
+isomorphism([T::Type=Float64,] domain → codomain) -> TensorMap

Construct a specific isomorphism between the codomain and the domain, i.e. return a t::TensorMap where either scalartype(t) = T if T is a Number type or storagetype(t) = T if T is a DenseVector type. If the spaces are not isomorphic, an error will be thrown.

Note

There is no canonical choice for a specific isomorphism, but the current choice is such that isomorphism(cod, dom) == inv(isomorphism(dom, cod)).

See also unitary when InnerProductStyle(cod) === EuclideanInnerProduct().

source
TensorKit.unitaryFunction
unitary([T::Type=Float64,] codomain::TensorSpace, domain::TensorSpace) -> TensorMap
 unitary([T::Type=Float64,] codomain ← domain) -> TensorMap
-unitary([T::Type=Float64,] domain → codomain) -> TensorMap

Construct a specific unitary morphism between the codomain and the domain, i.e. return a t::TensorMap where either scalartype(t) = T if T is a Number type or storagetype(t) = T if T is a DenseVector type. If the spaces are not isomorphic, or the spacetype does not have a Euclidean inner product, an error will be thrown.

Note

There is no canonical choice for a specific unitary, but the current choice is such that unitary(cod, dom) == inv(unitary(dom, cod)) = adjoint(unitary(dom, cod)).

See also isomorphism and isometry.

source
TensorKit.isometryFunction
isometry([T::Type=Float64,] codomain::TensorSpace, domain::TensorSpace) -> TensorMap
+unitary([T::Type=Float64,] domain → codomain) -> TensorMap

Construct a specific unitary morphism between the codomain and the domain, i.e. return a t::TensorMap where either scalartype(t) = T if T is a Number type or storagetype(t) = T if T is a DenseVector type. If the spaces are not isomorphic, or the spacetype does not have a Euclidean inner product, an error will be thrown.

Note

There is no canonical choice for a specific unitary, but the current choice is such that unitary(cod, dom) == inv(unitary(dom, cod)) = adjoint(unitary(dom, cod)).

See also isomorphism and isometry.

source
TensorKit.isometryFunction
isometry([T::Type=Float64,] codomain::TensorSpace, domain::TensorSpace) -> TensorMap
 isometry([T::Type=Float64,] codomain ← domain) -> TensorMap
-isometry([T::Type=Float64,] domain → codomain) -> TensorMap

Construct a specific isometry between the codomain and the domain, i.e. return a t::TensorMap where either scalartype(t) = T if T is a Number type or storagetype(t) = T if T is a DenseVector type. The isometry t then satisfies t' * t = id(domain) and (t * t')^2 = t * t'. If the spaces do not allow for such an isometric inclusion, an error will be thrown.

See also isomorphism and unitary.

source

AbstractTensorMap properties and data access

The following methods exist to obtain type information:

Base.eltypeMethod
eltype(::AbstractTensorMap) -> Type{T}
-eltype(::Type{<:AbstractTensorMap}) -> Type{T}

Return the scalar or element type T of a tensor.

source
TensorKit.spacetypeMethod
spacetype(::AbstractTensorMap) -> Type{S<:IndexSpace}
-spacetype(::Type{<:AbstractTensorMap}) -> Type{S<:IndexSpace}

Return the type of the elementary space S of a tensor.

source
TensorKit.sectortypeMethod
sectortype(::AbstractTensorMap) -> Type{I<:Sector}
-sectortype(::Type{<:AbstractTensorMap}) -> Type{I<:Sector}

Return the type of sector I of a tensor.

source
TensorKit.fieldMethod
field(::AbstractTensorMap) -> Type{𝔽<:Field}
-field(::Type{<:AbstractTensorMap}) -> Type{𝔽<:Field}

Return the type of field 𝔽 of a tensor.

source
TensorKit.storagetypeFunction
storagetype(t::AbstractTensorMap) -> Type{A<:AbstractVector}
-storagetype(T::Type{<:AbstractTensorMap}) -> Type{A<:AbstractVector}

Return the type of vector that stores the data of a tensor.

source

To obtain information about the indices, you can use:

TensorKit.spaceMethod
space(t::AbstractTensorMap{T,S,N₁,N₂}) -> HomSpace{S,N₁,N₂}
-space(t::AbstractTensorMap{T,S,N₁,N₂}, i::Int) -> S

The index information of a tensor, i.e. the HomSpace of its domain and codomain. If i is specified, return the i-th index space.

source
TensorKit.domainFunction
domain(t::AbstractTensorMap{T,S,N₁,N₂}) -> ProductSpace{S,N₂}
-domain(t::AbstractTensorMap{T,S,N₁,N₂}, i::Int) -> S

Return the domain of a tensor, i.e. the product space of the input spaces. If i is specified, return the i-th input space. Implementations should provide domain(t).

See also codomain and space.

source
TensorKit.codomainFunction
codomain(t::AbstractTensorMap{T,S,N₁,N₂}) -> ProductSpace{S,N₁}
-codomain(t::AbstractTensorMap{T,S,N₁,N₂}, i::Int) -> S

Return the codomain of a tensor, i.e. the product space of the output spaces. If i is specified, return the i-th output space. Implementations should provide codomain(t).

See also domain and space.

source
TensorKit.numinFunction
numin(::Union{TT,Type{TT}}) where {TT<:AbstractTensorMap} -> Int

Return the number of input spaces of a tensor. This is equivalent to the number of spaces in the domain of that tensor.

See also numout and numind.

source
TensorKit.numoutFunction
numout(::Union{TT,Type{TT}}) where {TT<:AbstractTensorMap} -> Int

Return the number of output spaces of a tensor. This is equivalent to the number of spaces in the codomain of that tensor.

See also numin and numind.

source
TensorKit.numindFunction
numind(::Union{T,Type{T}}) where {T<:AbstractTensorMap} -> Int

Return the total number of input and output spaces of a tensor. This is equivalent to the total number of spaces in the domain and codomain of that tensor.

See also numout and numin.

source
TensorKit.allindFunction
allind(::Union{TT,Type{TT}}) where {TT<:AbstractTensorMap} -> Tuple{Int}

Return all indices of a tensor, i.e. the indices of its domain and codomain.

See also codomainind and domainind.

source

In TensorMap instances, all data is gathered in a single AbstractVector, which has an internal structure into blocks associated to total coupled charge, within which live subblocks associated with the different possible fusion-splitting tree pairs.

To obtain information about the structure of the data, you can use:

TensorKit.fusionblockstructureMethod
fusionblockstructure(t::AbstractTensorMap) -> TensorStructure

Return the necessary structure information to decompose a tensor in blocks labeled by coupled sectors and in subblocks labeled by a splitting-fusion tree couple.

source
TensorKitSectors.dimMethod
dim(t::AbstractTensorMap) -> Int

The total number of free parameters of a tensor, discounting the entries that are fixed by symmetry. This is also the dimension of the HomSpace on which the TensorMap is defined.

source
TensorKit.hasblockMethod
hasblock(t::AbstractTensorMap, c::Sector) -> Bool

Verify whether a tensor has a block corresponding to a coupled sector c.

source
TensorKit.fusiontreesMethod
fusiontrees(t::AbstractTensorMap)

Return an iterator over all splitting - fusion tree pairs of a tensor.

source

Data can be accessed (and modified) in a number of ways. To access the full matrix block associated with the coupled charges, you can use:

To access the data associated with a specific fusion tree pair, you can use:

Base.getindexMethod
Base.getindex(t::TensorMap{T,S,N₁,N₂,I},
+isometry([T::Type=Float64,] domain → codomain) -> TensorMap

Construct a specific isometry between the codomain and the domain, i.e. return a t::TensorMap where either scalartype(t) = T if T is a Number type or storagetype(t) = T if T is a DenseVector type. The isometry t then satisfies t' * t = id(domain) and (t * t')^2 = t * t'. If the spaces do not allow for such an isometric inclusion, an error will be thrown.

See also isomorphism and unitary.

source

AbstractTensorMap properties and data access

The following methods exist to obtain type information:

Base.eltypeMethod
eltype(::AbstractTensorMap) -> Type{T}
+eltype(::Type{<:AbstractTensorMap}) -> Type{T}

Return the scalar or element type T of a tensor.

source
TensorKit.spacetypeMethod
spacetype(::AbstractTensorMap) -> Type{S<:IndexSpace}
+spacetype(::Type{<:AbstractTensorMap}) -> Type{S<:IndexSpace}

Return the type of the elementary space S of a tensor.

source
TensorKit.sectortypeMethod
sectortype(::AbstractTensorMap) -> Type{I<:Sector}
+sectortype(::Type{<:AbstractTensorMap}) -> Type{I<:Sector}

Return the type of sector I of a tensor.

source
TensorKit.fieldMethod
field(::AbstractTensorMap) -> Type{𝔽<:Field}
+field(::Type{<:AbstractTensorMap}) -> Type{𝔽<:Field}

Return the type of field 𝔽 of a tensor.

source
TensorKit.storagetypeFunction
storagetype(t::AbstractTensorMap) -> Type{A<:AbstractVector}
+storagetype(T::Type{<:AbstractTensorMap}) -> Type{A<:AbstractVector}

Return the type of vector that stores the data of a tensor.

source

To obtain information about the indices, you can use:

TensorKit.spaceMethod
space(t::AbstractTensorMap{T,S,N₁,N₂}) -> HomSpace{S,N₁,N₂}
+space(t::AbstractTensorMap{T,S,N₁,N₂}, i::Int) -> S

The index information of a tensor, i.e. the HomSpace of its domain and codomain. If i is specified, return the i-th index space.

source
TensorKit.domainFunction
domain(t::AbstractTensorMap{T,S,N₁,N₂}) -> ProductSpace{S,N₂}
+domain(t::AbstractTensorMap{T,S,N₁,N₂}, i::Int) -> S

Return the domain of a tensor, i.e. the product space of the input spaces. If i is specified, return the i-th input space. Implementations should provide domain(t).

See also codomain and space.

source
TensorKit.codomainFunction
codomain(t::AbstractTensorMap{T,S,N₁,N₂}) -> ProductSpace{S,N₁}
+codomain(t::AbstractTensorMap{T,S,N₁,N₂}, i::Int) -> S

Return the codomain of a tensor, i.e. the product space of the output spaces. If i is specified, return the i-th output space. Implementations should provide codomain(t).

See also domain and space.

source
TensorKit.numinFunction
numin(::Union{TT,Type{TT}}) where {TT<:AbstractTensorMap} -> Int

Return the number of input spaces of a tensor. This is equivalent to the number of spaces in the domain of that tensor.

See also numout and numind.

source
TensorKit.numoutFunction
numout(::Union{TT,Type{TT}}) where {TT<:AbstractTensorMap} -> Int

Return the number of output spaces of a tensor. This is equivalent to the number of spaces in the codomain of that tensor.

See also numin and numind.

source
TensorKit.numindFunction
numind(::Union{T,Type{T}}) where {T<:AbstractTensorMap} -> Int

Return the total number of input and output spaces of a tensor. This is equivalent to the total number of spaces in the domain and codomain of that tensor.

See also numout and numin.

source
TensorKit.allindFunction
allind(::Union{TT,Type{TT}}) where {TT<:AbstractTensorMap} -> Tuple{Int}

Return all indices of a tensor, i.e. the indices of its domain and codomain.

See also codomainind and domainind.

source

In TensorMap instances, all data is gathered in a single AbstractVector, which has an internal structure into blocks associated to total coupled charge, within which live subblocks associated with the different possible fusion-splitting tree pairs.

To obtain information about the structure of the data, you can use:

TensorKit.fusionblockstructureMethod
fusionblockstructure(t::AbstractTensorMap) -> TensorStructure

Return the necessary structure information to decompose a tensor in blocks labeled by coupled sectors and in subblocks labeled by a splitting-fusion tree couple.

source
TensorKitSectors.dimMethod
dim(t::AbstractTensorMap) -> Int

The total number of free parameters of a tensor, discounting the entries that are fixed by symmetry. This is also the dimension of the HomSpace on which the TensorMap is defined.

source
TensorKit.hasblockMethod
hasblock(t::AbstractTensorMap, c::Sector) -> Bool

Verify whether a tensor has a block corresponding to a coupled sector c.

source
TensorKit.fusiontreesMethod
fusiontrees(t::AbstractTensorMap)

Return an iterator over all splitting - fusion tree pairs of a tensor.

source

Data can be accessed (and modified) in a number of ways. To access the full matrix block associated with the coupled charges, you can use:

To access the data associated with a specific fusion tree pair, you can use:

Base.getindexMethod
Base.getindex(t::TensorMap{T,S,N₁,N₂,I},
               f₁::FusionTree{I,N₁},
               f₂::FusionTree{I,N₂}) where {T,SN₁,N₂,I<:Sector}
     -> StridedViews.StridedView
-t[f₁, f₂]

Return a view into the data slice of t corresponding to the splitting - fusion tree pair (f₁, f₂). In particular, if f₁.coupled == f₂.coupled == c, then a StridedViews.StridedView of size (dims(codomain(t), f₁.uncoupled)..., dims(domain(t), f₂.uncoupled)) is returned which represents the slice of block(t, c) whose row indices correspond to f₁.uncoupled and column indices correspond to f₂.uncoupled.

source
Base.setindex!Method
Base.setindex!(t::TensorMap{T,S,N₁,N₂,I},
+t[f₁, f₂]

Return a view into the data slice of t corresponding to the splitting - fusion tree pair (f₁, f₂). In particular, if f₁.coupled == f₂.coupled == c, then a StridedViews.StridedView of size (dims(codomain(t), f₁.uncoupled)..., dims(domain(t), f₂.uncoupled)) is returned which represents the slice of block(t, c) whose row indices correspond to f₁.uncoupled and column indices correspond to f₂.uncoupled.

source
Base.setindex!Method
Base.setindex!(t::TensorMap{T,S,N₁,N₂,I},
                v,
                f₁::FusionTree{I,N₁},
                f₂::FusionTree{I,N₂}) where {T,S,N₁,N₂,I<:Sector}
-t[f₁, f₂] = v

Copies v into the data slice of t corresponding to the splitting - fusion tree pair (f₁, f₂). Here, v can be any object that can be copied into a StridedViews.StridedView of size (dims(codomain(t), f₁.uncoupled)..., dims(domain(t), f₂.uncoupled)) using Base.copy!.

See also Base.getindex(::TensorMap{T,S,N₁,N₂,I<:Sector}, ::FusionTree{I<:Sector,N₁}, ::FusionTree{I<:Sector,N₂})

source

For a tensor t with FusionType(sectortype(t)) isa UniqeFuison, fusion trees are completely determined by the outcoming sectors, and the data can be accessed in a more straightforward way:

For a tensor t with FusionType(sectortype(t)) isa UniqeFuison, fusion trees are completely determined by the outcoming sectors, and the data can be accessed in a more straightforward way:

Base.getindexMethod
Base.getindex(t::TensorMap
               sectors::NTuple{N₁+N₂,I}) where {N₁,N₂,I<:Sector} 
     -> StridedViews.StridedView
-t[sectors]

Return a view into the data slice of t corresponding to the splitting - fusion tree pair with combined uncoupled charges sectors. In particular, if sectors == (s₁..., s₂...) where s₁ and s₂ correspond to the coupled charges in the codomain and domain respectively, then a StridedViews.StridedView of size (dims(codomain(t), s₁)..., dims(domain(t), s₂)) is returned.

This method is only available for the case where FusionStyle(I) isa UniqueFusion, since it assumes a uniquely defined coupled charge.

source

For tensor t with sectortype(t) == Trivial, the data can be accessed and manipulated directly as multidimensional arrays:

Base.getindexMethod
Base.getindex(t::AbstractTensorMap)
-t[]

Return a view into the data of t as a StridedViews.StridedView of size (dims(codomain(t))..., dims(domain(t))...).

source
Base.getindexMethod
Base.getindex(t::AbstractTensorMap, indices::Vararg{Int})
-t[indices]

Return a view into the data slice of t corresponding to indices, by slicing the StridedViews.StridedView into the full data array.

source
Base.setindex!Method
Base.setindex!(t::AbstractTensorMap, v, indices::Vararg{Int})
-t[indices] = v

Assigns v to the data slice of t corresponding to indices.

source

AbstractTensorMap operations

The operations that can be performed on an AbstractTensorMap can be organized into the following categories:

  • vector operations: these do not change the space or index strucure of a tensor and can be straightforwardly implemented on on the full data. All the methods described in VectorInterface.jl are supported. For compatibility reasons, we also provide implementations for equivalent methods from LinearAlgebra.jl, such as axpy!, axpby!.

  • index manipulations: these change (permute) the index structure of a tensor, which affects the data in a way that is fully determined by the categorical data of the sectortype of the tensor.

  • (planar) contractions and (planar) traces (i.e., contractions with identity tensors). Tensor contractions correspond to a combination of some index manipulations followed by a composition or multiplication of the tensors in their role as linear maps. Tensor contractions are however of such important and frequency that they require a dedicated implementation.

  • tensor factorisations, which relies on their identification of tensors with linear maps between tensor spaces. The factorisations are applied as ordinary matrix factorisations to the matrix blocks associated with the coupled charges.

Index manipulations

A general index manipulation of a TensorMap object can be built up by considering some transformation of the fusion trees, along with a permutation of the stored data. They come in three flavours, which are either of the type transform(!) which are exported, or of the type add_transform!, for additional expert-mode options that allows for addition and scaling, as well as the selection of a custom backend.

TensorKit.permuteMethod
permute(tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple;
+t[sectors]

Return a view into the data slice of t corresponding to the splitting - fusion tree pair with combined uncoupled charges sectors. In particular, if sectors == (s₁..., s₂...) where s₁ and s₂ correspond to the coupled charges in the codomain and domain respectively, then a StridedViews.StridedView of size (dims(codomain(t), s₁)..., dims(domain(t), s₂)) is returned.

This method is only available for the case where FusionStyle(I) isa UniqueFusion, since it assumes a uniquely defined coupled charge.

source

For tensor t with sectortype(t) == Trivial, the data can be accessed and manipulated directly as multidimensional arrays:

Base.getindexMethod
Base.getindex(t::AbstractTensorMap)
+t[]

Return a view into the data of t as a StridedViews.StridedView of size (dims(codomain(t))..., dims(domain(t))...).

source
Base.getindexMethod
Base.getindex(t::AbstractTensorMap, indices::Vararg{Int})
+t[indices]

Return a view into the data slice of t corresponding to indices, by slicing the StridedViews.StridedView into the full data array.

source
Base.setindex!Method
Base.setindex!(t::AbstractTensorMap, v, indices::Vararg{Int})
+t[indices] = v

Assigns v to the data slice of t corresponding to indices.

source

AbstractTensorMap operations

The operations that can be performed on an AbstractTensorMap can be organized into the following categories:

  • vector operations: these do not change the space or index strucure of a tensor and can be straightforwardly implemented on on the full data. All the methods described in VectorInterface.jl are supported. For compatibility reasons, we also provide implementations for equivalent methods from LinearAlgebra.jl, such as axpy!, axpby!.

  • index manipulations: these change (permute) the index structure of a tensor, which affects the data in a way that is fully determined by the categorical data of the sectortype of the tensor.

  • (planar) contractions and (planar) traces (i.e., contractions with identity tensors). Tensor contractions correspond to a combination of some index manipulations followed by a composition or multiplication of the tensors in their role as linear maps. Tensor contractions are however of such important and frequency that they require a dedicated implementation.

  • tensor factorisations, which relies on their identification of tensors with linear maps between tensor spaces. The factorisations are applied as ordinary matrix factorisations to the matrix blocks associated with the coupled charges.

Index manipulations

A general index manipulation of a TensorMap object can be built up by considering some transformation of the fusion trees, along with a permutation of the stored data. They come in three flavours, which are either of the type transform(!) which are exported, or of the type add_transform!, for additional expert-mode options that allows for addition and scaling, as well as the selection of a custom backend.

TensorKit.permuteMethod
permute(tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple;
         copy::Bool=false)
-    -> tdst::TensorMap

Return tensor tdst obtained by permuting the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively.

If copy=false, tdst might share data with tsrc whenever possible. Otherwise, a copy is always made.

To permute into an existing destination, see permute! and add_permute!

source
TensorKit.braidMethod
braid(tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple, levels::IndexTuple;
+    -> tdst::TensorMap

Return tensor tdst obtained by permuting the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively.

If copy=false, tdst might share data with tsrc whenever possible. Otherwise, a copy is always made.

To permute into an existing destination, see permute! and add_permute!

source
TensorKit.braidMethod
braid(tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple, levels::IndexTuple;
       copy::Bool = false)
-    -> tdst::TensorMap

Return tensor tdst obtained by braiding the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively. Here, levels is a tuple of length numind(tsrc) that assigns a level or height to the indices of tsrc, which determines whether they will braid over or under any other index with which they have to change places.

If copy=false, tdst might share data with tsrc whenever possible. Otherwise, a copy is always made.

To braid into an existing destination, see braid! and add_braid!

source
Base.transposeMethod
transpose(tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple;
+    -> tdst::TensorMap

Return tensor tdst obtained by braiding the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively. Here, levels is a tuple of length numind(tsrc) that assigns a level or height to the indices of tsrc, which determines whether they will braid over or under any other index with which they have to change places.

If copy=false, tdst might share data with tsrc whenever possible. Otherwise, a copy is always made.

To braid into an existing destination, see braid! and add_braid!

source
Base.transposeMethod
transpose(tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple;
           copy::Bool=false)
-    -> tdst::TensorMap

Return tensor tdst obtained by transposing the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively. The new index positions should be attainable without any indices crossing each other, i.e., the permutation (p₁..., reverse(p₂)...) should constitute a cyclic permutation of (codomainind(tsrc)..., reverse(domainind(tsrc))...).

If copy=false, tdst might share data with tsrc whenever possible. Otherwise, a copy is always made.

To permute into an existing destination, see permute! and add_permute!

source
TensorKit.repartitionMethod
repartition(tsrc::AbstractTensorMap{S}, N₁::Int, N₂::Int; copy::Bool=false) where {S}
-    -> tdst::AbstractTensorMap{S,N₁,N₂}

Return tensor tdst obtained by repartitioning the indices of t. The codomain and domain of tdst correspond to the first N₁ and last N₂ spaces of t, respectively.

If copy=false, tdst might share data with tsrc whenever possible. Otherwise, a copy is always made.

To repartition into an existing destination, see repartition!.

source
TensorKitSectors.twistMethod
twist(tsrc::AbstractTensorMap, i::Int; inv::Bool=false) -> tdst
-twist(tsrc::AbstractTensorMap, is; inv::Bool=false) -> tdst

Apply a twist to the ith index of tsrc and return the result as a new tensor. If inv=true, use the inverse twist.

See twist! for storing the result in place.

source
Base.permute!Method
permute!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple)
-    -> tdst

Write into tdst the result of permuting the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively.

See permute for creating a new tensor and add_permute! for a more general version.

source
TensorKit.braid!Function
braid!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap,
+    -> tdst::TensorMap

Return tensor tdst obtained by transposing the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively. The new index positions should be attainable without any indices crossing each other, i.e., the permutation (p₁..., reverse(p₂)...) should constitute a cyclic permutation of (codomainind(tsrc)..., reverse(domainind(tsrc))...).

If copy=false, tdst might share data with tsrc whenever possible. Otherwise, a copy is always made.

To permute into an existing destination, see permute! and add_permute!

source
TensorKit.repartitionMethod
repartition(tsrc::AbstractTensorMap{S}, N₁::Int, N₂::Int; copy::Bool=false) where {S}
+    -> tdst::AbstractTensorMap{S,N₁,N₂}

Return tensor tdst obtained by repartitioning the indices of t. The codomain and domain of tdst correspond to the first N₁ and last N₂ spaces of t, respectively.

If copy=false, tdst might share data with tsrc whenever possible. Otherwise, a copy is always made.

To repartition into an existing destination, see repartition!.

source
TensorKitSectors.twistMethod
twist(tsrc::AbstractTensorMap, i::Int; inv::Bool=false) -> tdst
+twist(tsrc::AbstractTensorMap, is; inv::Bool=false) -> tdst

Apply a twist to the ith index of tsrc and return the result as a new tensor. If inv=true, use the inverse twist.

See twist! for storing the result in place.

source
Base.permute!Method
permute!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple)
+    -> tdst

Write into tdst the result of permuting the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively.

See permute for creating a new tensor and add_permute! for a more general version.

source
TensorKit.braid!Function
braid!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap,
        (p₁, p₂)::Index2Tuple, levels::Tuple)
-    -> tdst

Write into tdst the result of braiding the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively. Here, levels is a tuple of length numind(tsrc) that assigns a level or height to the indices of tsrc, which determines whether they will braid over or under any other index with which they have to change places.

See braid for creating a new tensor and add_braid! for a more general version.

source
LinearAlgebra.transpose!Function
transpose!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap,
+    -> tdst

Write into tdst the result of braiding the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively. Here, levels is a tuple of length numind(tsrc) that assigns a level or height to the indices of tsrc, which determines whether they will braid over or under any other index with which they have to change places.

See braid for creating a new tensor and add_braid! for a more general version.

source
LinearAlgebra.transpose!Function
transpose!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap,
            (p₁, p₂)::Index2Tuple)
-    -> tdst

Write into tdst the result of transposing the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively. The new index positions should be attainable without any indices crossing each other, i.e., the permutation (p₁..., reverse(p₂)...) should constitute a cyclic permutation of (codomainind(tsrc)..., reverse(domainind(tsrc))...).

See transpose for creating a new tensor and add_transpose! for a more general version.

source
TensorKit.repartition!Function
repartition!(tdst::AbstractTensorMap{S}, tsrc::AbstractTensorMap{S}) where {S} -> tdst

Write into tdst the result of repartitioning the indices of tsrc. This is just a special case of a transposition that only changes the number of in- and outgoing indices.

See repartition for creating a new tensor.

source
TensorKit.twist!Function
twist!(t::AbstractTensorMap, i::Int; inv::Bool=false) -> t
-twist!(t::AbstractTensorMap, is; inv::Bool=false) -> t

Apply a twist to the ith index of t, or all indices in is, storing the result in t. If inv=true, use the inverse twist.

See twist for creating a new tensor.

source
TensorKit.add_permute!Function
add_permute!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple,
-             α::Number, β::Number, backend::AbstractBackend...)

Return the updated tdst, which is the result of adding α * tsrc to tdst after permuting the indices of tsrc according to (p₁, p₂).

See also permute, permute!, add_braid!, add_transpose!.

source
TensorKit.add_braid!Function
add_braid!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple,
-           levels::IndexTuple, α::Number, β::Number, backend::AbstractBackend...)

Return the updated tdst, which is the result of adding α * tsrc to tdst after braiding the indices of tsrc according to (p₁, p₂) and levels.

See also braid, braid!, add_permute!, add_transpose!.

source
TensorKit.add_transpose!Function
add_transpose!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple,
-               α::Number, β::Number, backend::AbstractBackend...)

Return the updated tdst, which is the result of adding α * tsrc to tdst after transposing the indices of tsrc according to (p₁, p₂).

See also transpose, transpose!, add_permute!, add_braid!.

source

Tensor map composition, traces, contractions and tensor products

TensorKit.composeMethod
compose(t1::AbstractTensorMap, t2::AbstractTensorMap) -> AbstractTensorMap

Return the AbstractTensorMap that implements the composition of the two tensor maps t1 and t2.

source
TensorKit.trace_permute!Function
trace_permute!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap,
+    -> tdst

Write into tdst the result of transposing the indices of tsrc. The codomain and domain of tdst correspond to the indices in p₁ and p₂ of tsrc respectively. The new index positions should be attainable without any indices crossing each other, i.e., the permutation (p₁..., reverse(p₂)...) should constitute a cyclic permutation of (codomainind(tsrc)..., reverse(domainind(tsrc))...).

See transpose for creating a new tensor and add_transpose! for a more general version.

source
TensorKit.repartition!Function
repartition!(tdst::AbstractTensorMap{S}, tsrc::AbstractTensorMap{S}) where {S} -> tdst

Write into tdst the result of repartitioning the indices of tsrc. This is just a special case of a transposition that only changes the number of in- and outgoing indices.

See repartition for creating a new tensor.

source
TensorKit.twist!Function
twist!(t::AbstractTensorMap, i::Int; inv::Bool=false) -> t
+twist!(t::AbstractTensorMap, is; inv::Bool=false) -> t

Apply a twist to the ith index of t, or all indices in is, storing the result in t. If inv=true, use the inverse twist.

See twist for creating a new tensor.

source
TensorKit.add_permute!Function
add_permute!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple,
+             α::Number, β::Number, backend::AbstractBackend...)

Return the updated tdst, which is the result of adding α * tsrc to tdst after permuting the indices of tsrc according to (p₁, p₂).

See also permute, permute!, add_braid!, add_transpose!.

source
TensorKit.add_braid!Function
add_braid!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple,
+           levels::IndexTuple, α::Number, β::Number, backend::AbstractBackend...)

Return the updated tdst, which is the result of adding α * tsrc to tdst after braiding the indices of tsrc according to (p₁, p₂) and levels.

See also braid, braid!, add_permute!, add_transpose!.

source
TensorKit.add_transpose!Function
add_transpose!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap, (p₁, p₂)::Index2Tuple,
+               α::Number, β::Number, backend::AbstractBackend...)

Return the updated tdst, which is the result of adding α * tsrc to tdst after transposing the indices of tsrc according to (p₁, p₂).

See also transpose, transpose!, add_permute!, add_braid!.

source

Tensor map composition, traces, contractions and tensor products

TensorKit.composeMethod
compose(t1::AbstractTensorMap, t2::AbstractTensorMap) -> AbstractTensorMap

Return the AbstractTensorMap that implements the composition of the two tensor maps t1 and t2.

source
TensorKit.trace_permute!Function
trace_permute!(tdst::AbstractTensorMap, tsrc::AbstractTensorMap,
                (p₁, p₂)::Index2Tuple, (q₁, q₂)::Index2Tuple,
-               α::Number, β::Number, backend=TO.DefaultBackend())

Return the updated tdst, which is the result of adding α * tsrc to tdst after permuting the indices of tsrc according to (p₁, p₂) and furthermore tracing the indices in q₁ and q₂.

source
TensorKit.contract!Function
contract!(C::AbstractTensorMap,
+               α::Number, β::Number, backend=TO.DefaultBackend())

Return the updated tdst, which is the result of adding α * tsrc to tdst after permuting the indices of tsrc according to (p₁, p₂) and furthermore tracing the indices in q₁ and q₂.

source
TensorKit.contract!Function
contract!(C::AbstractTensorMap,
           A::AbstractTensorMap, (oindA, cindA)::Index2Tuple,
           B::AbstractTensorMap, (cindB, oindB)::Index2Tuple,
           (p₁, p₂)::Index2Tuple,
           α::Number, β::Number,
-          backend, allocator)

Return the updated C, which is the result of adding α * A * B to C after permuting the indices of A and B according to (oindA, cindA) and (cindB, oindB) respectively.

source
TensorKitSectors.:⊗Method
⊗(t1::AbstractTensorMap, t2::AbstractTensorMap, ...) -> TensorMap
-otimes(t1::AbstractTensorMap, t2::AbstractTensorMap, ...) -> TensorMap

Compute the tensor product between two AbstractTensorMap instances, which results in a new TensorMap instance whose codomain is codomain(t1) ⊗ codomain(t2) and whose domain is domain(t1) ⊗ domain(t2).

source

TensorMap factorizations

The factorisation methods come in two flavors, namely a non-destructive version where you can specify an additional permutation of the domain and codomain indices before the factorisation is performed (provided that sectorstyle(t) has a symmetric braiding) as well as a destructive version The non-destructive methods are given first:

TensorKit.leftorthFunction
leftorth(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple;
-            alg::OrthogonalFactorizationAlgorithm = QRpos()) -> Q, R

Create orthonormal basis Q for indices in leftind, and remainder R such that permute(t, (leftind, rightind)) = Q*R.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using leftorth!(t, alg = QRpos()).

Different algorithms are available, namely QR(), QRpos(), SVD() and Polar(). QR() and QRpos() use a standard QR decomposition, producing an upper triangular matrix R. Polar() produces a Hermitian and positive semidefinite R. QRpos() corrects the standard QR decomposition such that the diagonal elements of R are positive. Only QRpos() and Polar() are unique (no residual freedom) so that they always return the same result for the same input tensor t.

Orthogonality requires InnerProductStyle(t) <: HasInnerProduct, and leftorth(!) is currently only implemented for InnerProductStyle(t) === EuclideanInnerProduct().

source
TensorKit.rightorthFunction
rightorth(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple;
-            alg::OrthogonalFactorizationAlgorithm = LQpos()) -> L, Q

Create orthonormal basis Q for indices in rightind, and remainder L such that permute(t, (leftind, rightind)) = L*Q.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using rightorth!(t, alg = LQpos()).

Different algorithms are available, namely LQ(), LQpos(), RQ(), RQpos(), SVD() and Polar(). LQ() and LQpos() produce a lower triangular matrix L and are computed using a QR decomposition of the transpose. RQ() and RQpos() produce an upper triangular remainder L and only works if the total left dimension is smaller than or equal to the total right dimension. LQpos() and RQpos() add an additional correction such that the diagonal elements of L are positive. Polar() produces a Hermitian and positive semidefinite L. Only LQpos(), RQpos() and Polar() are unique (no residual freedom) so that they always return the same result for the same input tensor t.

Orthogonality requires InnerProductStyle(t) <: HasInnerProduct, and rightorth(!) is currently only implemented for InnerProductStyle(t) === EuclideanInnerProduct().

source
TensorKit.leftnullFunction
leftnull(t::AbstractTensor, (leftind, rightind)::Index2Tuple;
-            alg::OrthogonalFactorizationAlgorithm = QRpos()) -> N

Create orthonormal basis for the orthogonal complement of the support of the indices in leftind, such that N' * permute(t, (leftind, rightind)) = 0.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using leftnull!(t, alg = QRpos()).

Different algorithms are available, namely QR() (or equivalently, QRpos()), SVD() and SDD(). The first assumes that the matrix is full rank and requires iszero(atol) and iszero(rtol). With SVD() and SDD(), rightnull will use the corresponding singular value decomposition, and one can specify an absolute or relative tolerance for which singular values are to be considered zero, where max(atol, norm(t)*rtol) is used as upper bound.

Orthogonality requires InnerProductStyle(t) <: HasInnerProduct, and leftnull(!) is currently only implemented for InnerProductStyle(t) === EuclideanInnerProduct().

source
TensorKit.rightnullFunction
rightnull(t::AbstractTensor, (leftind, rightind)::Index2Tuple;
+          backend, allocator)

Return the updated C, which is the result of adding α * A * B to C after permuting the indices of A and B according to (oindA, cindA) and (cindB, oindB) respectively.

source
TensorKitSectors.:⊗Method
⊗(t1::AbstractTensorMap, t2::AbstractTensorMap, ...) -> TensorMap
+otimes(t1::AbstractTensorMap, t2::AbstractTensorMap, ...) -> TensorMap

Compute the tensor product between two AbstractTensorMap instances, which results in a new TensorMap instance whose codomain is codomain(t1) ⊗ codomain(t2) and whose domain is domain(t1) ⊗ domain(t2).

source

TensorMap factorizations

The factorisation methods come in two flavors, namely a non-destructive version where you can specify an additional permutation of the domain and codomain indices before the factorisation is performed (provided that sectorstyle(t) has a symmetric braiding) as well as a destructive version The non-destructive methods are given first:

TensorKit.leftorthFunction
leftorth(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple;
+            alg::OrthogonalFactorizationAlgorithm = QRpos()) -> Q, R

Create orthonormal basis Q for indices in leftind, and remainder R such that permute(t, (leftind, rightind)) = Q*R.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using leftorth!(t, alg = QRpos()).

Different algorithms are available, namely QR(), QRpos(), SVD() and Polar(). QR() and QRpos() use a standard QR decomposition, producing an upper triangular matrix R. Polar() produces a Hermitian and positive semidefinite R. QRpos() corrects the standard QR decomposition such that the diagonal elements of R are positive. Only QRpos() and Polar() are unique (no residual freedom) so that they always return the same result for the same input tensor t.

Orthogonality requires InnerProductStyle(t) <: HasInnerProduct, and leftorth(!) is currently only implemented for InnerProductStyle(t) === EuclideanInnerProduct().

source
TensorKit.rightorthFunction
rightorth(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple;
+            alg::OrthogonalFactorizationAlgorithm = LQpos()) -> L, Q

Create orthonormal basis Q for indices in rightind, and remainder L such that permute(t, (leftind, rightind)) = L*Q.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using rightorth!(t, alg = LQpos()).

Different algorithms are available, namely LQ(), LQpos(), RQ(), RQpos(), SVD() and Polar(). LQ() and LQpos() produce a lower triangular matrix L and are computed using a QR decomposition of the transpose. RQ() and RQpos() produce an upper triangular remainder L and only works if the total left dimension is smaller than or equal to the total right dimension. LQpos() and RQpos() add an additional correction such that the diagonal elements of L are positive. Polar() produces a Hermitian and positive semidefinite L. Only LQpos(), RQpos() and Polar() are unique (no residual freedom) so that they always return the same result for the same input tensor t.

Orthogonality requires InnerProductStyle(t) <: HasInnerProduct, and rightorth(!) is currently only implemented for InnerProductStyle(t) === EuclideanInnerProduct().

source
TensorKit.leftnullFunction
leftnull(t::AbstractTensor, (leftind, rightind)::Index2Tuple;
+            alg::OrthogonalFactorizationAlgorithm = QRpos()) -> N

Create orthonormal basis for the orthogonal complement of the support of the indices in leftind, such that N' * permute(t, (leftind, rightind)) = 0.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using leftnull!(t, alg = QRpos()).

Different algorithms are available, namely QR() (or equivalently, QRpos()), SVD() and SDD(). The first assumes that the matrix is full rank and requires iszero(atol) and iszero(rtol). With SVD() and SDD(), rightnull will use the corresponding singular value decomposition, and one can specify an absolute or relative tolerance for which singular values are to be considered zero, where max(atol, norm(t)*rtol) is used as upper bound.

Orthogonality requires InnerProductStyle(t) <: HasInnerProduct, and leftnull(!) is currently only implemented for InnerProductStyle(t) === EuclideanInnerProduct().

source
TensorKit.rightnullFunction
rightnull(t::AbstractTensor, (leftind, rightind)::Index2Tuple;
             alg::OrthogonalFactorizationAlgorithm = LQ(),
             atol::Real = 0.0,
-            rtol::Real = eps(real(float(one(scalartype(t)))))*iszero(atol)) -> N

Create orthonormal basis for the orthogonal complement of the support of the indices in rightind, such that permute(t, (leftind, rightind))*N' = 0.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using rightnull!(t, alg = LQpos()).

Different algorithms are available, namely LQ() (or equivalently, LQpos), SVD() and SDD(). The first assumes that the matrix is full rank and requires iszero(atol) and iszero(rtol). With SVD() and SDD(), rightnull will use the corresponding singular value decomposition, and one can specify an absolute or relative tolerance for which singular values are to be considered zero, where max(atol, norm(t)*rtol) is used as upper bound.

Orthogonality requires InnerProductStyle(t) <: HasInnerProduct, and rightnull(!) is currently only implemented for InnerProductStyle(t) === EuclideanInnerProduct().

source
TensorKit.tsvdFunction
tsvd(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple;
+            rtol::Real = eps(real(float(one(scalartype(t)))))*iszero(atol)) -> N

Create orthonormal basis for the orthogonal complement of the support of the indices in rightind, such that permute(t, (leftind, rightind))*N' = 0.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using rightnull!(t, alg = LQpos()).

Different algorithms are available, namely LQ() (or equivalently, LQpos), SVD() and SDD(). The first assumes that the matrix is full rank and requires iszero(atol) and iszero(rtol). With SVD() and SDD(), rightnull will use the corresponding singular value decomposition, and one can specify an absolute or relative tolerance for which singular values are to be considered zero, where max(atol, norm(t)*rtol) is used as upper bound.

Orthogonality requires InnerProductStyle(t) <: HasInnerProduct, and rightnull(!) is currently only implemented for InnerProductStyle(t) === EuclideanInnerProduct().

source
TensorKit.tsvdFunction
tsvd(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple;
     trunc::TruncationScheme = notrunc(), p::Real = 2, alg::Union{SVD, SDD} = SDD())
-    -> U, S, V, ϵ

Compute the (possibly truncated) singular value decomposition such that norm(permute(t, (leftind, rightind)) - U * S * V) ≈ ϵ, where ϵ thus represents the truncation error.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using tsvd!(t, trunc = notrunc(), p = 2).

A truncation parameter trunc can be specified for the new internal dimension, in which case a truncated singular value decomposition will be computed. Choices are:

  • notrunc(): no truncation (default);
  • truncerr(η::Real): truncates such that the p-norm of the truncated singular values is smaller than η;
  • truncdim(χ::Int): truncates such that the equivalent total dimension of the internal vector space is no larger than χ;
  • truncspace(V): truncates such that the dimension of the internal vector space is smaller than that of V in any sector.
  • truncbelow(η::Real): truncates such that every singular value is larger then η ;

Truncation options can also be combined using &, i.e. truncbelow(η) & truncdim(χ) will choose the truncation space such that every singular value is larger than η, and the equivalent total dimension of the internal vector space is no larger than χ.

The method tsvd also returns the truncation error ϵ, computed as the p norm of the singular values that were truncated.

THe keyword alg can be equal to SVD() or SDD(), corresponding to the underlying LAPACK algorithm that computes the decomposition (_gesvd or _gesdd).

Orthogonality requires InnerProductStyle(t) <: HasInnerProduct, and tsvd(!) is currently only implemented for InnerProductStyle(t) === EuclideanInnerProduct().

source
TensorKit.eighFunction
eigh(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple) -> D, V

Compute eigenvalue factorization of tensor t as linear map from rightind to leftind. The function eigh assumes that the linear map is hermitian and D and V tensors with the same scalartype as t. See eig and eigen for non-hermitian tensors. Hermiticity requires that the tensor acts on inner product spaces, and the current implementation requires InnerProductStyle(t) === EuclideanInnerProduct().

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using eigh!(t). Note that the permuted tensor on which eigh! is called should have equal domain and codomain, as otherwise the eigenvalue decomposition is meaningless and cannot satisfy

permute(t, (leftind, rightind)) * V = V * D

See also eigen and eig.

source
TensorKit.eigFunction
eig(t::AbstractTensor, (leftind, rightind)::Index2Tuple; kwargs...) -> D, V

Compute eigenvalue factorization of tensor t as linear map from rightind to leftind. The function eig assumes that the linear map is not hermitian and returns type stable complex valued D and V tensors for both real and complex valued t. See eigh for hermitian linear maps

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using eig!(t). Note that the permuted tensor on which eig! is called should have equal domain and codomain, as otherwise the eigenvalue decomposition is meaningless and cannot satisfy

permute(t, (leftind, rightind)) * V = V * D

Accepts the same keyword arguments scale and permute as eigen of dense matrices. See the corresponding documentation for more information.

See also eigen and eigh.

source
LinearAlgebra.eigenFunction
eigen(t::AbstractTensor, (leftind, rightind)::Index2Tuple; kwargs...) -> D, V

Compute eigenvalue factorization of tensor t as linear map from rightind to leftind.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using eigen!(t). Note that the permuted tensor on which eigen! is called should have equal domain and codomain, as otherwise the eigenvalue decomposition is meaningless and cannot satisfy

permute(t, (leftind, rightind)) * V = V * D

Accepts the same keyword arguments scale and permute as eigen of dense matrices. See the corresponding documentation for more information.

See also eig and eigh

source
LinearAlgebra.isposdefFunction
isposdef(t::AbstractTensor, (leftind, rightind)::Index2Tuple) -> ::Bool

Test whether a tensor t is positive definite as linear map from rightind to leftind.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using isposdef!(t). Note that the permuted tensor on which isposdef! is called should have equal domain and codomain, as otherwise it is meaningless.

source

The corresponding destructive methods have an exclamation mark at the end of their name, and only accept the TensorMap object as well as the method-specific algorithm and keyword arguments.

TODO: document svd truncation types

+ -> U, S, V, ϵ

Compute the (possibly truncated) singular value decomposition such that norm(permute(t, (leftind, rightind)) - U * S * V) ≈ ϵ, where ϵ thus represents the truncation error.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using tsvd!(t, trunc = notrunc(), p = 2).

A truncation parameter trunc can be specified for the new internal dimension, in which case a truncated singular value decomposition will be computed. Choices are:

  • notrunc(): no truncation (default);
  • truncerr(η::Real): truncates such that the p-norm of the truncated singular values is smaller than η;
  • truncdim(χ::Int): truncates such that the equivalent total dimension of the internal vector space is no larger than χ;
  • truncspace(V): truncates such that the dimension of the internal vector space is smaller than that of V in any sector.
  • truncbelow(η::Real): truncates such that every singular value is larger then η ;

Truncation options can also be combined using &, i.e. truncbelow(η) & truncdim(χ) will choose the truncation space such that every singular value is larger than η, and the equivalent total dimension of the internal vector space is no larger than χ.

The method tsvd also returns the truncation error ϵ, computed as the p norm of the singular values that were truncated.

THe keyword alg can be equal to SVD() or SDD(), corresponding to the underlying LAPACK algorithm that computes the decomposition (_gesvd or _gesdd).

Orthogonality requires InnerProductStyle(t) <: HasInnerProduct, and tsvd(!) is currently only implemented for InnerProductStyle(t) === EuclideanInnerProduct().

source
TensorKit.eighFunction
eigh(t::AbstractTensorMap, (leftind, rightind)::Index2Tuple) -> D, V

Compute eigenvalue factorization of tensor t as linear map from rightind to leftind. The function eigh assumes that the linear map is hermitian and D and V tensors with the same scalartype as t. See eig and eigen for non-hermitian tensors. Hermiticity requires that the tensor acts on inner product spaces, and the current implementation requires InnerProductStyle(t) === EuclideanInnerProduct().

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using eigh!(t). Note that the permuted tensor on which eigh! is called should have equal domain and codomain, as otherwise the eigenvalue decomposition is meaningless and cannot satisfy

permute(t, (leftind, rightind)) * V = V * D

See also eigen and eig.

source
TensorKit.eigFunction
eig(t::AbstractTensor, (leftind, rightind)::Index2Tuple; kwargs...) -> D, V

Compute eigenvalue factorization of tensor t as linear map from rightind to leftind. The function eig assumes that the linear map is not hermitian and returns type stable complex valued D and V tensors for both real and complex valued t. See eigh for hermitian linear maps

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using eig!(t). Note that the permuted tensor on which eig! is called should have equal domain and codomain, as otherwise the eigenvalue decomposition is meaningless and cannot satisfy

permute(t, (leftind, rightind)) * V = V * D

Accepts the same keyword arguments scale and permute as eigen of dense matrices. See the corresponding documentation for more information.

See also eigen and eigh.

source
LinearAlgebra.eigenFunction
eigen(t::AbstractTensor, (leftind, rightind)::Index2Tuple; kwargs...) -> D, V

Compute eigenvalue factorization of tensor t as linear map from rightind to leftind.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using eigen!(t). Note that the permuted tensor on which eigen! is called should have equal domain and codomain, as otherwise the eigenvalue decomposition is meaningless and cannot satisfy

permute(t, (leftind, rightind)) * V = V * D

Accepts the same keyword arguments scale and permute as eigen of dense matrices. See the corresponding documentation for more information.

See also eig and eigh

source
LinearAlgebra.isposdefFunction
isposdef(t::AbstractTensor, (leftind, rightind)::Index2Tuple) -> ::Bool

Test whether a tensor t is positive definite as linear map from rightind to leftind.

If leftind and rightind are not specified, the current partition of left and right indices of t is used. In that case, less memory is allocated if one allows the data in t to be destroyed/overwritten, by using isposdef!(t). Note that the permuted tensor on which isposdef! is called should have equal domain and codomain, as otherwise it is meaningless.

source

The corresponding destructive methods have an exclamation mark at the end of their name, and only accept the TensorMap object as well as the method-specific algorithm and keyword arguments.

TODO: document svd truncation types

diff --git a/dev/man/categories/index.html b/dev/man/categories/index.html index 8dbb05b6..c2f23f29 100644 --- a/dev/man/categories/index.html +++ b/dev/man/categories/index.html @@ -6,4 +6,4 @@ Springer Science & Business Media.
  • kitaev
      Kitaev, A. (2006). Anyons in an exactly solved model and beyond.
             Annals of Physics, 321(1), 2-111.
  • beer
        From categories to anyons: a travelogue
             Kerstin Beer, Dmytro Bondarenko, Alexander Hahn, Maria Kalabakov, Nicole Knust, Laura Niermann, Tobias J. Osborne, Christin Schridde, Stefan Seckmeyer, Deniz E. Stiegemann, and Ramona Wolf
    -        [https://arxiv.org/abs/1811.06670](https://arxiv.org/abs/1811.06670)
  • + [https://arxiv.org/abs/1811.06670](https://arxiv.org/abs/1811.06670) diff --git a/dev/man/intro/index.html b/dev/man/intro/index.html index ce256dce..22faa9b5 100644 --- a/dev/man/intro/index.html +++ b/dev/man/intro/index.html @@ -1,2 +1,2 @@ -Introduction · TensorKit.jl

    Introduction

    Before providing a typical "user guide" and discussing the implementation of TensorKit.jl on the next pages, let us discuss some of the rationale behind this package.

    What is a tensor?

    At the very start we should ponder about the most suitable and sufficiently general definition of a tensor. A good starting point is the following:

    • A tensor $t$ is an element from the tensor product of $N$ vector spaces $V_1 , V_2, …, V_N$, where $N$ is referred to as the rank or order of the tensor, i.e.

      $t ∈ V_1 ⊗ V_2 ⊗ … ⊗ V_N.$

    If you think of a tensor as an object with indices, a rank $N$ tensor has $N$ indices where every index is associated with the corresponding vector space in that it labels a particular basis in that space. We will return to index notation at the very end of this manual.

    As the tensor product of vector spaces is itself a vector space, this implies that a tensor behaves as a vector, i.e. tensors from the same tensor product space can be added and multiplied by scalars. The tensor product is only defined for vector spaces over the same field of scalars, e.g. there is no meaning in $ℝ^5 ⊗ ℂ^3$. When all the vector spaces in the tensor product have an inner product, this also implies an inner product for the tensor product space. It is hence clear that the different vector spaces in the tensor product should have some form of homogeneity in their structure, yet they do not need to be all equal and can e.g. have different dimensions. It goes without saying that defining the vector spaces and their properties will be an important part of the definition of a tensor. As a consequence, this also constitutes a significant part of the implementation, and is discussed in the section on Vector spaces.

    Aside from the interpretation of a tensor as a vector, we also want to interpret it as a matrix (or more correctly, a linear map) in order to decompose tensors using linear algebra factorisations (e.g. eigenvalue or singular value decomposition). Henceforth, we use the term "tensor map" as follows:

    • A tensor map $t$ is a linear map from a source or domain $W_1 ⊗ W_2 ⊗ … ⊗ W_{N_2}$ to a target or codomain $V_1 ⊗ V_2 ⊗ … ⊗ V_{N_1}$, i.e.

      $t:W_1 ⊗ W_2 ⊗ … ⊗ W_{N_2} → V_1 ⊗ V_2 ⊗ … ⊗ V_{N_1}.$

    A tensor of rank $N$ is then just a special case of a tensor map with $N_1 = N$ and $N_2 = 0$. A contraction between two tensors is just a composition of linear maps (i.e. matrix multiplication), where the contracted indices correspond to the domain of the first tensor and the codomain of the second tensor.

    In order to allow for arbitrary tensor contractions or decompositions, we need to be able to reorganise which vector spaces appear in the domain and the codomain of the tensor map, and in which order. This amounts to defining canonical isomorphisms between the different ways to order and partition the tensor indices (i.e. the vector spaces). For example, a linear map $W → V$ is often denoted as a rank 2 tensor in $V ⊗ W^*$, where $W^*$ corresponds to the dual space of $W$. This simple example introduces two new concepts.

    1. Typical vector spaces can appear in the domain and codomain in different related forms, e.g. as normal space or dual space. In fact, the most generic case is that every vector space $V$ has associated with it a dual space $V^*$, a conjugate space $\overline{V}$ and a conjugate dual space $\overline{V}^*$. The four different vector spaces $V$, $V^*$, $\overline{V}$ and $\overline{V}^*$ correspond to the representation spaces of respectively the fundamental, dual or contragredient, complex conjugate and dual complex conjugate representation of the general linear group $\mathsf{GL}(V)$. In index notation these spaces are denoted with respectively contravariant (upper), covariant (lower), dotted contravariant and dotted covariant indices.

      For real vector spaces, the conjugate (dual) space is identical to the normal (dual) space and we only have upper and lower indices, i.e. this is the setting of e.g. general relativity. For (complex) vector spaces with a sesquilinear inner product $\overline{V} ⊗ V → ℂ$, the inner product allows to define an isomorphism from the conjugate space to the dual space (known as Riesz representation theorem in the more general context of Hilbert spaces).

      In particular, in spaces with a Euclidean inner product (the setting of e.g. quantum mechanics), the conjugate and dual space are naturally isomorphic (because the dual and conjugate representation of the unitary group are the same). Again we only need upper and lower indices (or kets and bras).

      Finally, in $ℝ^d$ with a Euclidean inner product, these four different spaces are all equivalent and we only need one type of index. The space is completely characterized by its dimension $d$. This is the setting of much of classical mechanics and we refer to such tensors as cartesian tensors and the corresponding space as cartesian space. These are the tensors that can equally well be represented as multidimensional arrays (i.e. using some AbstractArray{<:Real,N} in Julia) without loss of structure.

      The implementation of all of this is discussed in Vector spaces.

    2. In the generic case, the identification between maps $W → V$ and tensors in $V ⊗ W^*$ is not an equivalence but an isomorphism, which needs to be defined. Similarly, there is an isomorphism between between $V ⊗ W$ and $W ⊗ V$ that can be non-trivial (e.g. in the case of fermions / super vector spaces). The correct formalism here is provided by theory of monoidal categories, which is introduced on the next page. Nonetheless, we try to hide these canonical isomorphisms from the user wherever possible, and one does not need to know category theory to be able to use this package.

    This brings us to our final (yet formal) definition

    • A tensor (map) is a homomorphism between two objects from the category $\mathbf{Vect}$ (or some subcategory thereof). In practice, this will be $\mathbf{FinVect}$, the category of finite dimensional vector spaces. More generally even, our concept of a tensor makes sense, in principle, for any linear (a.k.a. $\mathbf{Vect}$-enriched) monoidal category. We refer to the next page on "Monoidal categories and their properties".

    Symmetries and block sparsity

    Physical problems often have some symmetry, i.e. the setup is invariant under the action of a group $\mathsf{G}$ which acts on the vector spaces $V$ in the problem according to a certain representation. Having quantum mechanics in mind, TensorKit.jl is so far restricted to unitary representations. A general representation space $V$ can be specified as the number of times every irreducible representation (irrep) $a$ of $\mathsf{G}$ appears, i.e.

    $V = \bigoplus_{a} ℂ^{n_a} ⊗ R_a$

    with $R_a$ the space associated with irrep $a$ of $\mathsf{G}$, which itself has dimension $d_a$ (often called the quantum dimension), and $n_a$ the number of times this irrep appears in $V$. If the unitary irrep $a$ for $g ∈ \mathsf{G}$ is given by $u_a(g)$, then there exists a specific basis for $V$ such that the group action of $\mathsf{G}$ on $V$ is given by the unitary representation

    $u(g) = \bigoplus_{a} 𝟙_{n_a} ⊗ u_a(g)$

    with $𝟙_{n_a}$ the $n_a × n_a$ identity matrix. The total dimension of $V$ is given by $∑_a n_a d_a$.

    The reason for implementing symmetries is to exploit the computation and memory gains resulting from restricting to tensor maps $t:W_1 ⊗ W_2 ⊗ … ⊗ W_{N_2} → V_1 ⊗ V_2 ⊗ … ⊗ V_{N_1}$ that are equivariant under the symmetry, i.e. that act as intertwiners between the symmetry action on the domain and the codomain. Indeed, such tensors should be block diagonal because of Schur's lemma, but only after we couple the individual irreps in the spaces $W_i$ to a joint irrep, which is then again split into the individual irreps of the spaces $V_i$. The basis change from the tensor product of irreps in the (co)domain to the joint irrep is implemented by a sequence of Clebsch–Gordan coefficients, also known as a fusion (or splitting) tree. We implement the necessary machinery to manipulate these fusion trees under index permutations and repartitions for arbitrary groups $\mathsf{G}$. In particular, this fits with the formalism of monoidal categories, and more specifically fusion categories, and only requires the topological data of the group, i.e. the fusion rules of the irreps, their quantum dimensions and the F-symbol (6j-symbol or more precisely Racah's W-symbol in the case of $\mathsf{SU}_2$). In particular, we don't actually need the Clebsch–Gordan coefficients themselves (but they can be useful for checking purposes).

    Hence, a second major part of TensorKit.jl is the interface and implementation for specifying symmetries, and further details are provided in Sectors, representation spaces and fusion trees.

    +Introduction · TensorKit.jl

    Introduction

    Before providing a typical "user guide" and discussing the implementation of TensorKit.jl on the next pages, let us discuss some of the rationale behind this package.

    What is a tensor?

    At the very start we should ponder about the most suitable and sufficiently general definition of a tensor. A good starting point is the following:

    • A tensor $t$ is an element from the tensor product of $N$ vector spaces $V_1 , V_2, …, V_N$, where $N$ is referred to as the rank or order of the tensor, i.e.

      $t ∈ V_1 ⊗ V_2 ⊗ … ⊗ V_N.$

    If you think of a tensor as an object with indices, a rank $N$ tensor has $N$ indices where every index is associated with the corresponding vector space in that it labels a particular basis in that space. We will return to index notation at the very end of this manual.

    As the tensor product of vector spaces is itself a vector space, this implies that a tensor behaves as a vector, i.e. tensors from the same tensor product space can be added and multiplied by scalars. The tensor product is only defined for vector spaces over the same field of scalars, e.g. there is no meaning in $ℝ^5 ⊗ ℂ^3$. When all the vector spaces in the tensor product have an inner product, this also implies an inner product for the tensor product space. It is hence clear that the different vector spaces in the tensor product should have some form of homogeneity in their structure, yet they do not need to be all equal and can e.g. have different dimensions. It goes without saying that defining the vector spaces and their properties will be an important part of the definition of a tensor. As a consequence, this also constitutes a significant part of the implementation, and is discussed in the section on Vector spaces.

    Aside from the interpretation of a tensor as a vector, we also want to interpret it as a matrix (or more correctly, a linear map) in order to decompose tensors using linear algebra factorisations (e.g. eigenvalue or singular value decomposition). Henceforth, we use the term "tensor map" as follows:

    • A tensor map $t$ is a linear map from a source or domain $W_1 ⊗ W_2 ⊗ … ⊗ W_{N_2}$ to a target or codomain $V_1 ⊗ V_2 ⊗ … ⊗ V_{N_1}$, i.e.

      $t:W_1 ⊗ W_2 ⊗ … ⊗ W_{N_2} → V_1 ⊗ V_2 ⊗ … ⊗ V_{N_1}.$

    A tensor of rank $N$ is then just a special case of a tensor map with $N_1 = N$ and $N_2 = 0$. A contraction between two tensors is just a composition of linear maps (i.e. matrix multiplication), where the contracted indices correspond to the domain of the first tensor and the codomain of the second tensor.

    In order to allow for arbitrary tensor contractions or decompositions, we need to be able to reorganise which vector spaces appear in the domain and the codomain of the tensor map, and in which order. This amounts to defining canonical isomorphisms between the different ways to order and partition the tensor indices (i.e. the vector spaces). For example, a linear map $W → V$ is often denoted as a rank 2 tensor in $V ⊗ W^*$, where $W^*$ corresponds to the dual space of $W$. This simple example introduces two new concepts.

    1. Typical vector spaces can appear in the domain and codomain in different related forms, e.g. as normal space or dual space. In fact, the most generic case is that every vector space $V$ has associated with it a dual space $V^*$, a conjugate space $\overline{V}$ and a conjugate dual space $\overline{V}^*$. The four different vector spaces $V$, $V^*$, $\overline{V}$ and $\overline{V}^*$ correspond to the representation spaces of respectively the fundamental, dual or contragredient, complex conjugate and dual complex conjugate representation of the general linear group $\mathsf{GL}(V)$. In index notation these spaces are denoted with respectively contravariant (upper), covariant (lower), dotted contravariant and dotted covariant indices.

      For real vector spaces, the conjugate (dual) space is identical to the normal (dual) space and we only have upper and lower indices, i.e. this is the setting of e.g. general relativity. For (complex) vector spaces with a sesquilinear inner product $\overline{V} ⊗ V → ℂ$, the inner product allows to define an isomorphism from the conjugate space to the dual space (known as Riesz representation theorem in the more general context of Hilbert spaces).

      In particular, in spaces with a Euclidean inner product (the setting of e.g. quantum mechanics), the conjugate and dual space are naturally isomorphic (because the dual and conjugate representation of the unitary group are the same). Again we only need upper and lower indices (or kets and bras).

      Finally, in $ℝ^d$ with a Euclidean inner product, these four different spaces are all equivalent and we only need one type of index. The space is completely characterized by its dimension $d$. This is the setting of much of classical mechanics and we refer to such tensors as cartesian tensors and the corresponding space as cartesian space. These are the tensors that can equally well be represented as multidimensional arrays (i.e. using some AbstractArray{<:Real,N} in Julia) without loss of structure.

      The implementation of all of this is discussed in Vector spaces.

    2. In the generic case, the identification between maps $W → V$ and tensors in $V ⊗ W^*$ is not an equivalence but an isomorphism, which needs to be defined. Similarly, there is an isomorphism between between $V ⊗ W$ and $W ⊗ V$ that can be non-trivial (e.g. in the case of fermions / super vector spaces). The correct formalism here is provided by theory of monoidal categories, which is introduced on the next page. Nonetheless, we try to hide these canonical isomorphisms from the user wherever possible, and one does not need to know category theory to be able to use this package.

    This brings us to our final (yet formal) definition

    • A tensor (map) is a homomorphism between two objects from the category $\mathbf{Vect}$ (or some subcategory thereof). In practice, this will be $\mathbf{FinVect}$, the category of finite dimensional vector spaces. More generally even, our concept of a tensor makes sense, in principle, for any linear (a.k.a. $\mathbf{Vect}$-enriched) monoidal category. We refer to the next page on "Monoidal categories and their properties".

    Symmetries and block sparsity

    Physical problems often have some symmetry, i.e. the setup is invariant under the action of a group $\mathsf{G}$ which acts on the vector spaces $V$ in the problem according to a certain representation. Having quantum mechanics in mind, TensorKit.jl is so far restricted to unitary representations. A general representation space $V$ can be specified as the number of times every irreducible representation (irrep) $a$ of $\mathsf{G}$ appears, i.e.

    $V = \bigoplus_{a} ℂ^{n_a} ⊗ R_a$

    with $R_a$ the space associated with irrep $a$ of $\mathsf{G}$, which itself has dimension $d_a$ (often called the quantum dimension), and $n_a$ the number of times this irrep appears in $V$. If the unitary irrep $a$ for $g ∈ \mathsf{G}$ is given by $u_a(g)$, then there exists a specific basis for $V$ such that the group action of $\mathsf{G}$ on $V$ is given by the unitary representation

    $u(g) = \bigoplus_{a} 𝟙_{n_a} ⊗ u_a(g)$

    with $𝟙_{n_a}$ the $n_a × n_a$ identity matrix. The total dimension of $V$ is given by $∑_a n_a d_a$.

    The reason for implementing symmetries is to exploit the computation and memory gains resulting from restricting to tensor maps $t:W_1 ⊗ W_2 ⊗ … ⊗ W_{N_2} → V_1 ⊗ V_2 ⊗ … ⊗ V_{N_1}$ that are equivariant under the symmetry, i.e. that act as intertwiners between the symmetry action on the domain and the codomain. Indeed, such tensors should be block diagonal because of Schur's lemma, but only after we couple the individual irreps in the spaces $W_i$ to a joint irrep, which is then again split into the individual irreps of the spaces $V_i$. The basis change from the tensor product of irreps in the (co)domain to the joint irrep is implemented by a sequence of Clebsch–Gordan coefficients, also known as a fusion (or splitting) tree. We implement the necessary machinery to manipulate these fusion trees under index permutations and repartitions for arbitrary groups $\mathsf{G}$. In particular, this fits with the formalism of monoidal categories, and more specifically fusion categories, and only requires the topological data of the group, i.e. the fusion rules of the irreps, their quantum dimensions and the F-symbol (6j-symbol or more precisely Racah's W-symbol in the case of $\mathsf{SU}_2$). In particular, we don't actually need the Clebsch–Gordan coefficients themselves (but they can be useful for checking purposes).

    Hence, a second major part of TensorKit.jl is the interface and implementation for specifying symmetries, and further details are provided in Sectors, representation spaces and fusion trees.

    diff --git a/dev/man/sectors/index.html b/dev/man/sectors/index.html index f692bfe6..b1d4f8c2 100644 --- a/dev/man/sectors/index.html +++ b/dev/man/sectors/index.html @@ -234,7 +234,7 @@ FusionTree{Irrep[SU₂]}((1/2, 1/2, 1/2, 1/2, 1/2), 1/2, (true, false, false, true, false), (1, 1/2, 0)) FusionTree{Irrep[SU₂]}((1/2, 1/2, 1/2, 1/2, 1/2), 1/2, (true, false, false, true, false), (0, 1/2, 1)) FusionTree{Irrep[SU₂]}((1/2, 1/2, 1/2, 1/2, 1/2), 1/2, (true, false, false, true, false), (1, 1/2, 1)) - FusionTree{Irrep[SU₂]}((1/2, 1/2, 1/2, 1/2, 1/2), 1/2, (true, false, false, true, false), (1, 3/2, 1))
    julia> iter = fusiontrees(ntuple(n->s, 16))TensorKit.FusionTreeIterator{SU2Irrep, 16, NTuple{16, Tuple{SU2Irrep}}}(((Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),)), Irrep[SU₂](0), (false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false))
    julia> sum(n->1, iter)1430
    julia> length(iter)1430
    julia> @elapsed sum(n->1, iter)0.048881562
    julia> @elapsed length(iter)3.9935e-5
    julia> s2 = s ⊠ sIrrep[SU₂ × SU₂](1/2, 1/2)
    julia> collect(fusiontrees((s2,s2,s2,s2)))4-element Vector{FusionTree{ProductSector{Tuple{SU2Irrep, SU2Irrep}}, 4, 2, 3}}: + FusionTree{Irrep[SU₂]}((1/2, 1/2, 1/2, 1/2, 1/2), 1/2, (true, false, false, true, false), (1, 3/2, 1))
    julia> iter = fusiontrees(ntuple(n->s, 16))TensorKit.FusionTreeIterator{SU2Irrep, 16, NTuple{16, Tuple{SU2Irrep}}}(((Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),), (Irrep[SU₂](1/2),)), Irrep[SU₂](0), (false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false))
    julia> sum(n->1, iter)1430
    julia> length(iter)1430
    julia> @elapsed sum(n->1, iter)0.051907336
    julia> @elapsed length(iter)3.9894e-5
    julia> s2 = s ⊠ sIrrep[SU₂ × SU₂](1/2, 1/2)
    julia> collect(fusiontrees((s2,s2,s2,s2)))4-element Vector{FusionTree{ProductSector{Tuple{SU2Irrep, SU2Irrep}}, 4, 2, 3}}: FusionTree{Irrep[SU₂ × SU₂]}(((1/2, 1/2), (1/2, 1/2), (1/2, 1/2), (1/2, 1/2)), (0, 0), (false, false, false, false), ((0, 0), (1/2, 1/2))) FusionTree{Irrep[SU₂ × SU₂]}(((1/2, 1/2), (1/2, 1/2), (1/2, 1/2), (1/2, 1/2)), (0, 0), (false, false, false, false), ((1, 0), (1/2, 1/2))) FusionTree{Irrep[SU₂ × SU₂]}(((1/2, 1/2), (1/2, 1/2), (1/2, 1/2), (1/2, 1/2)), (0, 0), (false, false, false, false), ((0, 1), (1/2, 1/2))) @@ -315,4 +315,4 @@ 0.618034 0.786151 0.786151 -0.618034
    julia> Fτ'*Fτ2×2 Matrix{Float64}: 1.0 0.0 - 0.0 1.0
    julia> polar(x) = rationalize.((abs(x), angle(x)/(2pi)))polar (generic function with 1 method)
    julia> Rsymbol(τ,τ,𝟙) |> polar(1//1, 2//5)
    julia> Rsymbol(τ,τ,τ) |> polar(1//1, -3//10)
    julia> twist(τ) |> polar(1//1, -2//5) + 0.0 1.0
    julia> polar(x) = rationalize.((abs(x), angle(x)/(2pi)))polar (generic function with 1 method)
    julia> Rsymbol(τ,τ,𝟙) |> polar(1//1, 2//5)
    julia> Rsymbol(τ,τ,τ) |> polar(1//1, -3//10)
    julia> twist(τ) |> polar(1//1, -2//5) diff --git a/dev/man/spaces/index.html b/dev/man/spaces/index.html index 5d77fbac..9b1939f3 100644 --- a/dev/man/spaces/index.html +++ b/dev/man/spaces/index.html @@ -8,7 +8,7 @@ struct ComplexNumbers <: Field end const ℝ = RealNumbers() -const ℂ = ComplexNumbers()

    Note that and can be typed as \bbR+TAB and \bbC+TAB. One reason for defining this new type hierarchy instead of recycling the types from Julia's Number hierarchy is to introduce some syntactic sugar without committing type piracy. In particular, we now have

    julia> 3 ∈ ℝtrue
    julia> 5.0 ∈ ℂtrue
    julia> 5.0+1.0*im ∈ ℝfalse
    julia> Float64 ⊆ ℝtrue
    julia> ComplexF64 ⊆ ℂtrue
    julia> ℝ ⊆ ℂtrue
    julia> ℂ ⊆ ℝfalse

    and furthermore —probably more usefully— ℝ^n and ℂ^n create specific elementary vector spaces as described in the next section. The underlying field of a vector space or tensor a can be obtained with field(a):

    TensorKit.fieldFunction
    field(V::VectorSpace) -> Field

    Return the field type over which a vector space is defined.

    source

    Elementary spaces

    As mentioned at the beginning of this section, vector spaces that are associated with the individual indices of a tensor should be implemented as subtypes of ElementarySpace. As the domain and codomain of a tensor map will be the tensor product of such objects which all have the same type, it is important that related vector spaces, e.g. the dual space, are objects of the same concrete type (i.e. with the same type parameters in case of a parametric type). In particular, every ElementarySpace should implement the following methods

    For convenience, the dual of a space V can also be obtained as V'.

    There is concrete type GeneralSpace which is completely characterized by its field 𝔽, its dimension and whether its the dual and/or complex conjugate of $𝔽^d$.

    struct GeneralSpace{𝔽} <: ElementarySpace
    +const ℂ = ComplexNumbers()

    Note that and can be typed as \bbR+TAB and \bbC+TAB. One reason for defining this new type hierarchy instead of recycling the types from Julia's Number hierarchy is to introduce some syntactic sugar without committing type piracy. In particular, we now have

    julia> 3 ∈ ℝtrue
    julia> 5.0 ∈ ℂtrue
    julia> 5.0+1.0*im ∈ ℝfalse
    julia> Float64 ⊆ ℝtrue
    julia> ComplexF64 ⊆ ℂtrue
    julia> ℝ ⊆ ℂtrue
    julia> ℂ ⊆ ℝfalse

    and furthermore —probably more usefully— ℝ^n and ℂ^n create specific elementary vector spaces as described in the next section. The underlying field of a vector space or tensor a can be obtained with field(a):

    TensorKit.fieldFunction
    field(V::VectorSpace) -> Field

    Return the field type over which a vector space is defined.

    source

    Elementary spaces

    As mentioned at the beginning of this section, vector spaces that are associated with the individual indices of a tensor should be implemented as subtypes of ElementarySpace. As the domain and codomain of a tensor map will be the tensor product of such objects which all have the same type, it is important that related vector spaces, e.g. the dual space, are objects of the same concrete type (i.e. with the same type parameters in case of a parametric type). In particular, every ElementarySpace should implement the following methods

    For convenience, the dual of a space V can also be obtained as V'.

    There is concrete type GeneralSpace which is completely characterized by its field 𝔽, its dimension and whether its the dual and/or complex conjugate of $𝔽^d$.

    struct GeneralSpace{𝔽} <: ElementarySpace
         d::Int
         dual::Bool
         conj::Bool
    @@ -27,4 +27,4 @@
         codomain::P1
         domain::P2
     end

    and can create it as either domain → codomain or codomain ← domain (where the arrows are obtained as \to+TAB or \leftarrow+TAB, and as \rightarrow+TAB respectively). The reason for first listing the codomain and than the domain will become clear in the section on tensor maps.

    Note that HomSpace is not a subtype of VectorSpace, i.e. we restrict the latter to denote certain categories and their objects, and keep HomSpace distinct. However, HomSpace has a number of properties defined, which we illustrate via examples

    julia> W = ℂ^2 ⊗ ℂ^3 → ℂ^3 ⊗ dual(ℂ^4)(ℂ^3 ⊗ (ℂ^4)') ← (ℂ^2 ⊗ ℂ^3)
    julia> field(W)
    julia> dual(W)((ℂ^3)' ⊗ (ℂ^2)') ← (ℂ^4 ⊗ (ℂ^3)')
    julia> adjoint(W)(ℂ^2 ⊗ ℂ^3) ← (ℂ^3 ⊗ (ℂ^4)')
    julia> spacetype(W)ComplexSpace
    julia> spacetype(typeof(W))ComplexSpace
    julia> W[1]ℂ^3
    julia> W[2](ℂ^4)'
    julia> W[3](ℂ^2)'
    julia> W[4](ℂ^3)'
    julia> dim(W)72

    Note that indexing W yields first the spaces in the codomain, followed by the dual of the spaces in the domain. This particular convention is useful in combination with the instances of type TensorMap, which represent morphisms living in such a HomSpace. Also note that dim(W) here seems to be the product of the dimensions of the individual spaces, but that this is no longer true once symmetries are involved. At any time will dim(::HomSpace) represent the number of linearly independent morphisms in this space.

    Partial order among vector spaces

    Vector spaces of the same spacetype can be given a partial order, based on whether there exist injective morphisms (a.k.a monomorphisms) or surjective morphisms (a.k.a. epimorphisms) between them. In particular, we define ismonomorphic(V1, V2), with Unicode synonym V1 ≾ V2 (obtained as \precsim+TAB), to express whether there exist monomorphisms in V1→V2. Similarly, we define isepimorphic(V1, V2), with Unicode synonym V1 ≿ V2 (obtained as \succsim+TAB), to express whether there exist epimorphisms in V1→V2. Finally, we define isisomorphic(V1, V2), with Unicode alternative V1 ≅ V2 (obtained as \cong+TAB), to express whether there exist isomorphism in V1→V2. In particular V1 ≅ V2 if and only if V1 ≾ V2 && V1 ≿ V2.

    For completeness, we also export the strict comparison operators and (\prec+TAB and \succ+TAB), with definitions

    ≺(V1::VectorSpace, V2::VectorSpace) = V1 ≾ V2 && !(V1 ≿ V2)
    -≻(V1::VectorSpace, V2::VectorSpace) = V1 ≿ V2 && !(V1 ≾ V2)

    However, as we expect these to be less commonly used, no ASCII alternative is provided.

    In the context of InnerProductStyle(V) <: EuclideanInnerProduct, V1 ≾ V2 implies that there exists isometries $W:V1 → V2$ such that $W^† ∘ W = \mathrm{id}_{V1}$, while V1 ≅ V2 implies that there exist unitaries $U:V1→V2$ such that $U^† ∘ U = \mathrm{id}_{V1}$ and $U ∘ U^† = \mathrm{id}_{V2}$.

    Note that spaces that are isomorphic are not necessarily equal. One can be a dual space, and the other a normal space, or one can be an instance of ProductSpace, while the other is an ElementarySpace. There will exist (infinitely) many isomorphisms between the corresponding spaces, but in general none of those will be canonical.

    There are also a number of convenience functions to create isomorphic spaces. The function fuse(V1, V2, ...) or fuse(V1 ⊗ V2 ⊗ ...) returns an elementary space that is isomorphic to V1 ⊗ V2 ⊗ .... The function flip(V::ElementarySpace) returns a space that is isomorphic to V but has isdual(flip(V)) == isdual(V'), i.e. if V is a normal space than flip(V) is a dual space. flip(V) is different from dual(V) in the case of GradedSpace. It is useful to flip a tensor index from a ket to a bra (or vice versa), by contracting that index with a unitary map from V1 to flip(V1). We refer to the reference on vector space methods for further information. Some examples:

    julia> ℝ^3 ≾ ℝ^5true
    julia> ℂ^3 ≾ (ℂ^5)'true
    julia> (ℂ^5) ≅ (ℂ^5)'true
    julia> fuse(ℝ^5, ℝ^3)ℝ^15
    julia> fuse(ℂ^3, (ℂ^5)' ⊗ ℂ^2)ℂ^30
    julia> fuse(ℂ^3, (ℂ^5)') ⊗ ℂ^2 ≅ fuse(ℂ^3, (ℂ^5)', ℂ^2) ≅ ℂ^3 ⊗ (ℂ^5)' ⊗ ℂ^2true
    julia> flip(ℂ^4)(ℂ^4)'
    julia> flip(ℂ^4) ≅ ℂ^4true
    julia> flip(ℂ^4) == ℂ^4false

    We also define the direct sum V1 and V2 as V1 ⊕ V2, where is obtained by typing \oplus+TAB. This is possible only if isdual(V1) == isdual(V2). With a little pun on Julia Base, oneunit applied to an elementary space (in the value or type domain) returns the one-dimensional space, which is isomorphic to the scalar field of the space itself. Some examples illustrate this better

    julia> ℝ^5 ⊕ ℝ^3ℝ^8
    julia> ℂ^5 ⊕ ℂ^3ℂ^8
    julia> ℂ^5 ⊕ (ℂ^3)'ERROR: SpaceMismatch("Direct sum of a vector space and its dual does not exist")
    julia> oneunit(ℝ^3)ℝ^1
    julia> ℂ^5 ⊕ oneunit(ComplexSpace)ℂ^6
    julia> oneunit((ℂ^3)')ℂ^1
    julia> (ℂ^5) ⊕ oneunit((ℂ^5))ℂ^6
    julia> (ℂ^5)' ⊕ oneunit((ℂ^5)')ERROR: SpaceMismatch("Direct sum of a vector space and its dual does not exist")

    Finally, while spaces have a partial order, there is no unique infimum or supremum of a two or more spaces. However, if V1 and V2 are two ElementarySpace instances with isdual(V1) == isdual(V2), then we can define a unique infimum V::ElementarySpace with the same value of isdual that satisfies V ≾ V1 and V ≾ V2, as well as a unique supremum W::ElementarySpace with the same value of isdual that satisfies W ≿ V1 and W ≿ V2. For CartesianSpace and ComplexSpace, this simply amounts to the space with minimal or maximal dimension, i.e.

    julia> infimum(ℝ^5, ℝ^3)ℝ^3
    julia> supremum(ℂ^5, ℂ^3)ℂ^5
    julia> supremum(ℂ^5, (ℂ^3)')ERROR: SpaceMismatch("Supremum of space and dual space does not exist")

    The names infimum and supremum are especially suited in the case of GradedSpace, as the infimum of two spaces might be different from either of those two spaces, and similar for the supremum.

    +≻(V1::VectorSpace, V2::VectorSpace) = V1 ≿ V2 && !(V1 ≾ V2)

    However, as we expect these to be less commonly used, no ASCII alternative is provided.

    In the context of InnerProductStyle(V) <: EuclideanInnerProduct, V1 ≾ V2 implies that there exists isometries $W:V1 → V2$ such that $W^† ∘ W = \mathrm{id}_{V1}$, while V1 ≅ V2 implies that there exist unitaries $U:V1→V2$ such that $U^† ∘ U = \mathrm{id}_{V1}$ and $U ∘ U^† = \mathrm{id}_{V2}$.

    Note that spaces that are isomorphic are not necessarily equal. One can be a dual space, and the other a normal space, or one can be an instance of ProductSpace, while the other is an ElementarySpace. There will exist (infinitely) many isomorphisms between the corresponding spaces, but in general none of those will be canonical.

    There are also a number of convenience functions to create isomorphic spaces. The function fuse(V1, V2, ...) or fuse(V1 ⊗ V2 ⊗ ...) returns an elementary space that is isomorphic to V1 ⊗ V2 ⊗ .... The function flip(V::ElementarySpace) returns a space that is isomorphic to V but has isdual(flip(V)) == isdual(V'), i.e. if V is a normal space than flip(V) is a dual space. flip(V) is different from dual(V) in the case of GradedSpace. It is useful to flip a tensor index from a ket to a bra (or vice versa), by contracting that index with a unitary map from V1 to flip(V1). We refer to the reference on vector space methods for further information. Some examples:

    julia> ℝ^3 ≾ ℝ^5true
    julia> ℂ^3 ≾ (ℂ^5)'true
    julia> (ℂ^5) ≅ (ℂ^5)'true
    julia> fuse(ℝ^5, ℝ^3)ℝ^15
    julia> fuse(ℂ^3, (ℂ^5)' ⊗ ℂ^2)ℂ^30
    julia> fuse(ℂ^3, (ℂ^5)') ⊗ ℂ^2 ≅ fuse(ℂ^3, (ℂ^5)', ℂ^2) ≅ ℂ^3 ⊗ (ℂ^5)' ⊗ ℂ^2true
    julia> flip(ℂ^4)(ℂ^4)'
    julia> flip(ℂ^4) ≅ ℂ^4true
    julia> flip(ℂ^4) == ℂ^4false

    We also define the direct sum V1 and V2 as V1 ⊕ V2, where is obtained by typing \oplus+TAB. This is possible only if isdual(V1) == isdual(V2). With a little pun on Julia Base, oneunit applied to an elementary space (in the value or type domain) returns the one-dimensional space, which is isomorphic to the scalar field of the space itself. Some examples illustrate this better

    julia> ℝ^5 ⊕ ℝ^3ℝ^8
    julia> ℂ^5 ⊕ ℂ^3ℂ^8
    julia> ℂ^5 ⊕ (ℂ^3)'ERROR: SpaceMismatch("Direct sum of a vector space and its dual does not exist")
    julia> oneunit(ℝ^3)ℝ^1
    julia> ℂ^5 ⊕ oneunit(ComplexSpace)ℂ^6
    julia> oneunit((ℂ^3)')ℂ^1
    julia> (ℂ^5) ⊕ oneunit((ℂ^5))ℂ^6
    julia> (ℂ^5)' ⊕ oneunit((ℂ^5)')ERROR: SpaceMismatch("Direct sum of a vector space and its dual does not exist")

    Finally, while spaces have a partial order, there is no unique infimum or supremum of a two or more spaces. However, if V1 and V2 are two ElementarySpace instances with isdual(V1) == isdual(V2), then we can define a unique infimum V::ElementarySpace with the same value of isdual that satisfies V ≾ V1 and V ≾ V2, as well as a unique supremum W::ElementarySpace with the same value of isdual that satisfies W ≿ V1 and W ≿ V2. For CartesianSpace and ComplexSpace, this simply amounts to the space with minimal or maximal dimension, i.e.

    julia> infimum(ℝ^5, ℝ^3)ℝ^3
    julia> supremum(ℂ^5, ℂ^3)ℂ^5
    julia> supremum(ℂ^5, (ℂ^3)')ERROR: SpaceMismatch("Supremum of space and dual space does not exist")

    The names infimum and supremum are especially suited in the case of GradedSpace, as the infimum of two spaces might be different from either of those two spaces, and similar for the supremum.

    diff --git a/dev/man/tensors/index.html b/dev/man/tensors/index.html index 851ebc66..65070d8d 100644 --- a/dev/man/tensors/index.html +++ b/dev/man/tensors/index.html @@ -6,12 +6,12 @@ Tensor(undef, codomain) Tensor(undef, eltype::Type{<:Number}, codomain)

    Here, f is any of the typical functions from Base that normally create arrays, namely zeros, ones, rand, randn and Random.randexp. Remember that one(codomain) is the empty ProductSpace{S,0}(). The third and fourth calling syntax use the UndefInitializer from Julia Base and generates a TensorMap with unitialized data, which can thus contain NaNs.

    In all of these constructors, the last two arguments can be replaced by domain→codomain or codomain←domain, where the arrows are obtained as \rightarrow+TAB and \leftarrow+TAB and create a HomSpace as explained in the section on Spaces of morphisms. Some examples are perhaps in order

    julia> t1 = randn(ℂ^2 ⊗ ℂ^3, ℂ^2)TensorMap((ℂ^2 ⊗ ℂ^3) ← ℂ^2):
     [:, :, 1] =
    - 0.6392403631156164  -0.5644838009210904  0.07184161232575023
    - 0.267628515235668    0.5992385254523327  1.3927651538511976
    +  0.20370327474557676   0.7822800951213543   0.03899970483537545
    + -0.04483035636725029  -0.5004846542129925  -2.9052315083206484
     
     [:, :, 2] =
    -  1.9488398987748803  -0.25069994000184254  -0.7859422440421244
    - -0.9132994579562235  -0.6622921759833666   -0.3754515410793584
    julia> t2 = zeros(Float32, ℂ^2 ⊗ ℂ^3 ← ℂ^2)TensorMap((ℂ^2 ⊗ ℂ^3) ← ℂ^2): + 1.5706198264339848 -0.33747619956298264 0.16491034651303213 + 1.314044566766868 1.8042686491626758 0.7015820683108515
    julia> t2 = zeros(Float32, ℂ^2 ⊗ ℂ^3 ← ℂ^2)TensorMap((ℂ^2 ⊗ ℂ^3) ← ℂ^2): [:, :, 1] = 0.0f0 0.0f0 0.0f0 0.0f0 0.0f0 0.0f0 @@ -22,34 +22,34 @@ The type `TensorMap` exists, but no method is defined for this combination of argument types when trying to construct it. Closest candidates are: - TensorMap(::typeof(randuniform), ::HomSpace) - @ TensorKit deprecated.jl:103 TensorMap(::typeof(randn), ::HomSpace) @ TensorKit deprecated.jl:103 - TensorMap(::typeof(randhaar), ::HomSpace) + TensorMap(::typeof(randuniform), ::HomSpace) + @ TensorKit deprecated.jl:103 + TensorMap(::typeof(rand), ::HomSpace) @ TensorKit deprecated.jl:103 ...
    julia> domain(t1) == domain(t2) == domain(t3)ERROR: UndefVarError: `t3` not defined in `Main` Suggestion: check for spelling errors or missing imports.
    julia> codomain(t1) == codomain(t2) == codomain(t3)ERROR: UndefVarError: `t3` not defined in `Main` Suggestion: check for spelling errors or missing imports.
    julia> disp(x) = show(IOContext(Core.stdout, :compact=>false), "text/plain", trunc.(x; digits = 3));
    julia> t1[] |> disp2×3×2 StridedViews.StridedView{Float64, 3, Array{Float64, 3}, typeof(identity)}: [:, :, 1] = - 0.639 -0.564 0.071 - 0.267 0.599 1.392 + 0.203 0.782 0.038 + -0.044 -0.5 -2.905 [:, :, 2] = - 1.948 -0.25 -0.785 - -0.913 -0.662 -0.375
    julia> block(t1, Trivial()) |> disp6×2 Array{Float64, 2}: - 0.639 1.948 - 0.267 -0.913 - -0.564 -0.25 - 0.599 -0.662 - 0.071 -0.785 - 1.392 -0.375
    julia> reshape(t1[], dim(codomain(t1)), dim(domain(t1))) |> disp6×2 Array{Float64, 2}: - 0.639 1.948 - 0.267 -0.913 - -0.564 -0.25 - 0.599 -0.662 - 0.071 -0.785 - 1.392 -0.375

    Finally, all constructors can also be replaced by Tensor(..., codomain), in which case the domain is assumed to be the empty ProductSpace{S,0}(), which can easily be obtained as one(codomain). Indeed, the empty product space is the unit object of the monoidal category, equivalent to the field of scalars 𝕜, and thus the multiplicative identity (especially since * also acts as tensor product on vector spaces).

    The matrices created by f are the matrices $B_c$ discussed above, i.e. those returned by block(t, c). Only numerical matrices of type DenseMatrix are accepted, which in practice just means Julia's intrinsic Matrix{T} for some T<:Number. In the future, we will add support for CuMatrix from CuArrays.jl to harness GPU computing power, and maybe SharedArray from the Julia's SharedArrays standard library.

    Support for static or sparse data is currently unavailable, and if it would be implemented, it would lead to new subtypes of AbstractTensorMap which are distinct from TensorMap. Future implementations of e.g. SparseTensorMap or StaticTensorMap could be useful. Furthermore, there could be specific implementations for tensors whose blocks are Diagonal.

    Tensor maps from existing data

    To create a TensorMap with existing data, one can use the aforementioned form but with the function f replaced with the actual data, i.e. TensorMap(data, codomain, domain) or any of its equivalents.

    Here, data can be of two types. It can be a dictionary (any Associative subtype) which has blocksectors c of type sectortype(codomain) as keys, and the corresponding matrix blocks as value, i.e. data[c] is some DenseMatrix of size (blockdim(codomain, c), blockdim(domain, c)). This is the form of how the data is stored within the TensorMap objects.

    For those space types for which a TensorMap can be converted to a plain multidimensional array, the data can also be a general DenseArray, either of rank N₁+N₂ and with matching size (dims(codomain)..., dims(domain)...), or just as a DenseMatrix with size (dim(codomain), dim(domain)). This is true in particular if the sector type is Trivial, e.g. for CartesianSpace or ComplexSpace. Then the data array is just reshaped into matrix form and referred to as such in the resulting TensorMap instance. When spacetype is GradedSpace, the TensorMap constructor will try to reconstruct the tensor data such that the resulting tensor t satisfies data == convert(Array, t). This might not be possible, if the data does not respect the symmetry structure. This procedure can be sketched using a simple physical example, namely the SWAP gate on two qubits,

    \[\begin{align*} + 1.57 -0.337 0.164 + 1.314 1.804 0.701
    julia> block(t1, Trivial()) |> disp6×2 Array{Float64, 2}: + 0.203 1.57 + -0.044 1.314 + 0.782 -0.337 + -0.5 1.804 + 0.038 0.164 + -2.905 0.701
    julia> reshape(t1[], dim(codomain(t1)), dim(domain(t1))) |> disp6×2 Array{Float64, 2}: + 0.203 1.57 + -0.044 1.314 + 0.782 -0.337 + -0.5 1.804 + 0.038 0.164 + -2.905 0.701

    Finally, all constructors can also be replaced by Tensor(..., codomain), in which case the domain is assumed to be the empty ProductSpace{S,0}(), which can easily be obtained as one(codomain). Indeed, the empty product space is the unit object of the monoidal category, equivalent to the field of scalars 𝕜, and thus the multiplicative identity (especially since * also acts as tensor product on vector spaces).

    The matrices created by f are the matrices $B_c$ discussed above, i.e. those returned by block(t, c). Only numerical matrices of type DenseMatrix are accepted, which in practice just means Julia's intrinsic Matrix{T} for some T<:Number. In the future, we will add support for CuMatrix from CuArrays.jl to harness GPU computing power, and maybe SharedArray from the Julia's SharedArrays standard library.

    Support for static or sparse data is currently unavailable, and if it would be implemented, it would lead to new subtypes of AbstractTensorMap which are distinct from TensorMap. Future implementations of e.g. SparseTensorMap or StaticTensorMap could be useful. Furthermore, there could be specific implementations for tensors whose blocks are Diagonal.

    Tensor maps from existing data

    To create a TensorMap with existing data, one can use the aforementioned form but with the function f replaced with the actual data, i.e. TensorMap(data, codomain, domain) or any of its equivalents.

    Here, data can be of two types. It can be a dictionary (any Associative subtype) which has blocksectors c of type sectortype(codomain) as keys, and the corresponding matrix blocks as value, i.e. data[c] is some DenseMatrix of size (blockdim(codomain, c), blockdim(domain, c)). This is the form of how the data is stored within the TensorMap objects.

    For those space types for which a TensorMap can be converted to a plain multidimensional array, the data can also be a general DenseArray, either of rank N₁+N₂ and with matching size (dims(codomain)..., dims(domain)...), or just as a DenseMatrix with size (dim(codomain), dim(domain)). This is true in particular if the sector type is Trivial, e.g. for CartesianSpace or ComplexSpace. Then the data array is just reshaped into matrix form and referred to as such in the resulting TensorMap instance. When spacetype is GradedSpace, the TensorMap constructor will try to reconstruct the tensor data such that the resulting tensor t satisfies data == convert(Array, t). This might not be possible, if the data does not respect the symmetry structure. This procedure can be sketched using a simple physical example, namely the SWAP gate on two qubits,

    \[\begin{align*} \mathrm{SWAP}: \mathbb{C}^2 \otimes \mathbb{C}^2 & \to \mathbb{C}^2 \otimes \mathbb{C}^2\\ |i\rangle \otimes |j\rangle &\mapsto |j\rangle \otimes |i\rangle. \end{align*}\]

    This operator can be rewritten in terms of the familiar Heisenberg exchange interaction $\vec{S}_i \cdot \vec{S}_j$ as

    \[\mathrm{SWAP} = 2 \vec{S}_i \cdot \vec{S}_j + \frac{1}{2} 𝟙,\]

    where $\vec{S} = (S^x, S^y, S^z)$ and the spin-1/2 generators of SU₂ $S^k$ are defined defined in terms of the $2 \times 2$ Pauli matrices $\sigma^k$ as $S^k = \frac{1}{2}\sigma^k$. The SWAP gate can be realized as a rank-4 TensorMap in the following way:

    julia> # encode the matrix elements of the swap gate into a rank-4 array, where the first two
    @@ -149,192 +149,192 @@
      1.0

    Constructing similar tensors

    A third way to construct a TensorMap instance is to use Base.similar, i.e.

    similar(t [, T::Type{<:Number}, codomain, domain])

    where T is a possibly different eltype for the tensor data, and codomain and domain optionally define a new codomain and domain for the resulting tensor. By default, these values just take the value from the input tensor t. The result will be a new TensorMap instance, with undef data, but whose data is stored in the same subtype of DenseMatrix (e.g. Matrix or CuMatrix or ...) as t. In particular, this uses the methods storagetype(t) and TensorKit.similarstoragetype(t, T).

    Special purpose constructors

    Finally, there are methods zero, one, id, isomorphism, unitary and isometry to create specific new tensors. Tensor maps behave as vectors and can be added (if they have the same domain and codomain); zero(t) is the additive identity, i.e. a TensorMap instance where all entries are zero. For a t::TensorMap with domain(t) == codomain(t), i.e. an endomorphism, one(t) creates the identity tensor, i.e. the identity under composition. As discussed in the section on linear algebra operations, we denote composition of tensor maps with the mutliplication operator *, such that one(t) is the multiplicative identity. Similarly, it can be created as id(V) with V the relevant vector space, e.g. one(t) == id(domain(t)). The identity tensor is currently represented with dense data, and one can use id(A::Type{<:DenseMatrix}, V) to specify the type of DenseMatrix (and its eltype), e.g. A = Matrix{Float64}. Finally, it often occurs that we want to construct a specific isomorphism between two spaces that are isomorphic but not equal, and for which there is no canonical choice. Hereto, one can use the method u = isomorphism([A::Type{<:DenseMatrix}, ] codomain, domain), which will explicitly check that the domain and codomain are isomorphic, and return an error otherwise. Again, an optional first argument can be given to specify the specific type of DenseMatrix that is currently used to store the rather trivial data of this tensor. If InnerProductStyle(u) <: EuclideanProduct, the same result can be obtained with the method u = unitary([A::Type{<:DenseMatrix}, ] codomain, domain). Note that reversing the domain and codomain yields the inverse morphism, which in the case of EuclideanProduct coincides with the adjoint morphism, i.e. isomorphism(A, domain, codomain) == adjoint(u) == inv(u), where inv and adjoint will be further discussed below. Finally, if two spaces V1 and V2 are such that V2 can be embedded in V1, i.e. there exists an inclusion with a left inverse, and furthermore they represent tensor products of some ElementarySpace with EuclideanProduct, the function w = isometry([A::Type{<:DenseMatrix}, ], V1, V2) creates one specific isometric embedding, such that adjoint(w) * w == id(V2) and w * adjoint(w) is some hermitian idempotent (a.k.a. orthogonal projector) acting on V1. An error will be thrown if such a map cannot be constructed for the given domain and codomain.

    Let's conclude this section with some examples with GradedSpace.

    julia> V1 = ℤ₂Space(0=>3,1=>2)Rep[ℤ₂](0=>3, 1=>2)
    julia> V2 = ℤ₂Space(0=>2,1=>1) # First a `TensorMap{ℤ₂Space, 1, 1}`Rep[ℤ₂](0=>2, 1=>1)
    julia> m = randn(V1, V2)TensorMap(Rep[ℤ₂](0=>3, 1=>2) ← Rep[ℤ₂](0=>2, 1=>1)): * Data for sector (Irrep[ℤ₂](0),) ← (Irrep[ℤ₂](0),): - 1.0175199915518995 -1.451294472545277 - -0.2762137255354417 -0.645673975473561 - 0.37962919260313266 -0.7539349151069522 + -2.1834455630678424 0.2074595694725369 + -0.24358206030969787 1.3192786740637574 + -0.6361288079605442 -0.8410580378570695 * Data for sector (Irrep[ℤ₂](1),) ← (Irrep[ℤ₂](1),): - 0.26686752923202456 - 0.12101289102316652
    julia> convert(Array, m) |> disp + 0.9510550299841047 + 0.866259387576514
    julia> convert(Array, m) |> disp # compare with:5×3 Array{Float64, 2}: - 1.017 -1.451 0.0 - -0.276 -0.645 0.0 - 0.379 -0.753 0.0 - 0.0 0.0 0.266 - 0.0 0.0 0.121
    julia> block(m, Irrep[ℤ₂](0)) |> disp3×2 Array{Float64, 2}: - 1.017 -1.451 - -0.276 -0.645 - 0.379 -0.753
    julia> block(m, Irrep[ℤ₂](1)) |> disp + -2.183 0.207 0.0 + -0.243 1.319 0.0 + -0.636 -0.841 0.0 + 0.0 0.0 0.951 + 0.0 0.0 0.866
    julia> block(m, Irrep[ℤ₂](0)) |> disp3×2 Array{Float64, 2}: + -2.183 0.207 + -0.243 1.319 + -0.636 -0.841
    julia> block(m, Irrep[ℤ₂](1)) |> disp # Now a `TensorMap{ℤ₂Space, 2, 2}`2×1 Array{Float64, 2}: - 0.266 - 0.121
    julia> t = randn(V1 ⊗ V1, V2 ⊗ V2')TensorMap((Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>3, 1=>2)) ← (Rep[ℤ₂](0=>2, 1=>1) ⊗ Rep[ℤ₂](0=>2, 1=>1)')): + 0.951 + 0.866
    julia> t = randn(V1 ⊗ V1, V2 ⊗ V2')TensorMap((Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>3, 1=>2)) ← (Rep[ℤ₂](0=>2, 1=>1) ⊗ Rep[ℤ₂](0=>2, 1=>1)')): * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](0)): [:, :, 1, 1] = - 0.47642917547952646 -1.5937866901414017 -1.7353620307993363 - 0.20422568931690926 -0.3254456572172515 1.1543583321367523 - -0.3452482246950102 1.3298578205230374 -0.4965042703603741 + -2.196722624330248 -1.118305977331029 1.9751476809517978 + -0.9583773767624397 1.6916024584546816 0.7917670235408625 + -0.33174577702232333 0.26652620713036046 -0.47457934737298313 [:, :, 2, 1] = - -0.5380780215646306 0.2788466487718183 -0.3059492224906382 - -0.7754723628756528 -1.4447838387155472 -0.6843786997205582 - 0.9344786792882871 -0.3437518389300903 -0.3409351786731236 + 0.28764529726836924 -0.0737615986033633 -0.05902190404700212 + 0.2751068728418741 -0.25197321934093075 0.7017985563008357 + -0.07533621266267372 0.015505395914957733 -1.0009790733104245 [:, :, 1, 2] = - -0.7741995429551336 -0.6147157879420232 0.01943564891945509 - 1.3391749704049298 -0.447700710514759 0.6885432055920869 - -2.13121043128045 2.461819518187779 0.1318614178493565 + 0.08642772267454905 0.5079887905142385 0.17687542747926133 + 0.326121308571456 0.18021119869536442 -0.8115270158604198 + -0.9316781394857235 0.1622913614861214 0.8186145921666771 [:, :, 2, 2] = - 2.219893140555037 0.5100302335773275 0.020879529949220398 - 0.4246011949869516 -1.5044463459495199 -0.5634327756086613 - -0.042723165921663975 -0.3423305241619067 -0.8092180163606906 + 0.15541357424744262 1.7854502263328633 0.955318944633893 + 0.8021531950295123 -1.5927553282613016 0.13400297880146142 + 0.6493612729217777 -1.1250367267201402 0.622916533520488 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](0)): [:, :, 1, 1] = - 0.7909704726775071 -0.0686211569316282 - -0.6068996028061453 0.824786936686394 + 0.17243169246285917 -0.03827978903286681 + -0.11651097552880406 -0.41458726338777 [:, :, 2, 1] = - 1.4333710611747699 -1.271698140199787 - -0.7689384370996829 0.49453357243016755 + 0.7465930075600998 -1.1684733228624535 + 0.8606348818559589 -0.9281101203158333 [:, :, 1, 2] = - -0.8400510368591145 -1.6440089560430724 - 1.0599268582569146 0.35006787484228596 + 1.0449844428383521 1.4517753385429217 + 0.25267519439736313 1.9060395941702735 [:, :, 2, 2] = - 0.28801140447395 -0.0216839077343253 - -0.5660036454865778 0.3115149261241262 + 0.7829949986032156 0.9178573946200062 + -0.42907482161949995 -0.3857372111457989 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](1)): [:, :, 1, 1] = - 1.0438260682528258 0.6657848758277389 -0.7392584757462528 - -0.17363077182293524 -0.40527226968797003 -1.5411501179642733 - 0.11235294381739291 0.22294118656231673 0.8932955067250862 + -0.5221155982909581 0.31427662007203533 -1.1165987473052477 + 0.20534862305373391 0.768462132554422 1.1219878839636697 + -0.12741758087149457 -0.22405460890980855 -0.15342486982914738 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](1)): [:, :, 1, 1] = - -0.9916325082904474 -0.3089420025527419 - -0.8654486260576052 0.3740626516308682 + 0.6330603609827764 0.42664570813558245 + -1.726637679315727 0.5534314116378043 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](0)): [:, :, 1, 1] = - -1.5848600474198913 -0.5447117310734331 -0.9996827186672663 - -0.14802969162776164 -0.5005136272475688 -1.276355730956557 + 0.8851118851218334 0.08907908147851883 -0.17002976803984274 + 1.1945171739305063 0.522845401840556 -0.39265418377568995 [:, :, 1, 2] = - -0.7244543283016769 0.4131576411281648 -0.33306229374716095 - -0.40386175522042517 0.38766905019638637 0.975782706703984 + 0.7209943388082749 -0.6468816386254471 1.712619777553115 + 0.6588641183026648 0.23936400240128078 -0.7660604325191912 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](0)): [:, :, 1, 1] = - 0.7298545980287051 0.029364888116724622 - 0.398467332162959 -0.5843276355184462 - -0.305674576747131 2.173469936344023 + 1.8982007340018956 -0.6260159124114157 + -0.18169653897265736 0.2401643137388516 + -0.8255380372362732 -0.08958452955558203 [:, :, 1, 2] = - -0.4270409639544958 0.645478086750599 - 1.3258291935961122 0.9061090920420138 - -0.16932567532985138 -0.20544315650225015 + -0.4771409005438688 -0.891989818669103 + -0.1732180004956976 0.8157905616770785 + 0.04707913015450214 0.5182023996270906 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](1)): [:, :, 1, 1] = - -1.45378129590886 0.912254363479144 -0.11746552405368423 - -1.3038458770684522 -0.5436308981004667 -1.2049509546702564 + -0.6685799472608192 -0.8418375906160998 0.09807713328879489 + -0.5686712614073792 0.439697432869418 2.3288701888039167 [:, :, 2, 1] = - 0.8947397460282026 0.708541210051412 0.14892183570680698 - -1.0020335844550454 0.5632763805138592 1.7286854491084465 + -0.4239701218091856 0.9381649772360481 1.651999515895216 + 0.47809724711312673 0.785999745487966 -0.2037298944815304 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](1)): [:, :, 1, 1] = - 0.30391041182854717 -0.06804152870701238 - -0.5487249906279134 -1.2155202822981084 - 0.8783165888188127 0.926969416441396 + 0.4566877460974457 -0.48442367332963954 + 0.3078972165968684 -0.1576570956335884 + -0.06867009643228217 0.26109485636300644 [:, :, 2, 1] = - -0.26352142888660285 2.212163566090167 - -1.256049103219956 -0.42836464479264735 - 0.19735000168369796 0.8963658509915429
    julia> (array = convert(Array, t)) |> disp5×5×3×3 Array{Float64, 4}: + 0.44100169131296285 2.9096300644111235 + 0.11640618862145431 0.8267332533538535 + -0.46538713202383925 -0.2985914638076027
    julia> (array = convert(Array, t)) |> disp5×5×3×3 Array{Float64, 4}: [:, :, 1, 1] = - 0.476 -1.593 -1.735 0.0 0.0 - 0.204 -0.325 1.154 0.0 0.0 - -0.345 1.329 -0.496 0.0 0.0 - 0.0 0.0 0.0 0.79 -0.068 - 0.0 0.0 0.0 -0.606 0.824 + -2.196 -1.118 1.975 0.0 0.0 + -0.958 1.691 0.791 0.0 0.0 + -0.331 0.266 -0.474 0.0 0.0 + 0.0 0.0 0.0 0.172 -0.038 + 0.0 0.0 0.0 -0.116 -0.414 [:, :, 2, 1] = - -0.538 0.278 -0.305 0.0 0.0 - -0.775 -1.444 -0.684 0.0 0.0 - 0.934 -0.343 -0.34 0.0 0.0 - 0.0 0.0 0.0 1.433 -1.271 - 0.0 0.0 0.0 -0.768 0.494 + 0.287 -0.073 -0.059 0.0 0.0 + 0.275 -0.251 0.701 0.0 0.0 + -0.075 0.015 -1.0 0.0 0.0 + 0.0 0.0 0.0 0.746 -1.168 + 0.0 0.0 0.0 0.86 -0.928 [:, :, 3, 1] = - 0.0 0.0 0.0 0.729 0.029 - 0.0 0.0 0.0 0.398 -0.584 - 0.0 0.0 0.0 -0.305 2.173 - -1.584 -0.544 -0.999 0.0 0.0 - -0.148 -0.5 -1.276 0.0 0.0 + 0.0 0.0 0.0 1.898 -0.626 + 0.0 0.0 0.0 -0.181 0.24 + 0.0 0.0 0.0 -0.825 -0.089 + 0.885 0.089 -0.17 0.0 0.0 + 1.194 0.522 -0.392 0.0 0.0 [:, :, 1, 2] = - -0.774 -0.614 0.019 0.0 0.0 - 1.339 -0.447 0.688 0.0 0.0 - -2.131 2.461 0.131 0.0 0.0 - 0.0 0.0 0.0 -0.84 -1.644 - 0.0 0.0 0.0 1.059 0.35 + 0.086 0.507 0.176 0.0 0.0 + 0.326 0.18 -0.811 0.0 0.0 + -0.931 0.162 0.818 0.0 0.0 + 0.0 0.0 0.0 1.044 1.451 + 0.0 0.0 0.0 0.252 1.906 [:, :, 2, 2] = - 2.219 0.51 0.02 0.0 0.0 - 0.424 -1.504 -0.563 0.0 0.0 - -0.042 -0.342 -0.809 0.0 0.0 - 0.0 0.0 0.0 0.288 -0.021 - 0.0 0.0 0.0 -0.566 0.311 + 0.155 1.785 0.955 0.0 0.0 + 0.802 -1.592 0.134 0.0 0.0 + 0.649 -1.125 0.622 0.0 0.0 + 0.0 0.0 0.0 0.782 0.917 + 0.0 0.0 0.0 -0.429 -0.385 [:, :, 3, 2] = - 0.0 0.0 0.0 -0.427 0.645 - 0.0 0.0 0.0 1.325 0.906 - 0.0 0.0 0.0 -0.169 -0.205 - -0.724 0.413 -0.333 0.0 0.0 - -0.403 0.387 0.975 0.0 0.0 + 0.0 0.0 0.0 -0.477 -0.891 + 0.0 0.0 0.0 -0.173 0.815 + 0.0 0.0 0.0 0.047 0.518 + 0.72 -0.646 1.712 0.0 0.0 + 0.658 0.239 -0.766 0.0 0.0 [:, :, 1, 3] = - 0.0 0.0 0.0 0.303 -0.068 - 0.0 0.0 0.0 -0.548 -1.215 - 0.0 0.0 0.0 0.878 0.926 - -1.453 0.912 -0.117 0.0 0.0 - -1.303 -0.543 -1.204 0.0 0.0 + 0.0 0.0 0.0 0.456 -0.484 + 0.0 0.0 0.0 0.307 -0.157 + 0.0 0.0 0.0 -0.068 0.261 + -0.668 -0.841 0.098 0.0 0.0 + -0.568 0.439 2.328 0.0 0.0 [:, :, 2, 3] = - 0.0 0.0 0.0 -0.263 2.212 - 0.0 0.0 0.0 -1.256 -0.428 - 0.0 0.0 0.0 0.197 0.896 - 0.894 0.708 0.148 0.0 0.0 - -1.002 0.563 1.728 0.0 0.0 + 0.0 0.0 0.0 0.441 2.909 + 0.0 0.0 0.0 0.116 0.826 + 0.0 0.0 0.0 -0.465 -0.298 + -0.423 0.938 1.651 0.0 0.0 + 0.478 0.785 -0.203 0.0 0.0 [:, :, 3, 3] = - 1.043 0.665 -0.739 0.0 0.0 - -0.173 -0.405 -1.541 0.0 0.0 - 0.112 0.222 0.893 0.0 0.0 - 0.0 0.0 0.0 -0.991 -0.308 - 0.0 0.0 0.0 -0.865 0.374
    julia> d1 = dim(codomain(t))25
    julia> d2 = dim(domain(t))9
    julia> (matrix = reshape(array, d1, d2)) |> disp25×9 Array{Float64, 2}: - 0.476 -0.538 0.0 -0.774 2.219 0.0 0.0 0.0 1.043 - 0.204 -0.775 0.0 1.339 0.424 0.0 0.0 0.0 -0.173 - -0.345 0.934 0.0 -2.131 -0.042 0.0 0.0 0.0 0.112 - 0.0 0.0 -1.584 0.0 0.0 -0.724 -1.453 0.894 0.0 - 0.0 0.0 -0.148 0.0 0.0 -0.403 -1.303 -1.002 0.0 - -1.593 0.278 0.0 -0.614 0.51 0.0 0.0 0.0 0.665 - -0.325 -1.444 0.0 -0.447 -1.504 0.0 0.0 0.0 -0.405 - 1.329 -0.343 0.0 2.461 -0.342 0.0 0.0 0.0 0.222 - 0.0 0.0 -0.544 0.0 0.0 0.413 0.912 0.708 0.0 - 0.0 0.0 -0.5 0.0 0.0 0.387 -0.543 0.563 0.0 - -1.735 -0.305 0.0 0.019 0.02 0.0 0.0 0.0 -0.739 - 1.154 -0.684 0.0 0.688 -0.563 0.0 0.0 0.0 -1.541 - -0.496 -0.34 0.0 0.131 -0.809 0.0 0.0 0.0 0.893 - 0.0 0.0 -0.999 0.0 0.0 -0.333 -0.117 0.148 0.0 - 0.0 0.0 -1.276 0.0 0.0 0.975 -1.204 1.728 0.0 - 0.0 0.0 0.729 0.0 0.0 -0.427 0.303 -0.263 0.0 - 0.0 0.0 0.398 0.0 0.0 1.325 -0.548 -1.256 0.0 - 0.0 0.0 -0.305 0.0 0.0 -0.169 0.878 0.197 0.0 - 0.79 1.433 0.0 -0.84 0.288 0.0 0.0 0.0 -0.991 - -0.606 -0.768 0.0 1.059 -0.566 0.0 0.0 0.0 -0.865 - 0.0 0.0 0.029 0.0 0.0 0.645 -0.068 2.212 0.0 - 0.0 0.0 -0.584 0.0 0.0 0.906 -1.215 -0.428 0.0 - 0.0 0.0 2.173 0.0 0.0 -0.205 0.926 0.896 0.0 - -0.068 -1.271 0.0 -1.644 -0.021 0.0 0.0 0.0 -0.308 - 0.824 0.494 0.0 0.35 0.311 0.0 0.0 0.0 0.374
    julia> (u = reshape(convert(Array, unitary(codomain(t), fuse(codomain(t)))), d1, d1)) |> disp25×25 Array{Float64, 2}: + -0.522 0.314 -1.116 0.0 0.0 + 0.205 0.768 1.121 0.0 0.0 + -0.127 -0.224 -0.153 0.0 0.0 + 0.0 0.0 0.0 0.633 0.426 + 0.0 0.0 0.0 -1.726 0.553
    julia> d1 = dim(codomain(t))25
    julia> d2 = dim(domain(t))9
    julia> (matrix = reshape(array, d1, d2)) |> disp25×9 Array{Float64, 2}: + -2.196 0.287 0.0 0.086 0.155 0.0 0.0 0.0 -0.522 + -0.958 0.275 0.0 0.326 0.802 0.0 0.0 0.0 0.205 + -0.331 -0.075 0.0 -0.931 0.649 0.0 0.0 0.0 -0.127 + 0.0 0.0 0.885 0.0 0.0 0.72 -0.668 -0.423 0.0 + 0.0 0.0 1.194 0.0 0.0 0.658 -0.568 0.478 0.0 + -1.118 -0.073 0.0 0.507 1.785 0.0 0.0 0.0 0.314 + 1.691 -0.251 0.0 0.18 -1.592 0.0 0.0 0.0 0.768 + 0.266 0.015 0.0 0.162 -1.125 0.0 0.0 0.0 -0.224 + 0.0 0.0 0.089 0.0 0.0 -0.646 -0.841 0.938 0.0 + 0.0 0.0 0.522 0.0 0.0 0.239 0.439 0.785 0.0 + 1.975 -0.059 0.0 0.176 0.955 0.0 0.0 0.0 -1.116 + 0.791 0.701 0.0 -0.811 0.134 0.0 0.0 0.0 1.121 + -0.474 -1.0 0.0 0.818 0.622 0.0 0.0 0.0 -0.153 + 0.0 0.0 -0.17 0.0 0.0 1.712 0.098 1.651 0.0 + 0.0 0.0 -0.392 0.0 0.0 -0.766 2.328 -0.203 0.0 + 0.0 0.0 1.898 0.0 0.0 -0.477 0.456 0.441 0.0 + 0.0 0.0 -0.181 0.0 0.0 -0.173 0.307 0.116 0.0 + 0.0 0.0 -0.825 0.0 0.0 0.047 -0.068 -0.465 0.0 + 0.172 0.746 0.0 1.044 0.782 0.0 0.0 0.0 0.633 + -0.116 0.86 0.0 0.252 -0.429 0.0 0.0 0.0 -1.726 + 0.0 0.0 -0.626 0.0 0.0 -0.891 -0.484 2.909 0.0 + 0.0 0.0 0.24 0.0 0.0 0.815 -0.157 0.826 0.0 + 0.0 0.0 -0.089 0.0 0.0 0.518 0.261 -0.298 0.0 + -0.038 -1.168 0.0 1.451 0.917 0.0 0.0 0.0 0.426 + -0.414 -0.928 0.0 1.906 -0.385 0.0 0.0 0.0 0.553
    julia> (u = reshape(convert(Array, unitary(codomain(t), fuse(codomain(t)))), d1, d1)) |> disp25×25 Array{Float64, 2}: 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 @@ -370,255 +370,255 @@ 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0
    julia> u'*u ≈ I ≈ v'*vtrue
    julia> (u'*matrix*v) |> disp # compare with:25×9 Array{Float64, 2}: - 0.476 -0.538 -0.774 2.219 1.043 0.0 0.0 0.0 0.0 - 0.204 -0.775 1.339 0.424 -0.173 0.0 0.0 0.0 0.0 - -0.345 0.934 -2.131 -0.042 0.112 0.0 0.0 0.0 0.0 - -1.593 0.278 -0.614 0.51 0.665 0.0 0.0 0.0 0.0 - -0.325 -1.444 -0.447 -1.504 -0.405 0.0 0.0 0.0 0.0 - 1.329 -0.343 2.461 -0.342 0.222 0.0 0.0 0.0 0.0 - -1.735 -0.305 0.019 0.02 -0.739 0.0 0.0 0.0 0.0 - 1.154 -0.684 0.688 -0.563 -1.541 0.0 0.0 0.0 0.0 - -0.496 -0.34 0.131 -0.809 0.893 0.0 0.0 0.0 0.0 - 0.79 1.433 -0.84 0.288 -0.991 0.0 0.0 0.0 0.0 - -0.606 -0.768 1.059 -0.566 -0.865 0.0 0.0 0.0 0.0 - -0.068 -1.271 -1.644 -0.021 -0.308 0.0 0.0 0.0 0.0 - 0.824 0.494 0.35 0.311 0.374 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 -1.584 -0.724 -1.453 0.894 - 0.0 0.0 0.0 0.0 0.0 -0.148 -0.403 -1.303 -1.002 - 0.0 0.0 0.0 0.0 0.0 -0.544 0.413 0.912 0.708 - 0.0 0.0 0.0 0.0 0.0 -0.5 0.387 -0.543 0.563 - 0.0 0.0 0.0 0.0 0.0 -0.999 -0.333 -0.117 0.148 - 0.0 0.0 0.0 0.0 0.0 -1.276 0.975 -1.204 1.728 - 0.0 0.0 0.0 0.0 0.0 0.729 -0.427 0.303 -0.263 - 0.0 0.0 0.0 0.0 0.0 0.398 1.325 -0.548 -1.256 - 0.0 0.0 0.0 0.0 0.0 -0.305 -0.169 0.878 0.197 - 0.0 0.0 0.0 0.0 0.0 0.029 0.645 -0.068 2.212 - 0.0 0.0 0.0 0.0 0.0 -0.584 0.906 -1.215 -0.428 - 0.0 0.0 0.0 0.0 0.0 2.173 -0.205 0.926 0.896
    julia> block(t, Z2Irrep(0)) |> disp13×5 Array{Float64, 2}: - 0.476 -0.538 -0.774 2.219 1.043 - 0.204 -0.775 1.339 0.424 -0.173 - -0.345 0.934 -2.131 -0.042 0.112 - -1.593 0.278 -0.614 0.51 0.665 - -0.325 -1.444 -0.447 -1.504 -0.405 - 1.329 -0.343 2.461 -0.342 0.222 - -1.735 -0.305 0.019 0.02 -0.739 - 1.154 -0.684 0.688 -0.563 -1.541 - -0.496 -0.34 0.131 -0.809 0.893 - 0.79 1.433 -0.84 0.288 -0.991 - -0.606 -0.768 1.059 -0.566 -0.865 - -0.068 -1.271 -1.644 -0.021 -0.308 - 0.824 0.494 0.35 0.311 0.374
    julia> block(t, Z2Irrep(1)) |> disp12×4 Array{Float64, 2}: - -1.584 -0.724 -1.453 0.894 - -0.148 -0.403 -1.303 -1.002 - -0.544 0.413 0.912 0.708 - -0.5 0.387 -0.543 0.563 - -0.999 -0.333 -0.117 0.148 - -1.276 0.975 -1.204 1.728 - 0.729 -0.427 0.303 -0.263 - 0.398 1.325 -0.548 -1.256 - -0.305 -0.169 0.878 0.197 - 0.029 0.645 -0.068 2.212 - -0.584 0.906 -1.215 -0.428 - 2.173 -0.205 0.926 0.896

    Here, we illustrated some additional concepts. Firstly, note that we convert a TensorMap to an Array. This only works when sectortype(t) supports fusiontensor, and in particular when BraidingStyle(sectortype(t)) == Bosonic(), e.g. the case of trivial tensors (the category $\mathbf{Vect}$) and group representations (the category $\mathbf{Rep}_{\mathsf{G}}$, which can be interpreted as a subcategory of $\mathbf{Vect}$). Here, we are in this case with $\mathsf{G} = ℤ₂$. For a TensorMap{S,1,1}, the blocks directly correspond to the diagonal blocks in the block diagonal structure of its representation as an Array, there is no basis transform in between. This is no longer the case for TensorMap{S,N₁,N₂} with different values of N₁ and N₂. Here, we use the operation fuse(V), which creates an ElementarySpace which is isomorphic to a given space V (of type ProductSpace or ElementarySpace). The specific map between those two spaces constructed using the specific method unitary implements precisely the basis change from the product basis to the coupled basis. In this case, for a group G with FusionStyle(Irrep[G]) isa UniqueFusion, it is a permutation matrix. Specifically choosing V equal to the codomain and domain of t, we can construct the explicit basis transforms that bring t into block diagonal form.

    Let's repeat the same exercise for I = Irrep[SU₂], which has FusionStyle(I) isa MultipleFusion.

    julia> V1 = SU₂Space(0=>2,1=>1)Rep[SU₂](0=>2, 1=>1)
    julia> V2 = SU₂Space(0=>1,1=>1) + -2.196 0.287 0.086 0.155 -0.522 0.0 0.0 0.0 0.0 + -0.958 0.275 0.326 0.802 0.205 0.0 0.0 0.0 0.0 + -0.331 -0.075 -0.931 0.649 -0.127 0.0 0.0 0.0 0.0 + -1.118 -0.073 0.507 1.785 0.314 0.0 0.0 0.0 0.0 + 1.691 -0.251 0.18 -1.592 0.768 0.0 0.0 0.0 0.0 + 0.266 0.015 0.162 -1.125 -0.224 0.0 0.0 0.0 0.0 + 1.975 -0.059 0.176 0.955 -1.116 0.0 0.0 0.0 0.0 + 0.791 0.701 -0.811 0.134 1.121 0.0 0.0 0.0 0.0 + -0.474 -1.0 0.818 0.622 -0.153 0.0 0.0 0.0 0.0 + 0.172 0.746 1.044 0.782 0.633 0.0 0.0 0.0 0.0 + -0.116 0.86 0.252 -0.429 -1.726 0.0 0.0 0.0 0.0 + -0.038 -1.168 1.451 0.917 0.426 0.0 0.0 0.0 0.0 + -0.414 -0.928 1.906 -0.385 0.553 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.885 0.72 -0.668 -0.423 + 0.0 0.0 0.0 0.0 0.0 1.194 0.658 -0.568 0.478 + 0.0 0.0 0.0 0.0 0.0 0.089 -0.646 -0.841 0.938 + 0.0 0.0 0.0 0.0 0.0 0.522 0.239 0.439 0.785 + 0.0 0.0 0.0 0.0 0.0 -0.17 1.712 0.098 1.651 + 0.0 0.0 0.0 0.0 0.0 -0.392 -0.766 2.328 -0.203 + 0.0 0.0 0.0 0.0 0.0 1.898 -0.477 0.456 0.441 + 0.0 0.0 0.0 0.0 0.0 -0.181 -0.173 0.307 0.116 + 0.0 0.0 0.0 0.0 0.0 -0.825 0.047 -0.068 -0.465 + 0.0 0.0 0.0 0.0 0.0 -0.626 -0.891 -0.484 2.909 + 0.0 0.0 0.0 0.0 0.0 0.24 0.815 -0.157 0.826 + 0.0 0.0 0.0 0.0 0.0 -0.089 0.518 0.261 -0.298
    julia> block(t, Z2Irrep(0)) |> disp13×5 Array{Float64, 2}: + -2.196 0.287 0.086 0.155 -0.522 + -0.958 0.275 0.326 0.802 0.205 + -0.331 -0.075 -0.931 0.649 -0.127 + -1.118 -0.073 0.507 1.785 0.314 + 1.691 -0.251 0.18 -1.592 0.768 + 0.266 0.015 0.162 -1.125 -0.224 + 1.975 -0.059 0.176 0.955 -1.116 + 0.791 0.701 -0.811 0.134 1.121 + -0.474 -1.0 0.818 0.622 -0.153 + 0.172 0.746 1.044 0.782 0.633 + -0.116 0.86 0.252 -0.429 -1.726 + -0.038 -1.168 1.451 0.917 0.426 + -0.414 -0.928 1.906 -0.385 0.553
    julia> block(t, Z2Irrep(1)) |> disp12×4 Array{Float64, 2}: + 0.885 0.72 -0.668 -0.423 + 1.194 0.658 -0.568 0.478 + 0.089 -0.646 -0.841 0.938 + 0.522 0.239 0.439 0.785 + -0.17 1.712 0.098 1.651 + -0.392 -0.766 2.328 -0.203 + 1.898 -0.477 0.456 0.441 + -0.181 -0.173 0.307 0.116 + -0.825 0.047 -0.068 -0.465 + -0.626 -0.891 -0.484 2.909 + 0.24 0.815 -0.157 0.826 + -0.089 0.518 0.261 -0.298

    Here, we illustrated some additional concepts. Firstly, note that we convert a TensorMap to an Array. This only works when sectortype(t) supports fusiontensor, and in particular when BraidingStyle(sectortype(t)) == Bosonic(), e.g. the case of trivial tensors (the category $\mathbf{Vect}$) and group representations (the category $\mathbf{Rep}_{\mathsf{G}}$, which can be interpreted as a subcategory of $\mathbf{Vect}$). Here, we are in this case with $\mathsf{G} = ℤ₂$. For a TensorMap{S,1,1}, the blocks directly correspond to the diagonal blocks in the block diagonal structure of its representation as an Array, there is no basis transform in between. This is no longer the case for TensorMap{S,N₁,N₂} with different values of N₁ and N₂. Here, we use the operation fuse(V), which creates an ElementarySpace which is isomorphic to a given space V (of type ProductSpace or ElementarySpace). The specific map between those two spaces constructed using the specific method unitary implements precisely the basis change from the product basis to the coupled basis. In this case, for a group G with FusionStyle(Irrep[G]) isa UniqueFusion, it is a permutation matrix. Specifically choosing V equal to the codomain and domain of t, we can construct the explicit basis transforms that bring t into block diagonal form.

    Let's repeat the same exercise for I = Irrep[SU₂], which has FusionStyle(I) isa MultipleFusion.

    julia> V1 = SU₂Space(0=>2,1=>1)Rep[SU₂](0=>2, 1=>1)
    julia> V2 = SU₂Space(0=>1,1=>1) # First a `TensorMap{SU₂Space, 1, 1}`Rep[SU₂](0=>1, 1=>1)
    julia> m = randn(V1, V2)TensorMap(Rep[SU₂](0=>2, 1=>1) ← Rep[SU₂](0=>1, 1=>1)): * Data for fusiontree FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()): - -0.9518578276175027 - 1.1879064344331187 + -1.0527682492930184 + 0.5257771429691075 * Data for fusiontree FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()): - -0.5992670495872038
    julia> convert(Array, m) |> disp + -0.9308507478607294
    julia> convert(Array, m) |> disp # compare with:5×4 Array{Float64, 2}: - -0.951 0.0 0.0 0.0 - 1.187 0.0 0.0 0.0 - 0.0 -0.599 0.0 0.0 - 0.0 0.0 -0.599 0.0 - 0.0 0.0 0.0 -0.599
    julia> block(m, Irrep[SU₂](0)) |> disp2×1 Array{Float64, 2}: - -0.951 - 1.187
    julia> block(m, Irrep[SU₂](1)) |> disp + -1.052 0.0 0.0 0.0 + 0.525 0.0 0.0 0.0 + 0.0 -0.93 0.0 0.0 + 0.0 0.0 -0.93 0.0 + 0.0 0.0 0.0 -0.93
    julia> block(m, Irrep[SU₂](0)) |> disp2×1 Array{Float64, 2}: + -1.052 + 0.525
    julia> block(m, Irrep[SU₂](1)) |> disp # Now a `TensorMap{SU₂Space, 2, 2}`1×1 Array{Float64, 2}: - -0.599
    julia> t = randn(V1 ⊗ V1, V2 ⊗ V2')TensorMap((Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>2, 1=>1)) ← (Rep[SU₂](0=>1, 1=>1) ⊗ Rep[SU₂](0=>1, 1=>1)')): + -0.93
    julia> t = randn(V1 ⊗ V1, V2 ⊗ V2')TensorMap((Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>2, 1=>1)) ← (Rep[SU₂](0=>1, 1=>1) ⊗ Rep[SU₂](0=>1, 1=>1)')): * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 0), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0, 0), 0, (false, true), ()): [:, :, 1, 1] = - -0.7164601000777033 0.8402881670666787 - -1.4645668185619172 -0.03284094745363703 + -0.14142885082251372 -0.22265024345214926 + -0.10641507475143616 0.4144839187928769 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0, 0), 0, (false, true), ()): [:, :, 1, 1] = - 0.5221319556303144 + 0.3745894707185757 * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 0), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 0, (false, true), ()): [:, :, 1, 1] = - 0.7129636135872823 -0.028566834076525237 - -0.7411796189245599 0.8673471875980863 + -0.9502777270353865 0.4676512412925039 + 0.3692804864747098 2.091142714812541 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 0, (false, true), ()): [:, :, 1, 1] = - 0.2937111218523841 + 0.8333536386959417 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 0), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 0), 1, (false, true), ()): [:, :, 1, 1] = - -1.7868657048152816 0.15466876990842407 + -0.3284658974354606 0.5925056681953469 * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 0), 1, (false, true), ()): [:, :, 1, 1] = - -1.1981471013862421 - 1.2257355729332762 + -0.8651946080202491 + -0.05091483888723515 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 0), 1, (false, true), ()): [:, :, 1, 1] = - -0.4166416461033738 + -1.261244334418641 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 0), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0, 1), 1, (false, true), ()): [:, :, 1, 1] = - 0.868556965587991 -0.1500096447891483 + -0.5282302914719381 1.4446178291840646 * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0, 1), 1, (false, true), ()): [:, :, 1, 1] = - 0.9885294518048647 - 2.554370698368057 + 0.7321866306095242 + -0.439996319310446 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0, 1), 1, (false, true), ()): [:, :, 1, 1] = - 1.5458388338200124 + 1.4703776344935073 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 0), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 1, (false, true), ()): [:, :, 1, 1] = - -0.4657974434043403 1.0485837300449454 + 0.7953988648145892 -0.4587415424347604 * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 1, (false, true), ()): [:, :, 1, 1] = - 0.7572887531967569 - -0.22200786820735938 + 0.85895930189713 + 0.5353945130053149 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 1, (false, true), ()): [:, :, 1, 1] = - -2.151040091109563 + -2.047162563558052 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 2, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 2, (false, true), ()): [:, :, 1, 1] = - 1.5683963426572347
    julia> (array = convert(Array, t)) |> disp5×5×4×4 Array{Float64, 4}: + -1.4043563428009727
    julia> (array = convert(Array, t)) |> disp5×5×4×4 Array{Float64, 4}: [:, :, 1, 1] = - -0.716 0.84 0.0 0.0 0.0 - -1.464 -0.032 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.301 - 0.0 0.0 0.0 -0.301 0.0 - 0.0 0.0 0.301 0.0 0.0 + -0.141 -0.222 0.0 0.0 0.0 + -0.106 0.414 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.216 + 0.0 0.0 0.0 -0.216 0.0 + 0.0 0.0 0.216 0.0 0.0 [:, :, 2, 1] = - 0.0 0.0 -1.198 0.0 0.0 - 0.0 0.0 1.225 0.0 0.0 - -1.786 0.154 0.0 -0.294 0.0 - 0.0 0.0 0.294 0.0 0.0 + 0.0 0.0 -0.865 0.0 0.0 + 0.0 0.0 -0.05 0.0 0.0 + -0.328 0.592 0.0 -0.891 0.0 + 0.0 0.0 0.891 0.0 0.0 0.0 0.0 0.0 0.0 0.0 [:, :, 3, 1] = - 0.0 0.0 0.0 -1.198 0.0 - 0.0 0.0 0.0 1.225 0.0 - 0.0 0.0 0.0 0.0 -0.294 - -1.786 0.154 0.0 0.0 0.0 - 0.0 0.0 0.294 0.0 0.0 + 0.0 0.0 0.0 -0.865 0.0 + 0.0 0.0 0.0 -0.05 0.0 + 0.0 0.0 0.0 0.0 -0.891 + -0.328 0.592 0.0 0.0 0.0 + 0.0 0.0 0.891 0.0 0.0 [:, :, 4, 1] = - 0.0 0.0 0.0 0.0 -1.198 - 0.0 0.0 0.0 0.0 1.225 + 0.0 0.0 0.0 0.0 -0.865 + 0.0 0.0 0.0 0.0 -0.05 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 -0.294 - -1.786 0.154 0.0 0.294 0.0 + 0.0 0.0 0.0 0.0 -0.891 + -0.328 0.592 0.0 0.891 0.0 [:, :, 1, 2] = - 0.0 0.0 0.0 0.0 0.988 - 0.0 0.0 0.0 0.0 2.554 - 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 1.093 - 0.868 -0.15 0.0 -1.093 0.0 + 0.0 0.0 0.0 0.0 0.732 + 0.0 0.0 0.0 0.0 -0.439 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 1.039 + -0.528 1.444 0.0 -1.039 0.0 [:, :, 2, 2] = - 0.411 -0.016 0.0 0.535 0.0 - -0.427 0.5 0.0 -0.156 0.0 - 0.0 0.0 0.0 0.0 -0.716 - -0.329 0.741 0.0 0.424 0.0 - 0.0 0.0 1.434 0.0 0.0 + -0.548 0.269 0.0 0.607 0.0 + 0.213 1.207 0.0 0.378 0.0 + 0.0 0.0 0.0 0.0 -0.979 + 0.562 -0.324 0.0 -0.745 0.0 + 0.0 0.0 1.067 0.0 0.0 [:, :, 3, 2] = - 0.0 0.0 0.0 0.0 0.535 - 0.0 0.0 0.0 0.0 -0.156 - 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 -0.291 - -0.329 0.741 0.0 1.859 0.0 + 0.0 0.0 0.0 0.0 0.607 + 0.0 0.0 0.0 0.0 0.378 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 -1.725 + 0.562 -0.324 0.0 0.321 0.0 [:, :, 4, 2] = - 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 1.568 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 -1.404 [:, :, 1, 3] = - 0.0 0.0 0.0 -0.988 0.0 - 0.0 0.0 0.0 -2.554 0.0 - 0.0 0.0 0.0 0.0 -1.093 - -0.868 0.15 0.0 0.0 0.0 - 0.0 0.0 1.093 0.0 0.0 + 0.0 0.0 0.0 -0.732 0.0 + 0.0 0.0 0.0 0.439 0.0 + 0.0 0.0 0.0 0.0 -1.039 + 0.528 -1.444 0.0 0.0 0.0 + 0.0 0.0 1.039 0.0 0.0 [:, :, 2, 3] = - 0.0 0.0 -0.535 0.0 0.0 - 0.0 0.0 0.156 0.0 0.0 - 0.329 -0.741 0.0 0.291 0.0 - 0.0 0.0 -1.859 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 -0.607 0.0 0.0 + 0.0 0.0 -0.378 0.0 0.0 + -0.562 0.324 0.0 1.725 0.0 + 0.0 0.0 -0.321 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 [:, :, 3, 3] = - 0.411 -0.016 0.0 0.0 0.0 - -0.427 0.5 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 -0.424 - 0.0 0.0 0.0 -1.143 0.0 - 0.0 0.0 -0.424 0.0 0.0 + -0.548 0.269 0.0 0.0 0.0 + 0.213 1.207 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.745 + 0.0 0.0 0.0 0.658 0.0 + 0.0 0.0 0.745 0.0 0.0 [:, :, 4, 3] = - 0.0 0.0 0.0 0.0 0.535 - 0.0 0.0 0.0 0.0 -0.156 - 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 -1.859 - -0.329 0.741 0.0 0.291 0.0 + 0.0 0.0 0.0 0.0 0.607 + 0.0 0.0 0.0 0.0 0.378 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 -0.321 + 0.562 -0.324 0.0 1.725 0.0 [:, :, 1, 4] = - 0.0 0.0 0.988 0.0 0.0 - 0.0 0.0 2.554 0.0 0.0 - 0.868 -0.15 0.0 1.093 0.0 - 0.0 0.0 -1.093 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.732 0.0 0.0 + 0.0 0.0 -0.439 0.0 0.0 + -0.528 1.444 0.0 1.039 0.0 + 0.0 0.0 -1.039 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 [:, :, 2, 4] = - 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 1.568 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 -1.404 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 [:, :, 3, 4] = - 0.0 0.0 -0.535 0.0 0.0 - 0.0 0.0 0.156 0.0 0.0 - 0.329 -0.741 0.0 1.859 0.0 - 0.0 0.0 -0.291 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 -0.607 0.0 0.0 + 0.0 0.0 -0.378 0.0 0.0 + -0.562 0.324 0.0 0.321 0.0 + 0.0 0.0 -1.725 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 [:, :, 4, 4] = - 0.411 -0.016 0.0 -0.535 0.0 - -0.427 0.5 0.0 0.156 0.0 - 0.0 0.0 0.0 0.0 1.434 - 0.329 -0.741 0.0 0.424 0.0 - 0.0 0.0 -0.716 0.0 0.0
    julia> d1 = dim(codomain(t))25
    julia> d2 = dim(domain(t))16
    julia> (matrix = reshape(array, d1, d2)) |> disp25×16 Array{Float64, 2}: - -0.716 0.0 0.0 0.0 0.0 0.411 0.0 0.0 0.0 0.0 0.411 0.0 0.0 0.0 0.0 0.411 - -1.464 0.0 0.0 0.0 0.0 -0.427 0.0 0.0 0.0 0.0 -0.427 0.0 0.0 0.0 0.0 -0.427 - 0.0 -1.786 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.329 0.0 0.0 0.868 0.0 0.329 0.0 - 0.0 0.0 -1.786 0.0 0.0 -0.329 0.0 0.0 -0.868 0.0 0.0 0.0 0.0 0.0 0.0 0.329 - 0.0 0.0 0.0 -1.786 0.868 0.0 -0.329 0.0 0.0 0.0 0.0 -0.329 0.0 0.0 0.0 0.0 - 0.84 0.0 0.0 0.0 0.0 -0.016 0.0 0.0 0.0 0.0 -0.016 0.0 0.0 0.0 0.0 -0.016 - -0.032 0.0 0.0 0.0 0.0 0.5 0.0 0.0 0.0 0.0 0.5 0.0 0.0 0.0 0.0 0.5 - 0.0 0.154 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.741 0.0 0.0 -0.15 0.0 -0.741 0.0 - 0.0 0.0 0.154 0.0 0.0 0.741 0.0 0.0 0.15 0.0 0.0 0.0 0.0 0.0 0.0 -0.741 - 0.0 0.0 0.0 0.154 -0.15 0.0 0.741 0.0 0.0 0.0 0.0 0.741 0.0 0.0 0.0 0.0 - 0.0 -1.198 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.535 0.0 0.0 0.988 0.0 -0.535 0.0 - 0.0 1.225 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.156 0.0 0.0 2.554 0.0 0.156 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.568 0.0 0.0 - 0.0 0.294 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.859 0.0 0.0 -1.093 0.0 -0.291 0.0 - 0.301 0.0 0.294 0.0 0.0 1.434 0.0 0.0 1.093 0.0 -0.424 0.0 0.0 0.0 0.0 -0.716 - 0.0 0.0 -1.198 0.0 0.0 0.535 0.0 0.0 -0.988 0.0 0.0 0.0 0.0 0.0 0.0 -0.535 - 0.0 0.0 1.225 0.0 0.0 -0.156 0.0 0.0 -2.554 0.0 0.0 0.0 0.0 0.0 0.0 0.156 - 0.0 -0.294 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.291 0.0 0.0 1.093 0.0 1.859 0.0 - -0.301 0.0 0.0 0.0 0.0 0.424 0.0 0.0 0.0 0.0 -1.143 0.0 0.0 0.0 0.0 0.424 - 0.0 0.0 0.0 0.294 -1.093 0.0 1.859 0.0 0.0 0.0 0.0 0.291 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 -1.198 0.988 0.0 0.535 0.0 0.0 0.0 0.0 0.535 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 1.225 2.554 0.0 -0.156 0.0 0.0 0.0 0.0 -0.156 0.0 0.0 0.0 0.0 - 0.301 0.0 -0.294 0.0 0.0 -0.716 0.0 0.0 -1.093 0.0 -0.424 0.0 0.0 0.0 0.0 1.434 - 0.0 0.0 0.0 -0.294 1.093 0.0 -0.291 0.0 0.0 0.0 0.0 -1.859 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.568 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
    julia> (u = reshape(convert(Array, unitary(codomain(t), fuse(codomain(t)))), d1, d1)) |> disp25×25 Array{Float64, 2}: + -0.548 0.269 0.0 -0.607 0.0 + 0.213 1.207 0.0 -0.378 0.0 + 0.0 0.0 0.0 0.0 1.067 + -0.562 0.324 0.0 -0.745 0.0 + 0.0 0.0 -0.979 0.0 0.0
    julia> d1 = dim(codomain(t))25
    julia> d2 = dim(domain(t))16
    julia> (matrix = reshape(array, d1, d2)) |> disp25×16 Array{Float64, 2}: + -0.141 0.0 0.0 0.0 0.0 -0.548 0.0 0.0 0.0 0.0 -0.548 0.0 0.0 0.0 0.0 -0.548 + -0.106 0.0 0.0 0.0 0.0 0.213 0.0 0.0 0.0 0.0 0.213 0.0 0.0 0.0 0.0 0.213 + 0.0 -0.328 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.562 0.0 0.0 -0.528 0.0 -0.562 0.0 + 0.0 0.0 -0.328 0.0 0.0 0.562 0.0 0.0 0.528 0.0 0.0 0.0 0.0 0.0 0.0 -0.562 + 0.0 0.0 0.0 -0.328 -0.528 0.0 0.562 0.0 0.0 0.0 0.0 0.562 0.0 0.0 0.0 0.0 + -0.222 0.0 0.0 0.0 0.0 0.269 0.0 0.0 0.0 0.0 0.269 0.0 0.0 0.0 0.0 0.269 + 0.414 0.0 0.0 0.0 0.0 1.207 0.0 0.0 0.0 0.0 1.207 0.0 0.0 0.0 0.0 1.207 + 0.0 0.592 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.324 0.0 0.0 1.444 0.0 0.324 0.0 + 0.0 0.0 0.592 0.0 0.0 -0.324 0.0 0.0 -1.444 0.0 0.0 0.0 0.0 0.0 0.0 0.324 + 0.0 0.0 0.0 0.592 1.444 0.0 -0.324 0.0 0.0 0.0 0.0 -0.324 0.0 0.0 0.0 0.0 + 0.0 -0.865 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.607 0.0 0.0 0.732 0.0 -0.607 0.0 + 0.0 -0.05 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.378 0.0 0.0 -0.439 0.0 -0.378 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.404 0.0 0.0 + 0.0 0.891 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.321 0.0 0.0 -1.039 0.0 -1.725 0.0 + 0.216 0.0 0.891 0.0 0.0 1.067 0.0 0.0 1.039 0.0 0.745 0.0 0.0 0.0 0.0 -0.979 + 0.0 0.0 -0.865 0.0 0.0 0.607 0.0 0.0 -0.732 0.0 0.0 0.0 0.0 0.0 0.0 -0.607 + 0.0 0.0 -0.05 0.0 0.0 0.378 0.0 0.0 0.439 0.0 0.0 0.0 0.0 0.0 0.0 -0.378 + 0.0 -0.891 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.725 0.0 0.0 1.039 0.0 0.321 0.0 + -0.216 0.0 0.0 0.0 0.0 -0.745 0.0 0.0 0.0 0.0 0.658 0.0 0.0 0.0 0.0 -0.745 + 0.0 0.0 0.0 0.891 -1.039 0.0 0.321 0.0 0.0 0.0 0.0 1.725 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 -0.865 0.732 0.0 0.607 0.0 0.0 0.0 0.0 0.607 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 -0.05 -0.439 0.0 0.378 0.0 0.0 0.0 0.0 0.378 0.0 0.0 0.0 0.0 + 0.216 0.0 -0.891 0.0 0.0 -0.979 0.0 0.0 -1.039 0.0 0.745 0.0 0.0 0.0 0.0 1.067 + 0.0 0.0 0.0 -0.891 1.039 0.0 -1.725 0.0 0.0 0.0 0.0 -0.321 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.404 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
    julia> (u = reshape(convert(Array, unitary(codomain(t), fuse(codomain(t)))), d1, d1)) |> disp25×25 Array{Float64, 2}: 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 @@ -661,98 +661,98 @@ 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.707 0.0 0.0 0.0 0.707 0.0 0.0 0.0 0.0 0.577 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.707 0.0 0.0 0.0 0.408 0.0 0.0
    julia> u'*u ≈ I ≈ v'*vtrue
    julia> (u'*matrix*v) |> disp # compare with:25×16 Array{Float64, 2}: - -0.716 0.712 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 0.0 0.0 0.0 - -1.464 -0.741 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.84 -0.028 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 -0.0 0.0 0.0 - -0.032 0.867 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.522 0.293 0.0 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 -1.786 0.0 0.0 0.868 0.0 0.0 -0.465 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 -0.0 0.0 -1.786 0.0 0.0 0.868 0.0 0.0 -0.465 0.0 0.0 0.0 -0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 -1.786 0.0 0.0 0.868 0.0 0.0 -0.465 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.154 0.0 0.0 -0.15 0.0 0.0 1.048 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.154 0.0 0.0 -0.15 0.0 0.0 1.048 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.154 0.0 0.0 -0.15 0.0 0.0 1.048 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 -1.198 0.0 0.0 0.988 0.0 0.0 0.757 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 -1.198 0.0 0.0 0.988 0.0 0.0 0.757 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 -1.198 0.0 0.0 0.988 0.0 0.0 0.757 0.0 0.0 0.0 -0.0 0.0 - 0.0 0.0 1.225 0.0 0.0 2.554 0.0 0.0 -0.222 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 -0.0 0.0 1.225 0.0 0.0 2.554 0.0 0.0 -0.222 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 1.225 0.0 0.0 2.554 0.0 0.0 -0.222 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 -0.416 0.0 0.0 1.545 0.0 0.0 -2.151 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 -0.416 0.0 0.0 1.545 0.0 0.0 -2.151 0.0 0.0 0.0 -0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 -0.416 0.0 0.0 1.545 0.0 0.0 -2.151 0.0 0.0 0.0 -0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.568 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.568 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 -0.0 0.0 0.0 -0.0 0.0 0.0 0.0 1.568 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.568 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.568
    julia> block(t, SU2Irrep(0)) |> disp5×2 Array{Float64, 2}: - -0.716 0.712 - -1.464 -0.741 - 0.84 -0.028 - -0.032 0.867 - 0.522 0.293
    julia> block(t, SU2Irrep(1)) |> disp5×3 Array{Float64, 2}: - -1.786 0.868 -0.465 - 0.154 -0.15 1.048 - -1.198 0.988 0.757 - 1.225 2.554 -0.222 - -0.416 1.545 -2.151
    julia> block(t, SU2Irrep(2)) |> disp1×1 Array{Float64, 2}: - 1.568

    Note that the basis transforms u and v are no longer permutation matrices, but are still unitary. Furthermore, note that they render the tensor block diagonal, but that now every element of the diagonal blocks labeled by c comes itself in a tensor product with an identity matrix of size dim(c), i.e. dim(SU2Irrep(1)) = 3 and dim(SU2Irrep(2)) = 5.

    Tensor properties

    Given a t::AbstractTensorMap{S,N₁,N₂}, there are various methods to query its properties. The most important are clearly codomain(t) and domain(t). For t::AbstractTensor{S,N}, i.e. t::AbstractTensorMap{S,N,0}, we can use space(t) as synonym for codomain(t). However, for a general AbstractTensorMap this has no meaning. However, we can query space(t, i), the space associated with the ith index. For i ∈ 1:N₁, this corresponds to codomain(t, i) = codomain(t)[i]. For j = i-N₁ ∈ (1:N₂), this corresponds to dual(domain(t, j)) = dual(domain(t)[j]).

    The total number of indices, i.e. N₁+N₂, is given by numind(t), with N₁ == numout(t) and N₂ == numin(t), the number of outgoing and incoming indices. There are also the unexported methods TensorKit.codomainind(t) and TensorKit.domainind(t) which return the tuples (1, 2, …, N₁) and (N₁+1, …, N₁+N₂), and are useful for internal purposes. The type parameter S<:ElementarySpace can be obtained as spacetype(t); the corresponding sector can directly obtained as sectortype(t) and is Trivial when S != GradedSpace. The underlying field scalars of S can also directly be obtained as field(t). This is different from eltype(t), which returns the type of Number in the tensor data, i.e. the type parameter T in the (subtype of) DenseMatrix{T} in which the matrix blocks are stored. Note that during construction, a (one-time) warning is printed if !(T ⊂ field(S)). The specific DenseMatrix{T} subtype in which the tensor data is stored is obtained as storagetype(t). Each of the methods numind, numout, numin, TensorKit.codomainind, TensorKit.domainind, spacetype, sectortype, field, eltype and storagetype work in the type domain as well, i.e. they are encoded in typeof(t).

    Finally, there are methods to probe the data, which we already encountered. blocksectors(t) returns an iterator over the different coupled sectors that can be obtained from fusing the uncoupled sectors available in the domain, but they must also be obtained from fusing the uncoupled sectors available in the codomain (i.e. it is the intersection of both blocksectors(codomain(t)) and blocksectors(domain(t))). For a specific sector c ∈ blocksectors(t), block(t, c) returns the corresponding data. Both are obtained together with blocks(t), which returns an iterator over the pairs c=>block(t, c). Furthermore, there is fusiontrees(t) which returns an iterator over splitting-fusion tree pairs (f₁,f₂), for which the corresponding data is given by t[f₁,f₂] (i.e. using Base.getindex).

    Let's again illustrate these methods with an example, continuing with the tensor t from the previous example

    julia> typeof(t)TensorMap{Float64, GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, 2, 2, Vector{Float64}}
    julia> codomain(t)(Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>2, 1=>1))
    julia> domain(t)(Rep[SU₂](0=>1, 1=>1) ⊗ Rep[SU₂](0=>1, 1=>1)')
    julia> space(t,1)Rep[SU₂](0=>2, 1=>1)
    julia> space(t,2)Rep[SU₂](0=>2, 1=>1)
    julia> space(t,3)Rep[SU₂](0=>1, 1=>1)'
    julia> space(t,4)Rep[SU₂](0=>1, 1=>1)
    julia> numind(t)4
    julia> numout(t)2
    julia> numin(t)2
    julia> spacetype(t)GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}
    julia> sectortype(t)SU2Irrep
    julia> field(t)
    julia> eltype(t)Float64
    julia> storagetype(t)Vector{Float64} (alias for Array{Float64, 1})
    julia> blocksectors(t)3-element Vector{SU2Irrep}: + -0.141 -0.95 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 0.0 0.0 0.0 + -0.106 0.369 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 -0.0 0.0 0.0 + -0.222 0.467 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.414 2.091 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.0 0.0 0.0 + 0.374 0.833 0.0 -0.0 0.0 0.0 -0.0 0.0 0.0 -0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 -0.328 0.0 0.0 -0.528 0.0 0.0 0.795 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 -0.0 0.0 -0.328 0.0 0.0 -0.528 0.0 0.0 0.795 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 -0.328 0.0 0.0 -0.528 0.0 0.0 0.795 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.592 0.0 0.0 1.444 0.0 0.0 -0.458 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.592 0.0 0.0 1.444 0.0 0.0 -0.458 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.592 0.0 0.0 1.444 0.0 0.0 -0.458 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 -0.865 0.0 0.0 0.732 0.0 0.0 0.858 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 + 0.0 -0.0 0.0 -0.865 0.0 0.0 0.732 0.0 0.0 0.858 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 -0.865 0.0 0.0 0.732 0.0 0.0 0.858 0.0 0.0 0.0 -0.0 0.0 + 0.0 0.0 -0.05 0.0 0.0 -0.439 0.0 0.0 0.535 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 -0.05 0.0 0.0 -0.439 0.0 0.0 0.535 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 -0.05 0.0 0.0 -0.439 0.0 0.0 0.535 0.0 0.0 0.0 -0.0 0.0 + 0.0 0.0 -1.261 0.0 0.0 1.47 0.0 0.0 -2.047 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + -0.0 0.0 0.0 -1.261 0.0 0.0 1.47 0.0 0.0 -2.047 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 -1.261 0.0 0.0 1.47 0.0 0.0 -2.047 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.404 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.404 0.0 0.0 0.0 + -0.0 -0.0 0.0 -0.0 0.0 0.0 -0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.404 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.404 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.404
    julia> block(t, SU2Irrep(0)) |> disp5×2 Array{Float64, 2}: + -0.141 -0.95 + -0.106 0.369 + -0.222 0.467 + 0.414 2.091 + 0.374 0.833
    julia> block(t, SU2Irrep(1)) |> disp5×3 Array{Float64, 2}: + -0.328 -0.528 0.795 + 0.592 1.444 -0.458 + -0.865 0.732 0.858 + -0.05 -0.439 0.535 + -1.261 1.47 -2.047
    julia> block(t, SU2Irrep(2)) |> disp1×1 Array{Float64, 2}: + -1.404

    Note that the basis transforms u and v are no longer permutation matrices, but are still unitary. Furthermore, note that they render the tensor block diagonal, but that now every element of the diagonal blocks labeled by c comes itself in a tensor product with an identity matrix of size dim(c), i.e. dim(SU2Irrep(1)) = 3 and dim(SU2Irrep(2)) = 5.

    Tensor properties

    Given a t::AbstractTensorMap{S,N₁,N₂}, there are various methods to query its properties. The most important are clearly codomain(t) and domain(t). For t::AbstractTensor{S,N}, i.e. t::AbstractTensorMap{S,N,0}, we can use space(t) as synonym for codomain(t). However, for a general AbstractTensorMap this has no meaning. However, we can query space(t, i), the space associated with the ith index. For i ∈ 1:N₁, this corresponds to codomain(t, i) = codomain(t)[i]. For j = i-N₁ ∈ (1:N₂), this corresponds to dual(domain(t, j)) = dual(domain(t)[j]).

    The total number of indices, i.e. N₁+N₂, is given by numind(t), with N₁ == numout(t) and N₂ == numin(t), the number of outgoing and incoming indices. There are also the unexported methods TensorKit.codomainind(t) and TensorKit.domainind(t) which return the tuples (1, 2, …, N₁) and (N₁+1, …, N₁+N₂), and are useful for internal purposes. The type parameter S<:ElementarySpace can be obtained as spacetype(t); the corresponding sector can directly obtained as sectortype(t) and is Trivial when S != GradedSpace. The underlying field scalars of S can also directly be obtained as field(t). This is different from eltype(t), which returns the type of Number in the tensor data, i.e. the type parameter T in the (subtype of) DenseMatrix{T} in which the matrix blocks are stored. Note that during construction, a (one-time) warning is printed if !(T ⊂ field(S)). The specific DenseMatrix{T} subtype in which the tensor data is stored is obtained as storagetype(t). Each of the methods numind, numout, numin, TensorKit.codomainind, TensorKit.domainind, spacetype, sectortype, field, eltype and storagetype work in the type domain as well, i.e. they are encoded in typeof(t).

    Finally, there are methods to probe the data, which we already encountered. blocksectors(t) returns an iterator over the different coupled sectors that can be obtained from fusing the uncoupled sectors available in the domain, but they must also be obtained from fusing the uncoupled sectors available in the codomain (i.e. it is the intersection of both blocksectors(codomain(t)) and blocksectors(domain(t))). For a specific sector c ∈ blocksectors(t), block(t, c) returns the corresponding data. Both are obtained together with blocks(t), which returns an iterator over the pairs c=>block(t, c). Furthermore, there is fusiontrees(t) which returns an iterator over splitting-fusion tree pairs (f₁,f₂), for which the corresponding data is given by t[f₁,f₂] (i.e. using Base.getindex).

    Let's again illustrate these methods with an example, continuing with the tensor t from the previous example

    julia> typeof(t)TensorMap{Float64, GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, 2, 2, Vector{Float64}}
    julia> codomain(t)(Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>2, 1=>1))
    julia> domain(t)(Rep[SU₂](0=>1, 1=>1) ⊗ Rep[SU₂](0=>1, 1=>1)')
    julia> space(t,1)Rep[SU₂](0=>2, 1=>1)
    julia> space(t,2)Rep[SU₂](0=>2, 1=>1)
    julia> space(t,3)Rep[SU₂](0=>1, 1=>1)'
    julia> space(t,4)Rep[SU₂](0=>1, 1=>1)
    julia> numind(t)4
    julia> numout(t)2
    julia> numin(t)2
    julia> spacetype(t)GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}
    julia> sectortype(t)SU2Irrep
    julia> field(t)
    julia> eltype(t)Float64
    julia> storagetype(t)Vector{Float64} (alias for Array{Float64, 1})
    julia> blocksectors(t)3-element Vector{SU2Irrep}: 0 1 2
    julia> blocks(t)Base.Generator{TensorKit.SortedVectorDict{SU2Irrep, Tuple{Tuple{Int64, Int64}, UnitRange{Int64}}}, TensorKit.var"#162#163"{TensorMap{Float64, GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, 2, 2, Vector{Float64}}}}(TensorKit.var"#162#163"{TensorMap{Float64, GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, 2, 2, Vector{Float64}}}(TensorMap((Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>2, 1=>1)) ← (Rep[SU₂](0=>1, 1=>1) ⊗ Rep[SU₂](0=>1, 1=>1)')): * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 0), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0, 0), 0, (false, true), ()): [:, :, 1, 1] = - -0.7164601000777033 0.8402881670666787 - -1.4645668185619172 -0.03284094745363703 + -0.14142885082251372 -0.22265024345214926 + -0.10641507475143616 0.4144839187928769 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0, 0), 0, (false, true), ()): [:, :, 1, 1] = - 0.5221319556303144 + 0.3745894707185757 * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 0), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 0, (false, true), ()): [:, :, 1, 1] = - 0.7129636135872823 -0.028566834076525237 - -0.7411796189245599 0.8673471875980863 + -0.9502777270353865 0.4676512412925039 + 0.3692804864747098 2.091142714812541 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 0, (false, true), ()): [:, :, 1, 1] = - 0.2937111218523841 + 0.8333536386959417 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 0), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 0), 1, (false, true), ()): [:, :, 1, 1] = - -1.7868657048152816 0.15466876990842407 + -0.3284658974354606 0.5925056681953469 * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 0), 1, (false, true), ()): [:, :, 1, 1] = - -1.1981471013862421 - 1.2257355729332762 + -0.8651946080202491 + -0.05091483888723515 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 0), 1, (false, true), ()): [:, :, 1, 1] = - -0.4166416461033738 + -1.261244334418641 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 0), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0, 1), 1, (false, true), ()): [:, :, 1, 1] = - 0.868556965587991 -0.1500096447891483 + -0.5282302914719381 1.4446178291840646 * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0, 1), 1, (false, true), ()): [:, :, 1, 1] = - 0.9885294518048647 - 2.554370698368057 + 0.7321866306095242 + -0.439996319310446 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0, 1), 1, (false, true), ()): [:, :, 1, 1] = - 1.5458388338200124 + 1.4703776344935073 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 0), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 1, (false, true), ()): [:, :, 1, 1] = - -0.4657974434043403 1.0485837300449454 + 0.7953988648145892 -0.4587415424347604 * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 1, (false, true), ()): [:, :, 1, 1] = - 0.7572887531967569 - -0.22200786820735938 + 0.85895930189713 + 0.5353945130053149 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 1, (false, true), ()): [:, :, 1, 1] = - -2.151040091109563 + -2.047162563558052 * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 2, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1, 1), 2, (false, true), ()): [:, :, 1, 1] = - 1.5683963426572347 + -1.4043563428009727 ), TensorKit.SortedVectorDict{SU2Irrep, Tuple{Tuple{Int64, Int64}, UnitRange{Int64}}}(0 => ((5, 2), 1:10), 1 => ((5, 3), 11:25), 2 => ((1, 1), 26:26)))
    julia> block(t, first(blocksectors(t)))5×2 reshape(view(::Vector{Float64}, 1:10), 5, 2) with eltype Float64: - -0.71646 0.712964 - -1.46457 -0.74118 - 0.840288 -0.0285668 - -0.0328409 0.867347 - 0.522132 0.293711
    julia> fusiontrees(t)14-element Vector{Tuple{FusionTree{SU2Irrep, 2, 0, 1}, FusionTree{SU2Irrep, 2, 0, 1}}}: + -0.141429 -0.950278 + -0.106415 0.36928 + -0.22265 0.467651 + 0.414484 2.09114 + 0.374589 0.833354
    julia> fusiontrees(t)14-element Vector{Tuple{FusionTree{SU2Irrep, 2, 0, 1}, FusionTree{SU2Irrep, 2, 0, 1}}}: (FusionTree{Irrep[SU₂]}((0, 0), 0, (false, false), ()), FusionTree{Irrep[SU₂]}((0, 0), 0, (false, true), ())) (FusionTree{Irrep[SU₂]}((1, 1), 0, (false, false), ()), FusionTree{Irrep[SU₂]}((0, 0), 0, (false, true), ())) (FusionTree{Irrep[SU₂]}((0, 0), 0, (false, false), ()), FusionTree{Irrep[SU₂]}((1, 1), 0, (false, true), ())) @@ -768,16 +768,16 @@ (FusionTree{Irrep[SU₂]}((1, 1), 1, (false, false), ()), FusionTree{Irrep[SU₂]}((1, 1), 1, (false, true), ())) (FusionTree{Irrep[SU₂]}((1, 1), 2, (false, false), ()), FusionTree{Irrep[SU₂]}((1, 1), 2, (false, true), ()))
    julia> f1, f2 = first(fusiontrees(t))(FusionTree{Irrep[SU₂]}((0, 0), 0, (false, false), ()), FusionTree{Irrep[SU₂]}((0, 0), 0, (false, true), ()))
    julia> t[f1,f2]2×2×1×1 StridedViews.StridedView{Float64, 4, Vector{Float64}, typeof(identity)}: [:, :, 1, 1] = - -0.71646 0.840288 - -1.46457 -0.0328409

    Reading and writing tensors: Dict conversion

    There are no custom or dedicated methods for reading, writing or storing TensorMaps, however, there is the possibility to convert a t::AbstractTensorMap into a Dict, simply as convert(Dict, t). The backward conversion convert(TensorMap, dict) will return a tensor that is equal to t, i.e. t == convert(TensorMap, convert(Dict, t)).

    This conversion relies on that the string represenation of objects such as VectorSpace, FusionTree or Sector should be such that it represents valid code to recreate the object. Hence, we store information about the domain and codomain of the tensor, and the sector associated with each data block, as a String obtained with repr. This provides the flexibility to still change the internal structure of such objects, without this breaking the ability to load older data files. The resulting dictionary can then be stored using any of the provided Julia packages such as JLD.jl, JLD2.jl, BSON.jl, JSON.jl, ...

    Vector space and linear algebra operations

    AbstractTensorMap instances t represent linear maps, i.e. homomorphisms in a 𝕜-linear category, just like matrices. To a large extent, they follow the interface of Matrix in Julia's LinearAlgebra standard library. Many methods from LinearAlgebra are (re)exported by TensorKit.jl, and can then us be used without using LinearAlgebra explicitly. In all of the following methods, the implementation acts directly on the underlying matrix blocks (typically using the same method) and never needs to perform any basis transforms.

    In particular, AbstractTensorMap instances can be composed, provided the domain of the first object coincides with the codomain of the second. Composing tensor maps uses the regular multiplication symbol as in t = t1*t2, which is also used for matrix multiplication. TensorKit.jl also supports (and exports) the mutating method mul!(t, t1, t2). We can then also try to invert a tensor map using inv(t), though this can only exist if the domain and codomain are isomorphic, which can e.g. be checked as fuse(codomain(t)) == fuse(domain(t)). If the inverse is composed with another tensor t2, we can use the syntax t1\t2 or t2/t1. However, this syntax also accepts instances t1 whose domain and codomain are not isomorphic, and then amounts to pinv(t1), the Moore-Penrose pseudoinverse. This, however, is only really justified as minimizing the least squares problem if InnerProductStyle(t) <: EuclideanProduct.

    AbstractTensorMap instances behave themselves as vectors (i.e. they are 𝕜-linear) and so they can be multiplied by scalars and, if they live in the same space, i.e. have the same domain and codomain, they can be added to each other. There is also a zero(t), the additive identity, which produces a zero tensor with the same domain and codomain as t. In addition, TensorMap supports basic Julia methods such as fill! and copy!, as well as copy(t) to create a copy with independent data. Aside from basic + and * operations, TensorKit.jl reexports a number of efficient in-place methods from LinearAlgebra, such as axpy! (for y ← α * x + y), axpby! (for y ← α * x + β * y), lmul! and rmul! (for y ← α*y and y ← y*α, which is typically the same) and mul!, which can also be used for out-of-place scalar multiplication y ← α*x.

    For t::AbstractTensorMap{S} where InnerProductStyle(S) <: EuclideanProduct, we can compute norm(t), and for two such instances, the inner product dot(t1, t2), provided t1 and t2 have the same domain and codomain. Furthermore, there is normalize(t) and normalize!(t) to return a scaled version of t with unit norm. These operations should also exist for InnerProductStyle(S) <: HasInnerProduct, but require an interface for defining a custom inner product in these spaces. Currently, there is no concrete subtype of HasInnerProduct that is not an EuclideanProduct. In particular, CartesianSpace, ComplexSpace and GradedSpace all have InnerProductStyle(V) <: EuclideanProduct.

    With tensors that have InnerProductStyle(t) <: EuclideanProduct there is associated an adjoint operation, given by adjoint(t) or simply t', such that domain(t') == codomain(t) and codomain(t') == domain(t). Note that for an instance t::TensorMap{S,N₁,N₂}, t' is simply stored in a wrapper called AdjointTensorMap{S,N₂,N₁}, which is another subtype of AbstractTensorMap. This should be mostly unvisible to the user, as all methods should work for this type as well. It can be hard to reason about the index order of t', i.e. index i of t appears in t' at index position j = TensorKit.adjointtensorindex(t, i), where the latter method is typically not necessary and hence unexported. There is also a plural TensorKit.adjointtensorindices to convert multiple indices at once. Note that, because the adjoint interchanges domain and codomain, we have space(t', j) == space(t, i)'.

    AbstractTensorMap instances can furthermore be tested for exact (t1 == t2) or approximate (t1 ≈ t2) equality, though the latter requires that norm can be computed.

    When tensor map instances are endomorphisms, i.e. they have the same domain and codomain, there is a multiplicative identity which can be obtained as one(t) or one!(t), where the latter overwrites the contents of t. The multiplicative identity on a space V can also be obtained using id(A, V) as discussed above, such that for a general homomorphism t′, we have t′ == id(codomain(t′))*t′ == t′*id(domain(t′)). Returning to the case of endomorphisms t, we can compute the trace via tr(t) and exponentiate them using exp(t), or if the contents of t can be destroyed in the process, exp!(t). Furthermore, there are a number of tensor factorizations for both endomorphisms and general homomorphism that we discuss below.

    Finally, there are a number of operations that also belong in this paragraph because of their analogy to common matrix operations. The tensor product of two TensorMap instances t1 and t2 is obtained as t1 ⊗ t2 and results in a new TensorMap with codomain(t1⊗t2) = codomain(t1) ⊗ codomain(t2) and domain(t1⊗t2) = domain(t1) ⊗ domain(t2). If we have two TensorMap{S,N,1} instances t1 and t2 with the same codomain, we can combine them in a way that is analoguous to hcat, i.e. we stack them such that the new tensor catdomain(t1, t2) has also the same codomain, but has a domain which is domain(t1) ⊕ domain(t2). Similarly, if t1 and t2 are of type TensorMap{S,1,N} and have the same domain, the operation catcodomain(t1, t2) results in a new tensor with the same domain and a codomain given by codomain(t1) ⊕ codomain(t2), which is the analogy of vcat. Note that direct sum only makes sense between ElementarySpace objects, i.e. there is no way to give a tensor product meaning to a direct sum of tensor product spaces.

    Time for some more examples:

    julia> t == t + zero(t) == t*id(domain(t)) == id(codomain(t))*ttrue
    julia> t2 = randn(ComplexF64, codomain(t), domain(t));
    julia> dot(t2, t)-10.68263476454988 + 25.128394162215187im
    julia> tr(t2'*t)-10.68263476454988 + 25.128394162215187im
    julia> dot(t2, t) ≈ dot(t', t2')true
    julia> dot(t2, t2)63.145610488615716 + 0.0im
    julia> norm(t2)^263.145610488615716
    julia> t3 = copy!(similar(t, ComplexF64), t);
    julia> t3 == ttrue
    julia> rmul!(t3, 0.8);
    julia> t3 ≈ 0.8*ttrue
    julia> axpby!(0.5, t2, 1.3im, t3);
    julia> t3 ≈ 0.5 * t2 + 0.8 * 1.3im * ttrue
    julia> t4 = randn(fuse(codomain(t)), codomain(t));
    julia> t5 = TensorMap(undef, fuse(codomain(t)), domain(t));ERROR: MethodError: no method matching TensorMap(::UndefInitializer, ::GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, ::ProductSpace{GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, 2}) + -0.141429 -0.22265 + -0.106415 0.414484

    Reading and writing tensors: Dict conversion

    There are no custom or dedicated methods for reading, writing or storing TensorMaps, however, there is the possibility to convert a t::AbstractTensorMap into a Dict, simply as convert(Dict, t). The backward conversion convert(TensorMap, dict) will return a tensor that is equal to t, i.e. t == convert(TensorMap, convert(Dict, t)).

    This conversion relies on that the string represenation of objects such as VectorSpace, FusionTree or Sector should be such that it represents valid code to recreate the object. Hence, we store information about the domain and codomain of the tensor, and the sector associated with each data block, as a String obtained with repr. This provides the flexibility to still change the internal structure of such objects, without this breaking the ability to load older data files. The resulting dictionary can then be stored using any of the provided Julia packages such as JLD.jl, JLD2.jl, BSON.jl, JSON.jl, ...

    Vector space and linear algebra operations

    AbstractTensorMap instances t represent linear maps, i.e. homomorphisms in a 𝕜-linear category, just like matrices. To a large extent, they follow the interface of Matrix in Julia's LinearAlgebra standard library. Many methods from LinearAlgebra are (re)exported by TensorKit.jl, and can then us be used without using LinearAlgebra explicitly. In all of the following methods, the implementation acts directly on the underlying matrix blocks (typically using the same method) and never needs to perform any basis transforms.

    In particular, AbstractTensorMap instances can be composed, provided the domain of the first object coincides with the codomain of the second. Composing tensor maps uses the regular multiplication symbol as in t = t1*t2, which is also used for matrix multiplication. TensorKit.jl also supports (and exports) the mutating method mul!(t, t1, t2). We can then also try to invert a tensor map using inv(t), though this can only exist if the domain and codomain are isomorphic, which can e.g. be checked as fuse(codomain(t)) == fuse(domain(t)). If the inverse is composed with another tensor t2, we can use the syntax t1\t2 or t2/t1. However, this syntax also accepts instances t1 whose domain and codomain are not isomorphic, and then amounts to pinv(t1), the Moore-Penrose pseudoinverse. This, however, is only really justified as minimizing the least squares problem if InnerProductStyle(t) <: EuclideanProduct.

    AbstractTensorMap instances behave themselves as vectors (i.e. they are 𝕜-linear) and so they can be multiplied by scalars and, if they live in the same space, i.e. have the same domain and codomain, they can be added to each other. There is also a zero(t), the additive identity, which produces a zero tensor with the same domain and codomain as t. In addition, TensorMap supports basic Julia methods such as fill! and copy!, as well as copy(t) to create a copy with independent data. Aside from basic + and * operations, TensorKit.jl reexports a number of efficient in-place methods from LinearAlgebra, such as axpy! (for y ← α * x + y), axpby! (for y ← α * x + β * y), lmul! and rmul! (for y ← α*y and y ← y*α, which is typically the same) and mul!, which can also be used for out-of-place scalar multiplication y ← α*x.

    For t::AbstractTensorMap{S} where InnerProductStyle(S) <: EuclideanProduct, we can compute norm(t), and for two such instances, the inner product dot(t1, t2), provided t1 and t2 have the same domain and codomain. Furthermore, there is normalize(t) and normalize!(t) to return a scaled version of t with unit norm. These operations should also exist for InnerProductStyle(S) <: HasInnerProduct, but require an interface for defining a custom inner product in these spaces. Currently, there is no concrete subtype of HasInnerProduct that is not an EuclideanProduct. In particular, CartesianSpace, ComplexSpace and GradedSpace all have InnerProductStyle(V) <: EuclideanProduct.

    With tensors that have InnerProductStyle(t) <: EuclideanProduct there is associated an adjoint operation, given by adjoint(t) or simply t', such that domain(t') == codomain(t) and codomain(t') == domain(t). Note that for an instance t::TensorMap{S,N₁,N₂}, t' is simply stored in a wrapper called AdjointTensorMap{S,N₂,N₁}, which is another subtype of AbstractTensorMap. This should be mostly unvisible to the user, as all methods should work for this type as well. It can be hard to reason about the index order of t', i.e. index i of t appears in t' at index position j = TensorKit.adjointtensorindex(t, i), where the latter method is typically not necessary and hence unexported. There is also a plural TensorKit.adjointtensorindices to convert multiple indices at once. Note that, because the adjoint interchanges domain and codomain, we have space(t', j) == space(t, i)'.

    AbstractTensorMap instances can furthermore be tested for exact (t1 == t2) or approximate (t1 ≈ t2) equality, though the latter requires that norm can be computed.

    When tensor map instances are endomorphisms, i.e. they have the same domain and codomain, there is a multiplicative identity which can be obtained as one(t) or one!(t), where the latter overwrites the contents of t. The multiplicative identity on a space V can also be obtained using id(A, V) as discussed above, such that for a general homomorphism t′, we have t′ == id(codomain(t′))*t′ == t′*id(domain(t′)). Returning to the case of endomorphisms t, we can compute the trace via tr(t) and exponentiate them using exp(t), or if the contents of t can be destroyed in the process, exp!(t). Furthermore, there are a number of tensor factorizations for both endomorphisms and general homomorphism that we discuss below.

    Finally, there are a number of operations that also belong in this paragraph because of their analogy to common matrix operations. The tensor product of two TensorMap instances t1 and t2 is obtained as t1 ⊗ t2 and results in a new TensorMap with codomain(t1⊗t2) = codomain(t1) ⊗ codomain(t2) and domain(t1⊗t2) = domain(t1) ⊗ domain(t2). If we have two TensorMap{S,N,1} instances t1 and t2 with the same codomain, we can combine them in a way that is analoguous to hcat, i.e. we stack them such that the new tensor catdomain(t1, t2) has also the same codomain, but has a domain which is domain(t1) ⊕ domain(t2). Similarly, if t1 and t2 are of type TensorMap{S,1,N} and have the same domain, the operation catcodomain(t1, t2) results in a new tensor with the same domain and a codomain given by codomain(t1) ⊕ codomain(t2), which is the analogy of vcat. Note that direct sum only makes sense between ElementarySpace objects, i.e. there is no way to give a tensor product meaning to a direct sum of tensor product spaces.

    Time for some more examples:

    julia> t == t + zero(t) == t*id(domain(t)) == id(codomain(t))*ttrue
    julia> t2 = randn(ComplexF64, codomain(t), domain(t));
    julia> dot(t2, t)2.350178340556755 + 5.927750853923753im
    julia> tr(t2'*t)2.3501783405567553 + 5.9277508539237544im
    julia> dot(t2, t) ≈ dot(t', t2')true
    julia> dot(t2, t2)59.87948091043234 + 0.0im
    julia> norm(t2)^259.879480910432335
    julia> t3 = copy!(similar(t, ComplexF64), t);
    julia> t3 == ttrue
    julia> rmul!(t3, 0.8);
    julia> t3 ≈ 0.8*ttrue
    julia> axpby!(0.5, t2, 1.3im, t3);
    julia> t3 ≈ 0.5 * t2 + 0.8 * 1.3im * ttrue
    julia> t4 = randn(fuse(codomain(t)), codomain(t));
    julia> t5 = TensorMap(undef, fuse(codomain(t)), domain(t));ERROR: MethodError: no method matching TensorMap(::UndefInitializer, ::GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, ::ProductSpace{GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, 2}) The type `TensorMap` exists, but no method is defined for this combination of argument types when trying to construct it. Closest candidates are: - TensorMap(::typeof(randuniform), ::TensorSpace, ::TensorSpace) - @ TensorKit deprecated.jl:103 TensorMap(::typeof(randn), ::TensorSpace, ::TensorSpace) @ TensorKit deprecated.jl:103 - TensorMap(::typeof(randhaar), ::TensorSpace, ::TensorSpace) + TensorMap(::typeof(randuniform), ::TensorSpace, ::TensorSpace) + @ TensorKit deprecated.jl:103 + TensorMap(::typeof(rand), ::TensorSpace, ::TensorSpace) @ TensorKit deprecated.jl:103 ...
    julia> mul!(t5, t4, t) == t4*tERROR: SpaceMismatch("(Rep[SU₂](1/2=>1) ⊗ Rep[SU₂](1/2=>1)) ← (Rep[SU₂](1/2=>1) ⊗ Rep[SU₂](1/2=>1)) ≠ Rep[SU₂](0=>5, 1=>5, 2=>1) ← (Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>2, 1=>1)) * (Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>2, 1=>1)) ← (Rep[SU₂](0=>1, 1=>1) ⊗ Rep[SU₂](0=>1, 1=>1)')")
    julia> inv(t4) * t4 ≈ id(codomain(t))true
    julia> t4 * inv(t4) ≈ id(fuse(codomain(t)))true
    julia> t4 \ (t4 * t) ≈ ttrue
    julia> t6 = randn(ComplexF64, V1, codomain(t));
    julia> numout(t4) == numout(t6) == 1true
    julia> t7 = catcodomain(t4, t6);ERROR: MethodError: no method matching catcodomain(::TensorMap{Float64, GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, 1, 2, Vector{Float64}}, ::TensorMap{ComplexF64, GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, 1, 2, Vector{ComplexF64}}) The function `catcodomain` exists, but no method is defined for this combination of argument types. @@ -794,31 +794,31 @@ p1::NTuple{N₁′,Int}, p2::NTuple{N₂′,Int})

    and

    permute(t::AbstractTensorMap{S,N₁,N₂},
             p1::NTuple{N₁′,Int}, p2::NTuple{N₂′,Int}; copy = false)

    both of which return an instance of AbstractTensorMap{S,N₁′,N₂′}.

    In these methods, p1 and p2 specify which of the original tensor indices ranging from 1 to N₁+N₂ make up the new codomain (with N₁′ spaces) and new domain (with N₂′ spaces). Hence, (p1..., p2...) should be a valid permutation of 1:(N₁+N₂). Note that, throughout TensorKit.jl, permutations are always specified using tuples of Ints, for reasons of type stability. For braid, we also need to specify levels or depths for each of the indices of the original tensor, which determine whether indices will braid over or underneath each other (use the braiding or its inverse). We refer to the section on manipulating fusion trees for more details.

    When BraidingStyle(sectortype(t)) isa SymmetricBraiding, we can use the simpler interface of permute, which does not require the argument levels. permute accepts a keyword argument copy. When copy == true, the result will be a tensor with newly allocated data that can independently be modified from that of the input tensor t. When copy takes the default value false, permute can try to return the result in a way that it shares its data with the input tensor t, though this is only possible in specific cases (e.g. when sectortype(S) == Trivial and (p1..., p2...) = (1:(N₁+N₂)...)).

    Both braid and permute come in a version where the result is stored in an already existing tensor, i.e. braid!(tdst, tsrc, levels, p1, p2) and permute!(tdst, tsrc, p1, p2).

    Another operation that belongs und index manipulations is taking the transpose of a tensor, i.e. LinearAlgebra.transpose(t) and LinearAlgebra.transpose!(tdst, tsrc), both of which are reexported by TensorKit.jl. Note that transpose(t) is not simply equal to reshuffling domain and codomain with braid(t, (1:(N₁+N₂)...), reverse(domainind(tsrc)), reverse(codomainind(tsrc)))). Indeed, the graphical representation (where we draw the codomain and domain as a single object), makes clear that this introduces an additional (inverse) twist, which is then compensated in the transpose implementation.

    transpose

    In categorical language, the reason for this extra twist is that we use the left coevaluation $η$, but the right evaluation $\tilde{ϵ}$, when repartitioning the indices between domain and codomain.

    There are a number of other index related manipulations. We can apply a twist (or inverse twist) to one of the tensor map indices via twist(t, i; inv = false) or twist!(t, i; inv = false). Note that the latter method does not store the result in a new destination tensor, but just modifies the tensor t in place. Twisting several indices simultaneously can be obtained by using the defining property

    $θ_{V⊗W} = τ_{W,V} ∘ (θ_W ⊗ θ_V) ∘ τ_{V,W} = (θ_V ⊗ θ_W) ∘ τ_{W,V} ∘ τ_{V,W}.$

    but is currently not implemented explicitly.

    For all sector types I with BraidingStyle(I) == Bosonic(), all twists are 1 and thus have no effect. Let us start with some examples, in which we illustrate that, albeit permute might act highly non-trivial on the fusion trees and on the corresponding data, after conversion to a regular Array (when possible), it just acts like permutedims

    julia> domain(t) → codomain(t)(Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>2, 1=>1)) ← (Rep[SU₂](0=>1, 1=>1) ⊗ Rep[SU₂](0=>1, 1=>1)')
    julia> ta = convert(Array, t);
    julia> t′ = permute(t, (1,2,3,4));
    julia> domain(t′) → codomain(t′)(Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>1, 1=>1)' ⊗ Rep[SU₂](0=>1, 1=>1)) ← ProductSpace{GradedSpace{SU2Irrep, TensorKit.SortedVectorDict{SU2Irrep, Int64}}, 0}()
    julia> convert(Array, t′) ≈ tatrue
    julia> t′′ = permute(t, (4,2,3),(1,));
    julia> domain(t′′) → codomain(t′′)(Rep[SU₂](0=>1, 1=>1) ⊗ Rep[SU₂](0=>2, 1=>1) ⊗ Rep[SU₂](0=>1, 1=>1)') ← Rep[SU₂](0=>2, 1=>1)'
    julia> convert(Array, t′′) ≈ permutedims(ta, (4,2,3,1))true
    julia> mTensorMap(Rep[SU₂](0=>2, 1=>1) ← Rep[SU₂](0=>1, 1=>1)): * Data for fusiontree FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()): - -0.9518578276175027 - 1.1879064344331187 + -1.0527682492930184 + 0.5257771429691075 * Data for fusiontree FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()): - -0.5992670495872038
    julia> transpose(m)TensorMap(Rep[SU₂](0=>1, 1=>1)' ← Rep[SU₂](0=>2, 1=>1)'): + -0.9308507478607294
    julia> transpose(m)TensorMap(Rep[SU₂](0=>1, 1=>1)' ← Rep[SU₂](0=>2, 1=>1)'): * Data for fusiontree FusionTree{Irrep[SU₂]}((0,), 0, (true,), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (true,), ()): - -0.9518578276175027 1.1879064344331187 + -1.0527682492930184 0.5257771429691075 * Data for fusiontree FusionTree{Irrep[SU₂]}((1,), 1, (true,), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (true,), ()): - -0.5992670495872037
    julia> convert(Array, transpose(t)) ≈ permutedims(ta,(4,3,2,1))true
    julia> dot(t2, t) ≈ dot(transpose(t2), transpose(t))true
    julia> transpose(transpose(t)) ≈ ttrue
    julia> twist(t, 3) ≈ t + -0.9308507478607293
    julia> convert(Array, transpose(t)) ≈ permutedims(ta,(4,3,2,1))true
    julia> dot(t2, t) ≈ dot(transpose(t2), transpose(t))true
    julia> transpose(transpose(t)) ≈ ttrue
    julia> twist(t, 3) ≈ t # as twist acts trivially fortrue
    julia> BraidingStyle(sectortype(t))Bosonic()

    Note that transpose acts like one would expect on a TensorMap{S,1,1}. On a TensorMap{S,N₁,N₂}, because transpose replaces the codomain with the dual of the domain, which has its tensor product operation reversed, this in the end amounts in a complete reversal of all tensor indices when representing it as a plain mutli-dimensional Array. Also, note that we have not defined the conjugation of TensorMap instances. One definition that one could think of is conj(t) = adjoint(transpose(t)). However note that codomain(adjoint(tranpose(t))) == domain(transpose(t)) == dual(codomain(t)) and similarly domain(adjoint(tranpose(t))) == dual(domain(t)), where dual of a ProductSpace is composed of the dual of the ElementarySpace instances, in reverse order of tensor product. This might be very confusing, and as such we leave tensor conjugation undefined. However, note that we have a conjugation syntax within the context of tensor contractions.

    To show the effect of twist, we now consider a type of sector I for which BraidingStyle{I} != Bosonic(). In particular, we use FibonacciAnyon. We cannot convert the resulting TensorMap to an Array, so we have to rely on indirect tests to verify our results.

    julia> V1 = GradedSpace{FibonacciAnyon}(:I=>3,:τ=>2)Vect[FibonacciAnyon](:I=>3, :τ=>2)
    julia> V2 = GradedSpace{FibonacciAnyon}(:I=>2,:τ=>1)Vect[FibonacciAnyon](:I=>2, :τ=>1)
    julia> m = TensorMap(randn, Float32, V1, V2)┌ Warning: Tensors with real data might be incompatible with sector type FibonacciAnyon └ @ TensorKit ~/work/TensorKit.jl/TensorKit.jl/src/tensors/tensor.jl:35 TensorMap(Vect[FibonacciAnyon](:I=>3, :τ=>2) ← Vect[FibonacciAnyon](:I=>2, :τ=>1)): * Data for fusiontree FusionTree{FibonacciAnyon}((:I,), :I, (false,), ()) ← FusionTree{FibonacciAnyon}((:I,), :I, (false,), ()): - -0.6703278f0 0.7385759f0 - 0.51674694f0 0.6184607f0 - 0.46419704f0 0.8896455f0 + -1.975851f0 -0.84952635f0 + -0.71975386f0 -1.3395505f0 + -0.15452242f0 0.26271293f0 * Data for fusiontree FusionTree{FibonacciAnyon}((:τ,), :τ, (false,), ()) ← FusionTree{FibonacciAnyon}((:τ,), :τ, (false,), ()): - 1.834889f0 - -0.6406162f0
    julia> transpose(m)┌ Warning: Tensors with real data might be incompatible with sector type FibonacciAnyon + 1.4607205f0 + 1.4340441f0
    julia> transpose(m)┌ Warning: Tensors with real data might be incompatible with sector type FibonacciAnyon └ @ TensorKit ~/work/TensorKit.jl/TensorKit.jl/src/tensors/tensor.jl:35 TensorMap(Vect[FibonacciAnyon](:I=>2, :τ=>1)' ← Vect[FibonacciAnyon](:I=>3, :τ=>2)'): * Data for fusiontree FusionTree{FibonacciAnyon}((:I,), :I, (true,), ()) ← FusionTree{FibonacciAnyon}((:I,), :I, (true,), ()): - -0.6703278f0 0.51674694f0 0.46419704f0 - 0.7385759f0 0.6184607f0 0.8896455f0 + -1.975851f0 -0.71975386f0 -0.15452242f0 + -0.84952635f0 -1.3395505f0 0.26271293f0 * Data for fusiontree FusionTree{FibonacciAnyon}((:τ,), :τ, (true,), ()) ← FusionTree{FibonacciAnyon}((:τ,), :τ, (true,), ()): - 1.834889f0 -0.6406162f0
    julia> twist(braid(m, (1,2), (2,), (1,)), 1)ERROR: AssertionError: length(levels) == numind(t)
    julia> t1 = randn(V1*V2', V2*V1);┌ Warning: Tensors with real data might be incompatible with sector type FibonacciAnyon + 1.4607205f0 1.4340441f0
    julia> twist(braid(m, (1,2), (2,), (1,)), 1)ERROR: AssertionError: length(levels) == numind(t)
    julia> t1 = randn(V1*V2', V2*V1);┌ Warning: Tensors with real data might be incompatible with sector type FibonacciAnyon └ @ TensorKit ~/work/TensorKit.jl/TensorKit.jl/src/tensors/tensor.jl:35
    julia> t2 = randn(ComplexF64, V1*V2', V2*V1);
    julia> dot(t1, t2) ≈ dot(transpose(t1), transpose(t2))┌ Warning: Tensors with real data might be incompatible with sector type FibonacciAnyon └ @ TensorKit ~/work/TensorKit.jl/TensorKit.jl/src/tensors/tensor.jl:35 true
    julia> transpose(transpose(t1)) ≈ t1┌ Warning: Tensors with real data might be incompatible with sector type FibonacciAnyon @@ -826,21 +826,21 @@ true

    A final operation that one might expect in this section is to fuse or join indices, and its inverse, to split a given index into two or more indices. For a plain tensor (i.e. with sectortype(t) == Trivial) amount to the equivalent of reshape on the multidimensional data. However, this represents only one possibility, as there is no canonically unique way to embed the tensor product of two spaces V₁ ⊗ V₂ in a new space V = fuse(V₁⊗V₂). Such a mapping can always be accompagnied by a basis transform. However, one particular choice is created by the function isomorphism, or for EuclideanProduct spaces, unitary. Hence, we can join or fuse two indices of a tensor by first constructing u = unitary(fuse(space(t, i) ⊗ space(t, j)), space(t, i) ⊗ space(t, j)) and then contracting this map with indices i and j of t, as explained in the section on contracting tensors. Note, however, that a typical algorithm is not expected to often need to fuse and split indices, as e.g. tensor factorizations can easily be applied without needing to reshape or fuse indices first, as explained in the next section.

    Tensor factorizations

    Eigenvalue decomposition

    As tensors are linear maps, they have various kinds of factorizations. Endomorphism, i.e. tensor maps t with codomain(t) == domain(t), have an eigenvalue decomposition. For this, we overload both LinearAlgebra.eigen(t; kwargs...) and LinearAlgebra.eigen!(t; kwargs...), where the latter destroys t in the process. The keyword arguments are the same that are accepted by LinearAlgebra.eigen(!) for matrices. The result is returned as D, V = eigen(t), such that t*V ≈ V*D. For given t::TensorMap{S,N,N}, V is a TensorMap{S,N,1}, whose codomain corresponds to that of t, but whose domain is a single space S (or more correctly a ProductSpace{S,1}), that corresponds to fuse(codomain(t)). The eigenvalues are encoded in D, a TensorMap{S,1,1}, whose domain and codomain correspond to the domain of V. Indeed, we cannot reasonably associate a tensor product structure with the different eigenvalues. Note that D stores the eigenvalues on the diagonal of a (collection of) DenseMatrix instance(s), as there is currently no dedicated DiagonalTensorMap or diagonal storage support.

    We also define LinearAlgebra.ishermitian(t), which can only return true for instances of AbstractEuclideanTensorMap. In all other cases, as the inner product is not defined, there is no notion of hermiticity (i.e. we are not working in a -category). For instances of EuclideanTensorMap, we also define and export the routines eigh and eigh!, which compute the eigenvalue decomposition under the guarantee (not checked) that the map is hermitian. Hence, eigenvalues will be real and V will be unitary with eltype(V) == eltype(t). We also define and export eig and eig!, which similarly assume that the TensorMap is not hermitian (hence this does not require EuclideanTensorMap), and always returns complex values eigenvalues and eigenvectors. Like for matrices, LinearAlgebra.eigen is type unstable and checks hermiticity at run-time, then falling back to either eig or eigh.

    Orthogonal factorizations

    Other factorizations that are provided by TensorKit.jl are orthogonal or unitary in nature, and thus always require a AbstractEuclideanTensorMap. However, they don't require equal domain and codomain. Let us first discuss the singular value decomposition, for which we define and export the methods tsvd and tsvd! (where as always, the latter destroys the input).

    U, Σ, Vʰ, ϵ = tsvd(t; trunc = notrunc(), p::Real = 2,
                             alg::OrthogonalFactorizationAlgorithm = SDD())

    This computes a (possibly truncated) singular value decomposition of t::TensorMap{S,N₁,N₂} (with InnerProductStyle(t)<:EuclideanProduct), such that norm(t - U*Σ*Vʰ) ≈ ϵ, where U::TensorMap{S,N₁,1}, S::TensorMap{S,1,1}, Vʰ::TensorMap{S,1,N₂} and ϵ::Real. U is an isometry, i.e. U'*U approximates the identity, whereas U*U' is an idempotent (squares to itself). The same holds for adjoint(Vʰ). The domain of U equals the domain and codomain of Σ and the codomain of . In the case of trunc = notrunc() (default value, see below), this space is given by min(fuse(codomain(t)), fuse(domain(t))). The singular values are contained in Σ and are stored on the diagonal of a (collection of) DenseMatrix instance(s), similar to the eigenvalues before.

    The keyword argument trunc provides a way to control the truncation, and is connected to the keyword argument p. The default value notrunc() implies no truncation, and thus ϵ = 0. Other valid options are

    Furthermore, the alg keyword can be either SVD() or SDD() (default), which corresponds to two different algorithms in LAPACK to compute singular value decompositions. The default value SDD() uses a divide-and-conquer algorithm and is typically the fastest, but can loose some accuracy. The SVD() method uses a QR-iteration scheme and can be more accurate, but is typically slower. Since Julia 1.3, these two algorithms are also available in the LinearAlgebra standard library, where they are specified as LinearAlgebra.DivideAndConquer() and LinearAlgebra.QRIteration().

    Note that we defined the new method tsvd (truncated or tensor singular value decomposition), rather than overloading LinearAlgebra.svd. We (will) also support LinearAlgebra.svd(t) as alternative for tsvd(t; trunc = notrunc()), but note that the return values are then given by U, Σ, V = svd(t) with V = adjoint(Vʰ).

    We also define the following pair of orthogonal factorization algorithms, which are useful when one is not interested in truncating a tensor or knowing the singular values, but only in its image or coimage.

    Furthermore, we can compute an orthonormal basis for the orthogonal complement of the image and of the co-image (i.e. the kernel) with the following methods:

    Note that the methods leftorth, rightorth, leftnull and rightnull also come in a form with exclamation mark, i.e. leftorth!, rightorth!, leftnull! and rightnull!, which destroy the input tensor t.

    Factorizations for custom index bipartions

    Finally, note that each of the factorizations take a single argument, the tensor map t, and a number of keyword arguments. They perform the factorization according to the given codomain and domain of the tensor map. In many cases, we want to perform the factorization according to a different bipartition of the indices. When BraidingStyle(sectortype(t)) isa SymmetricBraiding, we can immediately specify an alternative bipartition of the indices of t in all of these methods, in the form

    factorize(t::AbstracTensorMap, pleft::NTuple{N₁′,Int}, pright::NTuple{N₂′,Int}; kwargs...)

    where pleft will be the indices in the codomain of the new tensor map, and pright the indices of the domain. Here, factorize is any of the methods LinearAlgebra.eigen, eig, eigh, tsvd, LinearAlgebra.svd, leftorth, rightorth, leftnull and rightnull. This signature does not allow for the exclamation mark, because it amounts to

    factorize!(permute(t, pleft, pright; copy = true); kwargs...)

    where permute was introduced and discussed in the previous section. When the braiding is not symmetric, the user should manually apply braid to bring the tensor map in proper form before performing the factorization.

    Some examples to conclude this section

    julia> V1 = SU₂Space(0=>2,1/2=>1)Rep[SU₂](0=>2, 1/2=>1)
    julia> V2 = SU₂Space(0=>1,1/2=>1,1=>1)Rep[SU₂](0=>1, 1/2=>1, 1=>1)
    julia> t = randn(V1 ⊗ V1, V2);
    julia> U, S, W = tsvd(t);
    julia> t ≈ U * S * Wtrue
    julia> D, V = eigh(t'*t);
    julia> D ≈ S*Strue
    julia> U'*U ≈ id(domain(U))true
    julia> STensorMap(Rep[SU₂](0=>1, 1/2=>1, 1=>1) ← Rep[SU₂](0=>1, 1/2=>1, 1=>1)): * Data for fusiontree FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()): - 1.7773544148361027 + 2.4955927686884896 * Data for fusiontree FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()): - 2.336069436543462 + 2.456765661199782 * Data for fusiontree FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()): - 0.0806937453351382
    julia> Q, R = leftorth(t; alg = Polar());
    julia> isposdef(R)true
    julia> Q ≈ U*Wtrue
    julia> R ≈ W'*S*Wtrue
    julia> U2, S2, W2, ε = tsvd(t; trunc = truncspace(V1));
    julia> W2*W2' ≈ id(codomain(W2))true
    julia> S2TensorMap(Rep[SU₂](0=>1, 1/2=>1) ← Rep[SU₂](0=>1, 1/2=>1)): + 1.5209820679063877
    julia> Q, R = leftorth(t; alg = Polar());
    julia> isposdef(R)true
    julia> Q ≈ U*Wtrue
    julia> R ≈ W'*S*Wtrue
    julia> U2, S2, W2, ε = tsvd(t; trunc = truncspace(V1));
    julia> W2*W2' ≈ id(codomain(W2))true
    julia> S2TensorMap(Rep[SU₂](0=>1, 1/2=>1) ← Rep[SU₂](0=>1, 1/2=>1)): * Data for fusiontree FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()): - 1.7773544148361027 + 2.4955927686884896 * Data for fusiontree FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()): - 2.336069436543462
    julia> ε ≈ norm(block(S, Irrep[SU₂](1)))*sqrt(dim(Irrep[SU₂](1)))true
    julia> L, Q = rightorth(t, (1,), (2,3));
    julia> codomain(L), domain(L), domain(Q)(ProductSpace(Rep[SU₂](0=>2, 1/2=>1)), ProductSpace(Rep[SU₂](0=>2, 1/2=>1)), (Rep[SU₂](0=>2, 1/2=>1)' ⊗ Rep[SU₂](0=>1, 1/2=>1, 1=>1)))
    julia> Q*Q'TensorMap(Rep[SU₂](0=>2, 1/2=>1) ← Rep[SU₂](0=>2, 1/2=>1)): + 2.456765661199782
    julia> ε ≈ norm(block(S, Irrep[SU₂](1)))*sqrt(dim(Irrep[SU₂](1)))true
    julia> L, Q = rightorth(t, (1,), (2,3));
    julia> codomain(L), domain(L), domain(Q)(ProductSpace(Rep[SU₂](0=>2, 1/2=>1)), ProductSpace(Rep[SU₂](0=>2, 1/2=>1)), (Rep[SU₂](0=>2, 1/2=>1)' ⊗ Rep[SU₂](0=>1, 1/2=>1, 1=>1)))
    julia> Q*Q'TensorMap(Rep[SU₂](0=>2, 1/2=>1) ← Rep[SU₂](0=>2, 1/2=>1)): * Data for fusiontree FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()): - 1.0 2.9662769738393763e-16 - 2.9662769738393763e-16 0.9999999999999996 + 0.9999999999999998 -1.9923845569662841e-16 + -1.9923845569662841e-16 0.9999999999999996 * Data for fusiontree FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()): - 0.9999999999999999
    julia> P = Q'*Q;
    julia> P ≈ P*Ptrue
    julia> t′ = permute(t, (1,), (2,3));
    julia> t′ ≈ t′ * Ptrue

    Bosonic tensor contractions and tensor networks

    One of the most important operation with tensor maps is to compose them, more generally known as contracting them. As mentioned in the section on category theory, a typical composition of maps in a ribbon category can graphically be represented as a planar arrangement of the morphisms (i.e. tensor maps, boxes with lines eminating from top and bottom, corresponding to source and target, i.e. domain and codomain), where the lines connecting the source and targets of the different morphisms should be thought of as ribbons, that can braid over or underneath each other, and that can twist. Technically, we can embed this diagram in $ℝ × [0,1]$ and attach all the unconnected line endings corresponding objects in the source at some position $(x,0)$ for $x∈ℝ$, and all line endings corresponding to objects in the target at some position $(x,1)$. The resulting morphism is then invariant under what is known as framed three-dimensional isotopy, i.e. three-dimensional rearrangements of the morphism that respect the rules of boxes connected by ribbons whose open endings are kept fixed. Such a two-dimensional diagram cannot easily be encoded in a single line of code.

    However, things simplify when the braiding is symmetric (such that over- and under- crossings become equivalent, i.e. just crossings), and when twists, i.e. self-crossings in this case, are trivial. This amounts to BraidingStyle(I) == Bosonic() in the language of TensorKit.jl, and is true for any subcategory of $\mathbf{Vect}$, i.e. ordinary tensors, possibly with some symmetry constraint. The case of $\mathbf{SVect}$ and its subcategories, and more general categories, are discussed below.

    In the case of triival twists, we can deform the diagram such that we first combine every morphism with a number of coevaluations $η$ so as to represent it as a tensor, i.e. with a trivial domain. We can then rearrange the morphism to be all ligned up horizontally, where the original morphism compositions are now being performed by evaluations $ϵ$. This process will generate a number of crossings and twists, where the latter can be omitted because they act trivially. Similarly, double crossings can also be omitted. As a consequence, the diagram, or the morphism it represents, is completely specified by the tensors it is composed of, and which indices between the different tensors are connect, via the evaluation $ϵ$, and which indices make up the source and target of the resulting morphism. If we also compose the resulting morphisms with coevaluations so that it has a trivial domain, we just have one type of unconnected lines, henceforth called open indices. We sketch such a rearrangement in the following picture

    tensor unitary

    Hence, we can now specify such a tensor diagram, henceforth called a tensor contraction or also tensor network, using a one-dimensional syntax that mimicks abstract index notation and specifies which indices are connected by the evaluation map using Einstein's summation conventation. Indeed, for BraidingStyle(I) == Bosonic(), such a tensor contraction can take the same format as if all tensors were just multi-dimensional arrays. For this, we rely on the interface provided by the package TensorOperations.jl.

    The above picture would be encoded as

    @tensor E[a,b,c,d,e] := A[v,w,d,x]*B[y,z,c,x]*C[v,e,y,b]*D[a,w,z]

    or

    @tensor E[:] := A[1,2,-4,3]*B[4,5,-3,3]*C[1,-5,4,-2]*D[-1,2,5]

    where the latter syntax is known as NCON-style, and labels the unconnected or outgoing indices with negative integers, and the contracted indices with positive integers.

    A number of remarks are in order. TensorOperations.jl accepts both integers and any valid variable name as dummy label for indices, and everything in between [ ] is not resolved in the current context but interpreted as a dummy label. Here, we label the indices of a TensorMap, like A::TensorMap{S,N₁,N₂}, in a linear fashion, where the first position corresponds to the first space in codomain(A), and so forth, up to position N₁. Index N₁+1then corresponds to the first space in domain(A). However, because we have applied the coevaluation $η$, it actually corresponds to the corresponding dual space, in accordance with the interface of space(A, i) that we introduced above, and as indiated by the dotted box around $A$ in the above picture. The same holds for the other tensor maps. Note that our convention also requires that we braid indices that we brought from the domain to the codomain, and so this is only unambiguous for a symmetric braiding, where there is a unique way to permute the indices.

    With the current syntax, we create a new object E because we use the definition operator :=. Furthermore, with the current syntax, it will be a Tensor, i.e. it will have a trivial domain, and correspond to the dotted box in the picture above, rather than the actual morphism E. We can also directly define E with the correct codomain and domain by rather using

    @tensor E[a b c;d e] := A[v,w,d,x]*B[y,z,c,x]*C[v,e,y,b]*D[a,w,z]

    or

    @tensor E[(a,b,c);(d,e)] := A[v,w,d,x]*B[y,z,c,x]*C[v,e,y,b]*D[a,w,z]

    where the latter syntax can also be used when the codomain is empty. When using the assignment operator =, the TensorMap E is assumed to exist and the contents will be written to the currently allocated memory. Note that for existing tensors, both on the left hand side and right hand side, trying to specify the indices in the domain and the codomain seperately using the above syntax, has no effect, as the bipartition of indices are already fixed by the existing object. Hence, if E has been created by the previous line of code, all of the following lines are now equivalent

    @tensor E[(a,b,c);(d,e)] = A[v,w,d,x]*B[y,z,c,x]*C[v,e,y,b]*D[a,w,z]
    + 1.0000000000000002
    julia> P = Q'*Q;
    julia> P ≈ P*Ptrue
    julia> t′ = permute(t, (1,), (2,3));
    julia> t′ ≈ t′ * Ptrue

    Bosonic tensor contractions and tensor networks

    One of the most important operation with tensor maps is to compose them, more generally known as contracting them. As mentioned in the section on category theory, a typical composition of maps in a ribbon category can graphically be represented as a planar arrangement of the morphisms (i.e. tensor maps, boxes with lines eminating from top and bottom, corresponding to source and target, i.e. domain and codomain), where the lines connecting the source and targets of the different morphisms should be thought of as ribbons, that can braid over or underneath each other, and that can twist. Technically, we can embed this diagram in $ℝ × [0,1]$ and attach all the unconnected line endings corresponding objects in the source at some position $(x,0)$ for $x∈ℝ$, and all line endings corresponding to objects in the target at some position $(x,1)$. The resulting morphism is then invariant under what is known as framed three-dimensional isotopy, i.e. three-dimensional rearrangements of the morphism that respect the rules of boxes connected by ribbons whose open endings are kept fixed. Such a two-dimensional diagram cannot easily be encoded in a single line of code.

    However, things simplify when the braiding is symmetric (such that over- and under- crossings become equivalent, i.e. just crossings), and when twists, i.e. self-crossings in this case, are trivial. This amounts to BraidingStyle(I) == Bosonic() in the language of TensorKit.jl, and is true for any subcategory of $\mathbf{Vect}$, i.e. ordinary tensors, possibly with some symmetry constraint. The case of $\mathbf{SVect}$ and its subcategories, and more general categories, are discussed below.

    In the case of triival twists, we can deform the diagram such that we first combine every morphism with a number of coevaluations $η$ so as to represent it as a tensor, i.e. with a trivial domain. We can then rearrange the morphism to be all ligned up horizontally, where the original morphism compositions are now being performed by evaluations $ϵ$. This process will generate a number of crossings and twists, where the latter can be omitted because they act trivially. Similarly, double crossings can also be omitted. As a consequence, the diagram, or the morphism it represents, is completely specified by the tensors it is composed of, and which indices between the different tensors are connect, via the evaluation $ϵ$, and which indices make up the source and target of the resulting morphism. If we also compose the resulting morphisms with coevaluations so that it has a trivial domain, we just have one type of unconnected lines, henceforth called open indices. We sketch such a rearrangement in the following picture

    tensor unitary

    Hence, we can now specify such a tensor diagram, henceforth called a tensor contraction or also tensor network, using a one-dimensional syntax that mimicks abstract index notation and specifies which indices are connected by the evaluation map using Einstein's summation conventation. Indeed, for BraidingStyle(I) == Bosonic(), such a tensor contraction can take the same format as if all tensors were just multi-dimensional arrays. For this, we rely on the interface provided by the package TensorOperations.jl.

    The above picture would be encoded as

    @tensor E[a,b,c,d,e] := A[v,w,d,x]*B[y,z,c,x]*C[v,e,y,b]*D[a,w,z]

    or

    @tensor E[:] := A[1,2,-4,3]*B[4,5,-3,3]*C[1,-5,4,-2]*D[-1,2,5]

    where the latter syntax is known as NCON-style, and labels the unconnected or outgoing indices with negative integers, and the contracted indices with positive integers.

    A number of remarks are in order. TensorOperations.jl accepts both integers and any valid variable name as dummy label for indices, and everything in between [ ] is not resolved in the current context but interpreted as a dummy label. Here, we label the indices of a TensorMap, like A::TensorMap{S,N₁,N₂}, in a linear fashion, where the first position corresponds to the first space in codomain(A), and so forth, up to position N₁. Index N₁+1then corresponds to the first space in domain(A). However, because we have applied the coevaluation $η$, it actually corresponds to the corresponding dual space, in accordance with the interface of space(A, i) that we introduced above, and as indiated by the dotted box around $A$ in the above picture. The same holds for the other tensor maps. Note that our convention also requires that we braid indices that we brought from the domain to the codomain, and so this is only unambiguous for a symmetric braiding, where there is a unique way to permute the indices.

    With the current syntax, we create a new object E because we use the definition operator :=. Furthermore, with the current syntax, it will be a Tensor, i.e. it will have a trivial domain, and correspond to the dotted box in the picture above, rather than the actual morphism E. We can also directly define E with the correct codomain and domain by rather using

    @tensor E[a b c;d e] := A[v,w,d,x]*B[y,z,c,x]*C[v,e,y,b]*D[a,w,z]

    or

    @tensor E[(a,b,c);(d,e)] := A[v,w,d,x]*B[y,z,c,x]*C[v,e,y,b]*D[a,w,z]

    where the latter syntax can also be used when the codomain is empty. When using the assignment operator =, the TensorMap E is assumed to exist and the contents will be written to the currently allocated memory. Note that for existing tensors, both on the left hand side and right hand side, trying to specify the indices in the domain and the codomain seperately using the above syntax, has no effect, as the bipartition of indices are already fixed by the existing object. Hence, if E has been created by the previous line of code, all of the following lines are now equivalent

    @tensor E[(a,b,c);(d,e)] = A[v,w,d,x]*B[y,z,c,x]*C[v,e,y,b]*D[a,w,z]
     @tensor E[a,b,c,d,e] = A[v w d;x]*B[(y,z,c);(x,)]*C[v e y; b]*D[a,w,z]
     @tensor E[a b; c d e] = A[v; w d x]*B[y,z,c,x]*C[v,e,y,b]*D[a w;z]

    and none of those will or can change the partition of the indices of E into its codomain and its domain.

    Two final remarks are in order. Firstly, the order of the tensors appearing on the right hand side is irrelevant, as we can reorder them by using the allowed moves of the Penrose graphical calculus, which yields some crossings and a twist. As the latter is trivial, it can be omitted, and we just use the same rules to evaluate the newly ordered tensor network. For the particular case of matrix matrix multiplication, which also captures more general settings by appropriotely combining spaces into a single line, we indeed find

    tensor contraction reorder

    or thus, the following to lines of code yield the same result

    @tensor C[i,j] := B[i,k]*A[k,j]
     @tensor C[i,j] := A[k,j]*B[i,k]

    Reordering of tensors can be used internally by the @tensor macro to evaluate the contraction in a more efficient manner. In particular, the NCON-style of specifying the contraction gives the user control over the order, and there are other macros, such as @tensoropt, that try to automate this process. There is also an @ncon macro and ncon function, an we recommend reading the manual of TensorOperations.jl to learn more about the possibilities and how they work.

    A final remark involves the use of adjoints of tensors. The current framework is such that the user should not be to worried about the actual bipartition into codomain and domain of a given TensorMap instance. Indeed, for factorizations one just specifies the requested bipartition via the factorize(t, pleft, pright) interface, and for tensor contractions the @contract macro figures out the correct manipulations automatically. However, when wanting to use the adjoint of an instance t::TensorMap{S,N₁,N₂}, the resulting adjoint(t) is a AbstractTensorMap{S,N₂,N₁} and one need to know the values of N₁ and N₂ to know exactly where the ith index of t will end up in adjoint(t), and hence to know and understand the index order of t'. Within the @tensor macro, one can instead use conj() on the whole index expression so as to be able to use the original index ordering of t. Indeed, for matrices of thus, TensorMap{S,1,1} instances, this yields exactly the equivalence one expects, namely equivalence between the following to expressions.

    @tensor C[i,j] := B'[i,k]*A[k,j]
    -@tensor C[i,j] := conj(B[k,i])*A[k,j]

    For e.g. an instance A::TensorMap{S,3,2}, the following two syntaxes have the same effect within an @tensor expression: conj(A[a,b,c,d,e]) and A'[d,e,a,b,c].

    Some examples:

    Fermionic tensor contractions

    TODO

    Anyonic tensor contractions

    TODO

    +@tensor C[i,j] := conj(B[k,i])*A[k,j]

    For e.g. an instance A::TensorMap{S,3,2}, the following two syntaxes have the same effect within an @tensor expression: conj(A[a,b,c,d,e]) and A'[d,e,a,b,c].

    Some examples:

    Fermionic tensor contractions

    TODO

    Anyonic tensor contractions

    TODO

    diff --git a/dev/man/tutorial/index.html b/dev/man/tutorial/index.html index ac4c7e46..694e3317 100644 --- a/dev/man/tutorial/index.html +++ b/dev/man/tutorial/index.html @@ -1,652 +1,652 @@ Tutorial · TensorKit.jl

    Tutorial

    Before discussing at length all aspects of this package, both its usage and implementation, we start with a short tutorial to sketch the main capabilities. Thereto, we start by loading TensorKit.jl

    julia> using TensorKit

    Cartesian tensors

    The most important objects in TensorKit.jl are tensors, which we now create with random (normally distributed) entries in the following manner

    julia> A = randn(ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4)TensorMap((ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}()):
     [:, :, 1] =
    - -0.18625067146062688   0.18358118955960148
    - -0.6912531478133187   -1.4600929654091808
    -  0.33348499983532004  -0.1982481380658218
    +  0.37166926815343104   0.3113013692254882
    + -0.2052763482219897   -0.276589731037223
    +  0.5003034362720967    1.4662658852451327
     
     [:, :, 2] =
    - -0.027298703246889152  -1.4944618683605877
    - -0.07753906584873395   -0.04036843257291995
    -  0.5762015562401985     1.321981725171359
    + -0.6377045738851436  3.478572562376228
    + -0.7481252503936721  2.1084762737639133
    + -0.9998411060907221  0.698790036866186
     
     [:, :, 3] =
    -  0.27024648279305913   2.282017221483989
    -  0.4451803282183523   -0.551099248890618
    - -0.5209752808757565    0.17942442276321025
    +  0.40961671403550975   1.1382498614924474
    + -1.5241696418846578   -0.20234726657719546
    + -1.2001597197882345    0.994798147073486
     
     [:, :, 4] =
    -  0.664599349392822    -1.188362835880983
    - -0.22263921638010314  -1.98157572164118
    -  0.9271518349461549   -0.4232941479652183

    Note that we entered the tensor size not as plain dimensions, by specifying the vector space associated with these tensor indices, in this case ℝ^n, which can be obtained by typing \bbR+TAB. The tensor then lives in the tensor product of the different spaces, which we can obtain by typing (i.e. \otimes+TAB), although for simplicity also the usual multiplication sign * does the job. Note also that A is printed as an instance of a parametric type TensorMap, which we will discuss below and contains Tensor.

    Let us briefly sidetrack into the nature of ℝ^n:

    julia> V = ℝ^3ℝ^3
    julia> typeof(V)CartesianSpace
    julia> V == CartesianSpace(3)true
    julia> supertype(CartesianSpace)ElementarySpace
    julia> supertype(ElementarySpace)VectorSpace

    i.e. ℝ^n can also be created without Unicode using the longer syntax CartesianSpace(n). It is subtype of ElementarySpace, with a standard (Euclidean) inner product over the real numbers. Furthermore,

    julia> W = ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4(ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4)
    julia> typeof(W)ProductSpace{CartesianSpace, 3}
    julia> supertype(ProductSpace)CompositeSpace
    julia> supertype(CompositeSpace)VectorSpace

    i.e. the tensor product of a number of CartesianSpaces is some generic parametric ProductSpace type, specifically ProductSpace{CartesianSpace,N} for the tensor product of N instances of CartesianSpace.

    Tensors are itself vectors (but not Vectors or even AbstractArrays), so we can compute linear combinations, provided they live in the same space.

    julia> B = randn(ℝ^3 * ℝ^2 * ℝ^4);
    julia> C = 0.5*A + 2.5*BTensorMap((ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}()): + 0.1634800053481681 -0.5037392676058686 + 1.7681856239175135 -1.7832349230781555 + 1.2262107616377473 -0.8559914027442731

    Note that we entered the tensor size not as plain dimensions, by specifying the vector space associated with these tensor indices, in this case ℝ^n, which can be obtained by typing \bbR+TAB. The tensor then lives in the tensor product of the different spaces, which we can obtain by typing (i.e. \otimes+TAB), although for simplicity also the usual multiplication sign * does the job. Note also that A is printed as an instance of a parametric type TensorMap, which we will discuss below and contains Tensor.

    Let us briefly sidetrack into the nature of ℝ^n:

    julia> V = ℝ^3ℝ^3
    julia> typeof(V)CartesianSpace
    julia> V == CartesianSpace(3)true
    julia> supertype(CartesianSpace)ElementarySpace
    julia> supertype(ElementarySpace)VectorSpace

    i.e. ℝ^n can also be created without Unicode using the longer syntax CartesianSpace(n). It is subtype of ElementarySpace, with a standard (Euclidean) inner product over the real numbers. Furthermore,

    julia> W = ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4(ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4)
    julia> typeof(W)ProductSpace{CartesianSpace, 3}
    julia> supertype(ProductSpace)CompositeSpace
    julia> supertype(CompositeSpace)VectorSpace

    i.e. the tensor product of a number of CartesianSpaces is some generic parametric ProductSpace type, specifically ProductSpace{CartesianSpace,N} for the tensor product of N instances of CartesianSpace.

    Tensors are itself vectors (but not Vectors or even AbstractArrays), so we can compute linear combinations, provided they live in the same space.

    julia> B = randn(ℝ^3 * ℝ^2 * ℝ^4);
    julia> C = 0.5*A + 2.5*BTensorMap((ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}()): [:, :, 1] = - -3.50055025663506 1.778589872350755 - -0.6202203818466181 0.7626563759254427 - -4.057817206130052 -0.21271408238589445 + 0.9727877794257899 -2.510931173154135 + 1.1805475708105138 -0.34104540243422754 + 0.7779572818040328 0.44347007357390983 [:, :, 2] = - 0.0011455215353738217 -1.4090653418124037 - 4.098112721323191 -2.0081285177150554 - -0.7213837950076711 -0.10401020970574981 + -1.2897858969457174 0.8856347510417409 + -2.374855527693809 1.8791082982942888 + 1.1908521190191699 -0.09318324552060281 [:, :, 3] = - -4.370101871381916 2.640181522251796 - -1.2053336269118669 -3.868215990030047 - 3.820716299633353 -4.307028611744844 + -3.8501788293698187 0.20524305337602067 + -1.957450895819338 1.3091637397859577 + -8.342724707677483 4.286871064257394 [:, :, 4] = - -1.430260476716025 -1.439708932635976 - -5.074553880575435 -1.3482935817402284 - -1.731132108547596 -1.7535657998498955

    Given that they are behave as vectors, they also have a scalar product and norm, which they inherit from the Euclidean inner product on the individual ℝ^n spaces:

    julia> scalarBA = dot(B,A)-0.9516202992957803
    julia> scalarAA = dot(A,A)20.105093225574716
    julia> normA² = norm(A)^220.10509322557472

    More generally, our tensor objects implement the full interface layed out in VectorInterface.jl.

    If two tensors live on different spaces, these operations have no meaning and are thus not allowed

    julia> B′ = randn(ℝ^4 * ℝ^2 * ℝ^3);
    julia> space(B′) == space(A)false
    julia> C′ = 0.5*A + 2.5*B′ERROR: SpaceMismatch("(ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}() ≠ (ℝ^4 ⊗ ℝ^2 ⊗ ℝ^3) ← ProductSpace{CartesianSpace, 0}()")
    julia> scalarBA′ = dot(B′,A)ERROR: SpaceMismatch("(ℝ^4 ⊗ ℝ^2 ⊗ ℝ^3) ← ProductSpace{CartesianSpace, 0}() ≠ (ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}()")

    However, in this particular case, we can reorder the indices of B′ to match space of A, using the routine permute (we deliberately choose not to overload permutedims from Julia Base, for reasons that become clear below):

    julia> space(permute(B′,(3,2,1))) == space(A)true

    We can contract two tensors using Einstein summation convention, which takes the interface from TensorOperations.jl. TensorKit.jl reexports the @tensor macro

    julia> @tensor D[a,b,c,d] := A[a,b,e]*B[d,c,e]TensorMap((ℝ^3 ⊗ ℝ^2 ⊗ ℝ^2 ⊗ ℝ^3) ← ProductSpace{CartesianSpace, 0}()):
    + -1.6984688701862984   1.323979251863238
    +  2.516178560806458   -5.36636226498341
    +  1.9708253082850833  -1.6122204252903392

    Given that they are behave as vectors, they also have a scalar product and norm, which they inherit from the Euclidean inner product on the individual ℝ^n spaces:

    julia> scalarBA = dot(B,A)9.18518144677017
    julia> scalarAA = dot(A,A)36.8350566425113
    julia> normA² = norm(A)^236.8350566425113

    More generally, our tensor objects implement the full interface layed out in VectorInterface.jl.

    If two tensors live on different spaces, these operations have no meaning and are thus not allowed

    julia> B′ = randn(ℝ^4 * ℝ^2 * ℝ^3);
    julia> space(B′) == space(A)false
    julia> C′ = 0.5*A + 2.5*B′ERROR: SpaceMismatch("(ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}() ≠ (ℝ^4 ⊗ ℝ^2 ⊗ ℝ^3) ← ProductSpace{CartesianSpace, 0}()")
    julia> scalarBA′ = dot(B′,A)ERROR: SpaceMismatch("(ℝ^4 ⊗ ℝ^2 ⊗ ℝ^3) ← ProductSpace{CartesianSpace, 0}() ≠ (ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}()")

    However, in this particular case, we can reorder the indices of B′ to match space of A, using the routine permute (we deliberately choose not to overload permutedims from Julia Base, for reasons that become clear below):

    julia> space(permute(B′,(3,2,1))) == space(A)true

    We can contract two tensors using Einstein summation convention, which takes the interface from TensorOperations.jl. TensorKit.jl reexports the @tensor macro

    julia> @tensor D[a,b,c,d] := A[a,b,e]*B[d,c,e]TensorMap((ℝ^3 ⊗ ℝ^2 ⊗ ℝ^2 ⊗ ℝ^3) ← ProductSpace{CartesianSpace, 0}()):
     [:, :, 1, 1] =
    - -0.7018745088782049  -3.5336359232708285
    -  0.2964093753591434   4.3800129964159416
    - -0.165940092118019    0.2531232721446457
    + -0.41614559166379816  -2.740524365376608
    +  1.4390342853445055    0.692077026690695
    +  1.6192862819459466   -0.8138585727938167
     
     [:, :, 2, 1] =
    - -0.18115644268022232   2.2898731122106173
    - -0.10361496661367796  -0.6347531247878949
    - -0.5535168697001956   -0.23297708994576596
    + -0.13525654276715318  -2.003041933644632
    +  1.8108142026232437   -1.5195332406873132
    +  0.7553820231501354   -2.4869373231979632
     
     [:, :, 1, 2] =
    - -1.4984967561798912   -1.437298627112411
    -  0.13534834229807738   4.342354119462558
    - -0.6262614751716492    2.9472096283907216
    + 0.6120027821315158  -3.497286705285766
    + 2.3764828990951776  -2.8968214442726605
    + 2.4313474195488185  -0.8411367959213714
     
     [:, :, 2, 2] =
    - -0.5729008507322395  -1.811496353439539
    - -0.9589927215755835   0.2356422578678291
    -  0.3570262784445715  -1.3668934435410214
    + -0.302085264970263   2.6662700723314816
    + -4.254903658894499   3.795776469487194
    + -3.2423144899738774  2.2049875766858618
     
     [:, :, 1, 3] =
    -  0.18348567433436141  5.061829453759784
    -  2.1216057881532535   3.323533173274389
    - -2.4606111269593156   0.46570626535503945
    + -1.532640415315708   -1.3804876986145835
    +  5.131419741844031    1.0258151580366737
    +  3.8123280703544995  -2.7636667773501453
     
     [:, :, 2, 3] =
    - -0.8683688686865358   -2.831466969994214
    - -0.5904856092289589    2.270081260302662
    -  0.15293005411058344  -0.44999837583478375
    julia> @tensor d = A[a,b,c]*A[a,b,c]20.105093225574716
    julia> d ≈ scalarAA ≈ normA²true

    We hope that the index convention is clear. The := is to create a new tensor D, without the : the result would be written in an existing tensor D, which in this case would yield an error as no tensor D exists. If the contraction yields a scalar, regular assignment with = can be used.

    Finally, we can factorize a tensor, creating a bipartition of a subset of its indices and its complement. With a plain Julia Array, one would apply permutedims and reshape to cast the array into a matrix before applying e.g. the singular value decomposition. With TensorKit.jl, one just specifies which indices go to the left (rows) and right (columns)

    julia> U, S, Vd = tsvd(A, (1,3), (2,));
    julia> @tensor A′[a,b,c] := U[a,c,d]*S[d,e]*Vd[e,b];
    julia> A ≈ A′true
    julia> UTensorMap((ℝ^3 ⊗ ℝ^4) ← ℝ^2): + 0.6132837588975579 1.3120773640930188 + -2.9916649555342083 0.1967654366328028 + -2.280996852390781 1.6197815331417282
    julia> @tensor d = A[a,b,c]*A[a,b,c]36.83505664251129
    julia> d ≈ scalarAA ≈ normA²true

    We hope that the index convention is clear. The := is to create a new tensor D, without the : the result would be written in an existing tensor D, which in this case would yield an error as no tensor D exists. If the contraction yields a scalar, regular assignment with = can be used.

    Finally, we can factorize a tensor, creating a bipartition of a subset of its indices and its complement. With a plain Julia Array, one would apply permutedims and reshape to cast the array into a matrix before applying e.g. the singular value decomposition. With TensorKit.jl, one just specifies which indices go to the left (rows) and right (columns)

    julia> U, S, Vd = tsvd(A, (1,3), (2,));
    julia> @tensor A′[a,b,c] := U[a,c,d]*S[d,e]*Vd[e,b];
    julia> A ≈ A′true
    julia> UTensorMap((ℝ^3 ⊗ ℝ^4) ← ℝ^2): [:, :, 1] = - 0.040057535671312774 -0.3579473148757397 … -0.2704676682524183 - -0.36343114110624863 -0.011253921312390022 -0.47846663045818016 - -0.04052633445267537 0.32802892756081 -0.08209141956873055 + 0.02440783412744132 0.6320927824178592 … -0.09687019156399838 + -0.03105850663051166 0.410254377002975 -0.4321398103650496 + 0.20874662758362145 0.192364749697586 -0.23574780248314703 [:, :, 2] = - 0.12146018301809851 -0.06110812897535916 … -0.46106775351489687 - 0.33971657765790436 0.044507780212400656 0.031027456753629678 - -0.21070996640591266 -0.2777331944610234 -0.579181300382481

    Note that the tsvd routine returns the decomposition of the linear map as three factors, U, S and Vd, each of them a TensorMap, such that Vd is already what is commonly called V'. Furthermore, observe that U is printed differently then A, i.e. as a TensorMap((ℝ^3 ⊗ ℝ^4) ← ProductSpace(ℝ^2)). What this means is that tensors (or more appropriately, TensorMap instances) in TensorKit.jl are always considered to be linear maps between two ProductSpace instances, with

    julia> codomain(U)(ℝ^3 ⊗ ℝ^4)
    julia> domain(U)ProductSpace(ℝ^2)
    julia> codomain(A)(ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4)
    julia> domain(A)ProductSpace{CartesianSpace, 0}()

    An instance of TensorMap thus represents a linear map from its domain to its codomain, making it an element of the space of homomorphisms between these two spaces. That space is represented using its own type HomSpace in TensorKit.jl, and which admits a direct constructor as well as a unicode alternative using the symbol (obtained as \to+TAB or \rightarrow+TAB) or (obtained as \leftarrow+TAB).

    julia> P = space(U)(ℝ^3 ⊗ ℝ^4) ← ℝ^2
    julia> space(U) == HomSpace(ℝ^3 ⊗ ℝ^4, ℝ^2) == (ℝ^3 ⊗ ℝ^4 ← ℝ^2) == ℝ^2 → ℝ^3 ⊗ ℝ^4ERROR: MethodError: no method matching HomSpace(::ProductSpace{CartesianSpace, 2}, ::CartesianSpace) + 0.1728084976491283 0.30988007529932565 … -0.02080440002939668 + -0.11125876592523463 0.06547254372046697 0.3288459353442603 + 0.3908693177987496 -0.23272930315520682 0.2855729480249023

    Note that the tsvd routine returns the decomposition of the linear map as three factors, U, S and Vd, each of them a TensorMap, such that Vd is already what is commonly called V'. Furthermore, observe that U is printed differently then A, i.e. as a TensorMap((ℝ^3 ⊗ ℝ^4) ← ProductSpace(ℝ^2)). What this means is that tensors (or more appropriately, TensorMap instances) in TensorKit.jl are always considered to be linear maps between two ProductSpace instances, with

    julia> codomain(U)(ℝ^3 ⊗ ℝ^4)
    julia> domain(U)ProductSpace(ℝ^2)
    julia> codomain(A)(ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4)
    julia> domain(A)ProductSpace{CartesianSpace, 0}()

    An instance of TensorMap thus represents a linear map from its domain to its codomain, making it an element of the space of homomorphisms between these two spaces. That space is represented using its own type HomSpace in TensorKit.jl, and which admits a direct constructor as well as a unicode alternative using the symbol (obtained as \to+TAB or \rightarrow+TAB) or (obtained as \leftarrow+TAB).

    julia> P = space(U)(ℝ^3 ⊗ ℝ^4) ← ℝ^2
    julia> space(U) == HomSpace(ℝ^3 ⊗ ℝ^4, ℝ^2) == (ℝ^3 ⊗ ℝ^4 ← ℝ^2) == ℝ^2 → ℝ^3 ⊗ ℝ^4ERROR: MethodError: no method matching HomSpace(::ProductSpace{CartesianSpace, 2}, ::CartesianSpace) The type `HomSpace` exists, but no method is defined for this combination of argument types when trying to construct it. Closest candidates are: HomSpace(::P1, ::P2) where {S<:ElementarySpace, P1<:CompositeSpace{S}, P2<:CompositeSpace{S}} @ TensorKit ~/work/TensorKit.jl/TensorKit.jl/src/spaces/homspace.jl:12
    julia> (codomain(P), domain(P))((ℝ^3 ⊗ ℝ^4), ProductSpace(ℝ^2))

    Furthermore, a Tensor instance such as A is just a specific case of TensorMap with an empty domain, i.e. a ProductSpace{CartesianSpace,0} instance. Analogously, we can represent a vector v and matrix m as

    julia> v = randn(ℝ^3)TensorMap(ℝ^3 ← ProductSpace{CartesianSpace, 0}()):
    -  1.4711037337813315
    -  0.9642893344470368
    - -1.6297614264252664
    julia> M₁ = randn(ℝ^4, ℝ^3)TensorMap(ℝ^4 ← ℝ^3): - 0.9628730054365805 3.0430484283029537 -1.0266662616135547 - 1.6256832312712384 1.4255020898248867 -0.416191510159061 - -1.0109378869036625 -0.06574520586089931 -1.0765195127328842 - -1.0846260591558916 0.6134233862885026 -0.5137568291122526
    julia> M₂ = randn(ℝ^4 → ℝ^2) # alternative syntax for randn(ℝ^2, ℝ^4)TensorMap(ℝ^2 ← ℝ^4): - -0.7284051628529761 0.38059342266947627 … 1.1678166420436842 - 0.5959362891621706 0.6674074394651356 0.9901518496749971
    julia> w = M₁ * v # matrix vector productTensorMap(ℝ^4 ← ProductSpace{CartesianSpace, 0}()): - 6.024086288063366 - 4.444438002181835 - 0.20387807579850067 - -0.16677875383266919
    julia> M₃ = M₂ * M₁ # matrix matrix productTensorMap(ℝ^2 ← ℝ^3): - -0.694641951662551 -0.9150956703504376 0.6865630426825979 - 0.4805264522455829 3.3654508030017105 -1.5093956677742968
    julia> space(M₃)ℝ^2 ← ℝ^3

    Note that for the construction of M₁, in accordance with how one specifies the dimensions of a matrix (e.g. randn(4,3)), the first space is the codomain and the second the domain. This is somewhat opposite to the general notation for a function f:domain→codomain, so that we also support this more mathemical notation, as illustrated in the construction of M₂. However, as this is confusing from the perspective of rows and columns, we also support the syntax codomain ← domain and actually use this as the default way of printing HomSpace instances.

    The 'matrix vector' or 'matrix matrix' product can be computed between any two TensorMap instances for which the domain of the first matches with the codomain of the second, e.g.

    julia> v′ = v ⊗ vTensorMap((ℝ^3 ⊗ ℝ^3) ← ProductSpace{CartesianSpace, 0}()):
    -  2.1641461955453747   1.418569640350551   -2.397548119586998
    -  1.418569640350551    0.9298539205283092  -1.5715615611950735
    - -2.397548119586998   -1.5715615611950735   2.656122307063719
    julia> M₁′ = M₁ ⊗ M₁TensorMap((ℝ^4 ⊗ ℝ^4) ← (ℝ^3 ⊗ ℝ^3)): + 0.8760096288124156 + 0.9679992453618557 + -1.7783850463214066
    julia> M₁ = randn(ℝ^4, ℝ^3)TensorMap(ℝ^4 ← ℝ^3): + -1.3414765984689572 -0.09711359836334296 1.0969761502067803 + -1.1120851673647192 0.28824620593743633 2.0833663141614807 + 1.4896887972465733 0.12975818573084616 0.6086035093688841 + -0.8756892604969615 0.11511661893439118 0.4551951568099825
    julia> M₂ = randn(ℝ^4 → ℝ^2) # alternative syntax for randn(ℝ^2, ℝ^4)TensorMap(ℝ^2 ← ℝ^4): + 0.5511731115669959 0.04979143279169657 … 1.1172656645436456 + -0.09229210203339167 2.764080368060021 -0.8281132832567961
    julia> w = M₁ * v # matrix vector productTensorMap(ℝ^4 ← ProductSpace{CartesianSpace, 0}()): + -3.219998288714387 + -4.400202703959628 + 0.34825617598861364 + -1.4651916838146637
    julia> M₃ = M₂ * M₁ # matrix matrix productTensorMap(ℝ^2 ← ℝ^3): + -0.12144478884986185 0.2333108861849941 1.8917199771408606 + -4.006328205548735 0.5552002973575546 4.5526108257959175
    julia> space(M₃)ℝ^2 ← ℝ^3

    Note that for the construction of M₁, in accordance with how one specifies the dimensions of a matrix (e.g. randn(4,3)), the first space is the codomain and the second the domain. This is somewhat opposite to the general notation for a function f:domain→codomain, so that we also support this more mathemical notation, as illustrated in the construction of M₂. However, as this is confusing from the perspective of rows and columns, we also support the syntax codomain ← domain and actually use this as the default way of printing HomSpace instances.

    The 'matrix vector' or 'matrix matrix' product can be computed between any two TensorMap instances for which the domain of the first matches with the codomain of the second, e.g.

    julia> v′ = v ⊗ vTensorMap((ℝ^3 ⊗ ℝ^3) ← ProductSpace{CartesianSpace, 0}()):
    +  0.7673928697720662   0.8479766596201376  -1.557882424313566
    +  0.8479766596201376   0.9370225390211221  -1.7214753828019302
    + -1.557882424313566   -1.7214753828019302   3.1626533729795914
    julia> M₁′ = M₁ ⊗ M₁TensorMap((ℝ^4 ⊗ ℝ^4) ← (ℝ^3 ⊗ ℝ^3)): [:, :, 1, 1] = - 0.9271244245984732 1.5653264987819888 … -1.0443571533542677 - 1.5653264987819888 2.642845968436495 -1.7632583965695392 - -0.9734048014726354 -1.6434647705960637 1.096489576323704 - -1.0443571533542677 -1.7632583965695392 1.1764136882000398 + 1.7995594642398438 1.4918362275242045 … 1.1747166504872604 + 1.4918362275242045 1.2367334194726154 0.9738410378192506 + -1.9983826605076451 -1.6566608154073028 -1.30450448123146 + 1.1747166504872604 0.9738410378192506 0.7668316809497153 [:, :, 2, 1] = - 2.9300691858491277 4.947032801838409 … -3.3005696246107625 - 1.372577481485815 2.317414843570425 -1.5461367140052547 - -0.0633042839603308 -0.10688087870453955 0.07130896354130005 - 0.5906488195606949 0.9972321127588379 -0.6653349900641607 + 0.1302756195975378 0.10799859228928838 … 0.08504133513499473 + -0.3866755398625346 -0.3205543301721792 -0.2524141069184085 + -0.1740675696177187 -0.14430215369543037 -0.11362784970607205 + -0.15442625039535424 -0.128019484434113 -0.10080638690556752 [:, :, 3, 1] = - -0.988549228900182 -1.6690341256170862 … 1.1135489814022215 - -0.40073957022404416 -0.6765955590630387 0.45141215751796154 - -1.0365515786362354 -1.7500797199861342 1.1676211166998889 - -0.4946825821108824 -0.8352058620388723 0.5572340449244494 + -1.4715678345809635 -1.2199309055978127 … -0.9606102337573792 + -2.794787156486152 -2.3168807761662884 -1.8243815069923475 + -0.8164273655644407 -0.6768189355752509 -0.5329475570550937 + -0.6106336505969989 -0.506215782144639 -0.39860951024873204 [:, :, 1, 2] = - 2.9300691858491277 1.372577481485815 … 0.5906488195606949 - 4.947032801838409 2.317414843570425 0.9972321127588379 - -3.0763329478540995 -1.4410940704643258 -0.6201329419117879 - -3.3005696246107625 -1.5461367140052547 -0.6653349900641607 + 0.1302756195975378 -0.3866755398625346 … -0.15442625039535424 + 0.10799859228928838 -0.3205543301721792 -0.128019484434113 + -0.14466903954217517 0.4293971438338276 0.1714879376034653 + 0.08504133513499473 -0.2524141069184085 -0.10080638690556752 [:, :, 2, 2] = - 9.260143736997078 4.337871893984198 … 1.8666770715295034 - 4.337871893984198 2.032056208095119 0.8744363191017192 - -0.20006584536346378 -0.09371992835067935 -0.04032964681142756 - 1.8666770715295034 0.8744363191017192 0.3762882508456534 + 0.00943105098707669 -0.027992626273165636 … -0.011179389096140467 + -0.027992626273165636 0.08308587523732695 0.0331819286481839 + -0.012601284333421454 0.03740230472624159 0.014937323620395772 + -0.011179389096140467 0.0331819286481839 0.01325183595488583 [:, :, 3, 2] = - -3.1241951537947967 -1.463514901482826 … -0.6297810947871444 - -1.2664909208625632 -0.5932818674991169 -0.2553016055062969 - -3.275901011259265 -1.5345808151379952 -0.6603622449062545 - -1.5633869113599494 -0.7323614335613233 -0.31515045384288154 + -0.10653130126534745 0.3161992133009597 … 0.1262801854634694 + -0.20232319947719624 0.6005224356349078 0.23983008608807427 + -0.05910367677137084 0.17542765249578982 0.070060378270151 + -0.04420563963538356 0.13120827691157383 0.052400527407275195 [:, :, 1, 3] = - -0.988549228900182 -0.40073957022404416 … -0.4946825821108824 - -1.6690341256170862 -0.6765955590630387 -0.8352058620388723 - 1.0378958210708897 0.42074376582744527 0.5193762432050667 - 1.1135489814022215 0.45141215751796154 0.5572340449244494 + -1.4715678345809635 -2.794787156486152 … -0.6106336505969989 + -1.2199309055978127 -2.3168807761662884 -0.506215782144639 + 1.634153081809715 3.103567458767243 0.6780991256607282 + -0.9606102337573792 -1.8243815069923475 -0.39860951024873204 [:, :, 2, 3] = - -3.1241951537947967 -1.2664909208625632 … -1.5633869113599494 - -1.463514901482826 -0.5932818674991169 -0.7323614335613233 - 0.06749838472022306 0.02736259651296603 0.033777048492427915 - -0.6297810947871444 -0.2553016055062969 -0.31515045384288154 + -0.10653130126534745 -0.20232319947719624 … -0.04420563963538356 + 0.3161992133009597 0.6005224356349078 0.13120827691157383 + 0.14234163504084 0.2703338331383538 0.05906529770113135 + 0.1262801854634694 0.23983008608807427 0.052400527407275195 [:, :, 3, 3] = - 1.054043612735552 0.4272897818503029 … 0.5274568031231103 - 0.4272897818503029 0.17321537312847976 0.21382123056275903 - 1.1052262636915156 0.44803828171999555 0.5530692513391138 - 0.5274568031231103 0.21382123056275903 0.26394607945947635
    julia> w′ = M₁′ * v′TensorMap((ℝ^4 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}()): - 36.28961560603305 26.773678027091336 … -1.0046896041036777 - 26.773678027091336 19.753029155238067 -0.7412378314904453 - 1.2281791208544917 0.9061234678905667 -0.034002531415477044 - -1.0046896041036781 -0.7412378314904449 0.027815152729978365
    julia> w′ ≈ w ⊗ wtrue

    Another example involves checking that U from the singular value decomposition is a unitary, or at least a (left) isometric tensor

    julia> codomain(U)(ℝ^3 ⊗ ℝ^4)
    julia> domain(U)ProductSpace(ℝ^2)
    julia> space(U)(ℝ^3 ⊗ ℝ^4) ← ℝ^2
    julia> U'*U # should be the identity on the corresponding domain = codomainTensorMap(ℝ^2 ← ℝ^2): - 1.0000000000000004 -3.518923006507086e-17 - -3.518923006507086e-17 1.0000000000000002
    julia> U'*U ≈ one(U'*U)true
    julia> P = U*U' # should be a projectorTensorMap((ℝ^3 ⊗ ℝ^4) ← (ℝ^3 ⊗ ℝ^4)): + 1.2033566741224886 2.2854031587793506 … 0.4993382307101863 + 2.2854031587793506 4.340415198982793 0.9483382560673704 + 0.6676235347098146 1.2679440500995942 0.27703336988227484 + 0.4993382307101863 0.9483382560673704 0.20720263078326456
    julia> w′ = M₁′ * v′TensorMap((ℝ^4 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}()): + 10.36838897932358 14.168645176746416 … 4.717914714521768 + 14.168645176746416 19.361783835933615 6.4471404089404425 + -1.1213842907175526 -1.532397767255738 -0.5102620528956127 + 4.717914714521768 6.447140408940443 2.146786670319649
    julia> w′ ≈ w ⊗ wtrue

    Another example involves checking that U from the singular value decomposition is a unitary, or at least a (left) isometric tensor

    julia> codomain(U)(ℝ^3 ⊗ ℝ^4)
    julia> domain(U)ProductSpace(ℝ^2)
    julia> space(U)(ℝ^3 ⊗ ℝ^4) ← ℝ^2
    julia> U'*U # should be the identity on the corresponding domain = codomainTensorMap(ℝ^2 ← ℝ^2): + 1.0000000000000002 6.805435406336479e-17 + 6.805435406336479e-17 1.0000000000000002
    julia> U'*U ≈ one(U'*U)true
    julia> P = U*U' # should be a projectorTensorMap((ℝ^3 ⊗ ℝ^4) ← (ℝ^3 ⊗ ℝ^4)): [:, :, 1, 1] = - 0.01635718222284848 -0.02176069186332626 … -0.06683564199462094 - 0.026703881797681676 0.004955118775914277 -0.015397593541229384 - -0.027216256171365135 -0.020593494162447427 -0.07363584671279955 + 0.030458519226541473 0.06897792605028319 … -0.005959568681159059 + -0.019984531067919392 0.02132763270154616 0.04627977523360993 + 0.0726405926466654 -0.03552239433459438 0.04359533885849852 [:, :, 2, 1] = - 0.026703881797681676 0.10932975665862095 … -0.05833598598717629 - 0.24748954746138896 0.019210056237388785 0.18443021491044992 - -0.05685313669083936 -0.21356649778358025 -0.16692291092049555 + -0.019984531067919392 -0.054108732636445396 … 0.0053233153600749684 + 0.013343143829323688 -0.020026282704668455 -0.023165375780400958 + -0.04997099645323009 0.019918613209728934 -0.024450519092330425 [:, :, 3, 1] = - -0.027216256171365135 0.027382384402617435 … 0.10811263403620629 - -0.05685313669083936 -0.008922152694346677 0.012852704320174622 - 0.046041073726950946 0.04522734204620724 0.12536613667166263 + 0.0726405926466654 0.2530698502813485 … -0.028353127449066606 + -0.04997099645323009 0.1112304261493584 0.03832805835057296 + 0.19635397812399508 -0.05081125119071554 0.0624101446476647 [:, :, 1, 2] = - -0.02176069186332626 0.1318604836536211 … 0.12498816335982292 - 0.10932975665862095 0.0013085237419665516 0.16936981580107022 - 0.027382384402617435 -0.10044531795409775 0.06477708881285374 + 0.06897792605028319 0.4955669466522668 … -0.06767781796670551 + -0.054108732636445396 0.27960746743705256 -0.17124965192081368 + 0.2530698502813485 0.04947419588936708 -0.06052111778306117 [:, :, 2, 2] = - 0.004955118775914277 0.0013085237419665516 … -0.017477280380408026 - 0.019210056237388785 0.0021075932443408297 0.00676558903552107 - -0.008922152694346677 -0.016052899715716204 -0.024854223644306978 + 0.02132763270154616 0.27960746743705256 … -0.041103537080749764 + -0.020026282704668455 0.17259530783132762 -0.15575686880037226 + 0.1112304261493584 0.06368110106865327 -0.07801938051260082 [:, :, 3, 2] = - -0.020593494162447427 -0.10044531795409775 … 0.03933260088994634 - -0.21356649778358025 -0.016052899715716204 -0.16556825034301803 - 0.04522734204620724 0.18473870462221975 0.1339295124042408 + -0.03552239433459438 0.04947419588936708 … -0.013792616631962101 + 0.019918613209728934 0.06368110106865327 -0.15966055183332695 + -0.05081125119071554 0.09116712547332306 -0.11181076021024013 [:, :, 1, 3] = - 0.016732465815993587 -0.19464285472888532 … -0.12879482416824092 - -0.21532499757241447 -0.008165102772230886 -0.26514388935438016 - -0.013058880809139315 0.19307711463630806 -0.019740009185977552 + 0.05759314474237652 0.1976758706504196 … -0.02200548994818354 + -0.039542059778382294 0.0861642424364737 0.03280504485894555 + 0.1549040938174539 -0.04142306178620379 0.05087600280204527 [:, :, 2, 3] = - -0.040880515606788605 0.061981554530164844 … 0.16970104941485553 - -0.0560469433029534 -0.011800851678183337 0.04947224263986205 - 0.06736855439186162 0.04203145914211003 0.18158414679012821 + -0.09244928777134845 -0.11851459060099898 … 0.0035835815760718415 + 0.058288613632026115 -0.0028100894466001444 -0.21437554403239553 + -0.19677251321619574 0.14260101422182464 -0.17495634806847596 [:, :, 3, 3] = - 0.04044887115716632 -0.031212229580570544 … -0.15735299238458558 - 0.0978440591989417 0.013988182115523481 -0.005381515533133727 - -0.06923957606171627 -0.07899794707119519 -0.18937747051048467 + -0.037925838124153566 0.08325421150452536 … -0.01958468295943851 + 0.02047259384730449 0.08873286593538489 -0.19521929552063597 + -0.046306920164230415 0.10898639193084836 -0.1336569545232964 [:, :, 1, 4] = - -0.06683564199462094 0.12498816335982292 … 0.2857362329011739 - -0.05833598598717629 -0.017477280380408026 0.11510399409393884 - 0.10811263403620629 0.03933260088994634 0.28924489587947266 + -0.005959568681159059 -0.06767781796670551 … 0.009816657074228904 + 0.0053233153600749684 -0.041103537080749764 0.03502002382554918 + -0.028353127449066606 -0.013792616631962101 0.016895760939049934 [:, :, 2, 4] = - -0.015397593541229384 0.16936981580107022 … 0.11510399409393884 - 0.18443021491044992 0.00676558903552107 0.2298930195346031 - 0.012852704320174622 -0.16556825034301803 0.021307482160450794 + 0.04627977523360993 -0.17124965192081368 … 0.03502002382554918 + -0.023165375780400958 -0.15575686880037226 0.2948844648947825 + 0.03832805835057296 -0.15966055183332695 0.19578551386131116 [:, :, 3, 4] = - -0.07363584671279955 0.06477708881285374 … 0.28924489587947266 - -0.16692291092049555 -0.024854223644306978 0.021307482160450794 - 0.12536613667166263 0.1339295124042408 0.34218997987955097
    julia> P*P ≈ Ptrue

    Here, the adjoint of a TensorMap results in a new tensor map (actually a simple wrapper of type AdjointTensorMap <: AbstractTensorMap) with domain and codomain interchanged.

    Our original tensor A living in ℝ^4 * ℝ^2 * ℝ^3 is isomorphic to e.g. a linear map ℝ^3 → ℝ^4 * ℝ^2. This is where the full power of permute emerges. It allows to specify a permutation where some indices go to the codomain, and others go to the domain, as in

    julia> A2 = permute(A,(1,2),(3,))TensorMap((ℝ^3 ⊗ ℝ^2) ← ℝ^4):
    +  0.04359533885849852   -0.06052111778306117  …  0.016895760939049934
    + -0.024450519092330425  -0.07801938051260082     0.19578551386131116
    +  0.0624101446476647    -0.11181076021024013     0.13712893501926646
    julia> P*P ≈ Ptrue

    Here, the adjoint of a TensorMap results in a new tensor map (actually a simple wrapper of type AdjointTensorMap <: AbstractTensorMap) with domain and codomain interchanged.

    Our original tensor A living in ℝ^4 * ℝ^2 * ℝ^3 is isomorphic to e.g. a linear map ℝ^3 → ℝ^4 * ℝ^2. This is where the full power of permute emerges. It allows to specify a permutation where some indices go to the codomain, and others go to the domain, as in

    julia> A2 = permute(A,(1,2),(3,))TensorMap((ℝ^3 ⊗ ℝ^2) ← ℝ^4):
     [:, :, 1] =
    - -0.18625067146062688   0.18358118955960148
    - -0.6912531478133187   -1.4600929654091808
    -  0.33348499983532004  -0.1982481380658218
    +  0.37166926815343104   0.3113013692254882
    + -0.2052763482219897   -0.276589731037223
    +  0.5003034362720967    1.4662658852451327
     
     [:, :, 2] =
    - -0.027298703246889152  -1.4944618683605877
    - -0.07753906584873395   -0.04036843257291995
    -  0.5762015562401985     1.321981725171359
    + -0.6377045738851436  3.478572562376228
    + -0.7481252503936721  2.1084762737639133
    + -0.9998411060907221  0.698790036866186
     
     [:, :, 3] =
    -  0.27024648279305913   2.282017221483989
    -  0.4451803282183523   -0.551099248890618
    - -0.5209752808757565    0.17942442276321025
    +  0.40961671403550975   1.1382498614924474
    + -1.5241696418846578   -0.20234726657719546
    + -1.2001597197882345    0.994798147073486
     
     [:, :, 4] =
    -  0.664599349392822    -1.188362835880983
    - -0.22263921638010314  -1.98157572164118
    -  0.9271518349461549   -0.4232941479652183
    julia> codomain(A2)(ℝ^3 ⊗ ℝ^2)
    julia> domain(A2)ProductSpace(ℝ^4)

    In fact, tsvd(A, (1,3),(2,)) is a shorthand for tsvd(permute(A,(1,3),(2,))), where tsvd(A::TensorMap) will just compute the singular value decomposition according to the given codomain and domain of A.

    Note, finally, that the @tensor macro treats all indices at the same footing and thus does not distinguish between codomain and domain. The linear numbering is first all indices in the codomain, followed by all indices in the domain. However, when @tensor creates a new tensor (i.e. when using :=), the default syntax always creates a Tensor, i.e. with all indices in the codomain.

    julia> @tensor A′[a,b,c] := U[a,c,d]*S[d,e]*Vd[e,b];
    julia> codomain(A′)(ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4)
    julia> domain(A′)ProductSpace{CartesianSpace, 0}()
    julia> @tensor A2′[(a,b);(c,)] := U[a,c,d]*S[d,e]*Vd[e,b];
    julia> codomain(A2′)(ℝ^3 ⊗ ℝ^2)
    julia> domain(A2′)ProductSpace(ℝ^4)
    julia> @tensor A2′′[a b; c] := U[a,c,d]*S[d,e]*Vd[e,b];
    julia> A2 ≈ A2′ == A2′′true

    As illustrated for A2′ and A2′′, additional syntax is available that enables one to immediately specify the desired codomain and domain indices.

    Complex tensors

    For applications in e.g. quantum physics, we of course want to work with complex tensors. Trying to create a complex-valued tensor with CartesianSpace indices is of course somewhat contrived and prints a (one-time) warning

    julia> A = randn(ComplexF64, ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4)┌ Warning: scalartype(data) = ComplexF64 ⊈ ℝ)
    + 0.1634800053481681  -0.5037392676058686
    + 1.7681856239175135  -1.7832349230781555
    + 1.2262107616377473  -0.8559914027442731
    julia> codomain(A2)(ℝ^3 ⊗ ℝ^2)
    julia> domain(A2)ProductSpace(ℝ^4)

    In fact, tsvd(A, (1,3),(2,)) is a shorthand for tsvd(permute(A,(1,3),(2,))), where tsvd(A::TensorMap) will just compute the singular value decomposition according to the given codomain and domain of A.

    Note, finally, that the @tensor macro treats all indices at the same footing and thus does not distinguish between codomain and domain. The linear numbering is first all indices in the codomain, followed by all indices in the domain. However, when @tensor creates a new tensor (i.e. when using :=), the default syntax always creates a Tensor, i.e. with all indices in the codomain.

    julia> @tensor A′[a,b,c] := U[a,c,d]*S[d,e]*Vd[e,b];
    julia> codomain(A′)(ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4)
    julia> domain(A′)ProductSpace{CartesianSpace, 0}()
    julia> @tensor A2′[(a,b);(c,)] := U[a,c,d]*S[d,e]*Vd[e,b];
    julia> codomain(A2′)(ℝ^3 ⊗ ℝ^2)
    julia> domain(A2′)ProductSpace(ℝ^4)
    julia> @tensor A2′′[a b; c] := U[a,c,d]*S[d,e]*Vd[e,b];
    julia> A2 ≈ A2′ == A2′′true

    As illustrated for A2′ and A2′′, additional syntax is available that enables one to immediately specify the desired codomain and domain indices.

    Complex tensors

    For applications in e.g. quantum physics, we of course want to work with complex tensors. Trying to create a complex-valued tensor with CartesianSpace indices is of course somewhat contrived and prints a (one-time) warning

    julia> A = randn(ComplexF64, ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4)┌ Warning: scalartype(data) = ComplexF64 ⊈ ℝ)
     └ @ TensorKit ~/work/TensorKit.jl/TensorKit.jl/src/tensors/tensor.jl:32
     TensorMap((ℝ^3 ⊗ ℝ^2 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}()):
     [:, :, 1] =
    - -0.41071901317220644 - 0.24271047990933295im  …   0.4592759788299932 + 0.48408207754007654im
    -  0.09371159134203506 + 0.20456934746751823im      0.4520853767811823 + 0.37933192429221974im
    -    0.847192606626362 - 0.527886889545718im       -0.7011260862739672 - 0.34803026737230885im
    + -0.33307596562273484 - 0.3626112840292912im   …   0.9360032256839601 + 1.226604560723772im
    +  -0.2960857015128052 - 0.33987271311123546im      1.3588560064436943 - 0.1566122664802485im
    +   0.1142102745130189 - 0.23834399181831362im     -1.0831097314380242 - 0.6310864831652463im
     
     [:, :, 2] =
    -  0.49065626609560653 + 1.3482843407568996im  …     0.7795736441362159 + 0.6557699095084526im
    - -0.10488242223222306 - 1.376294968012425im      -0.007161928373862095 + 0.3086114421023815im
    -  0.11517912918097033 + 0.9140801959127663im       -0.7631873833699929 + 0.1726741463948766im
    +   0.2770145522883774 - 0.3567327209105283im   …    1.943457828224448 - 0.23293428196544394im
    + -0.30180229991187096 + 0.6196816123090613im       1.2136341566113906 + 0.5851556579363978im
    +   0.1294120504665036 + 0.28203807608120546im     -0.5650848076758752 - 1.4199959091566472im
     
     [:, :, 3] =
    - 0.7529306716029132 + 0.6862909561675409im  …  0.4572923472653177 + 0.19571141611619378im
    - 1.2249455227464328 + 0.6103371632223396im     1.3355461228236412 - 0.8479771693109247im
    - 1.0175666363897393 + 0.3413457351302651im     0.4769154038691181 - 0.17694443640315172im
    + -0.0606477572322467 - 0.592105792330395im    …  -0.08674551341342587 + 0.21420838440970535im
    + -0.8399197211701637 + 0.824782047213393im       -0.22717173589380088 + 0.12766718713714714im
    +  0.4837213637151629 - 0.08797823261249449im      -0.9580333315053001 - 0.23609458885956017im
     
     [:, :, 4] =
    - 0.08229490214053736 - 0.9527965158058781im  …   -0.271105698967916 + 0.02437511115813807im
    - -0.7414099563849608 + 0.2832365356652963im     -0.8041360375085252 + 0.09997438054793896im
    -  0.5220650913422754 + 0.83582717166449im       -0.6652805586375596 + 0.11663801061702594im

    although most of the above operations will work in the expected way (at your own risk). Indeed, we instead want to work with complex vector spaces

    julia> A = randn(ComplexF64, ℂ^3 ⊗ ℂ^2 ⊗ ℂ^4)TensorMap((ℂ^3 ⊗ ℂ^2 ⊗ ℂ^4) ← ProductSpace{ComplexSpace, 0}()):
    +  0.13774702157989216 - 0.11451726889061338im  …  -0.44588968714397786 - 0.3116501376557793im
    +  -0.7586137751961335 + 0.8890370533905918im        1.6490184252137305 - 0.22092006514334703im
    + -0.46784245441523764 + 0.1682175210047839im        -0.556040906072376 - 0.4070275085112227im

    although most of the above operations will work in the expected way (at your own risk). Indeed, we instead want to work with complex vector spaces

    julia> A = randn(ComplexF64, ℂ^3 ⊗ ℂ^2 ⊗ ℂ^4)TensorMap((ℂ^3 ⊗ ℂ^2 ⊗ ℂ^4) ← ProductSpace{ComplexSpace, 0}()):
     [:, :, 1] =
    - -0.32148052126260696 - 0.727034854196951im    …  0.28896075862729503 + 0.47629818887250835im
    -   0.8085258314234068 + 0.5163497611968915im      -0.4659828237881767 + 0.07254570467282116im
    -   -1.007418629111342 - 0.09768320714373435im      0.2539155598253959 + 0.026085305794719012im
    +   0.6830697801194765 + 0.35944925318255216im  …  -1.4230771560659192 - 0.6541868325692691im
    + -0.35749519964983695 + 0.7720853365426783im      -0.0809572833127678 - 0.8374984766961308im
    +   1.4204468199271982 - 0.15663210760099744im     -0.4714720155169251 + 0.11846026303805181im
     
     [:, :, 2] =
    -  0.6544843565530483 - 1.0916419076851345im  …   0.6721636950927143 + 1.232375796993436im
    - -0.0644527696469089 - 0.6865115650294868im     -0.9597395636581715 - 0.9672431378538481im
    -   1.397545038039156 + 0.5202374243236866im      0.6174201371094209 - 0.36103878542503004im
    +  0.5216798359986715 + 0.23070580381704808im  …  -0.3476290148615232 + 0.5377498300213139im
    +  1.0309690686534856 + 0.14780231295896182im     0.03186356483158779 + 1.0672221702994769im
    + 0.19379608347815871 + 0.10327079567391605im     -0.8094727888942401 + 0.26609221537321853im
     
     [:, :, 3] =
    -  0.7479191731956647 - 0.6619263430805501im   …  0.22292048274172208 - 1.4313678981682396im
    - -0.3093072965918093 + 1.2509941656865347im      0.26523451599979375 + 0.7374465019738256im
    - -0.6200781316386361 + 0.29809265241699967im      0.3303813640990814 - 0.3920921959967577im
    + -0.23788642433738164 + 1.286767253414969im   …  -0.40636693020300835 + 0.3182790808977942im
    +    -0.64738447114423 - 0.3304488632805771im      0.25079843467412644 - 0.5156246323460795im
    +   0.3136438439885254 + 0.390643183954557im      -0.09968047425015261 - 0.14257388988224037im
     
     [:, :, 4] =
    -  0.4685115299529507 - 0.2678297750009622im   …  0.16686058190175385 + 0.11133257995853149im
    - -1.0241629719238017 + 0.21617158714397156im      0.1851234712808075 - 0.13838085227211025im
    -  0.5942082096457666 + 0.45581051264659495im     -0.6938935516885014 - 0.4388464975076794im

    where is obtained as \bbC+TAB and we also have the non-Unicode alternative ℂ^n == ComplexSpace(n). Most functionality works exactly the same

    julia> B = randn(ℂ^3 * ℂ^2 * ℂ^4);
    julia> C = im*A + (2.5-0.8im)*BTensorMap((ℂ^3 ⊗ ℂ^2 ⊗ ℂ^4) ← ProductSpace{ComplexSpace, 0}()): + -0.5843308565430831 + 0.9232937854544754im … -0.34247404779097745 + 0.9840511476861568im + -0.6866961765513003 - 0.30115476491288756im 0.18506447017598796 - 0.9693965969269294im + -0.49224113801208846 - 0.5222578464536397im 0.5704545075572955 + 0.941750863865636im

    where is obtained as \bbC+TAB and we also have the non-Unicode alternative ℂ^n == ComplexSpace(n). Most functionality works exactly the same

    julia> B = randn(ℂ^3 * ℂ^2 * ℂ^4);
    julia> C = im*A + (2.5-0.8im)*BTensorMap((ℂ^3 ⊗ ℂ^2 ⊗ ℂ^4) ← ProductSpace{ComplexSpace, 0}()): [:, :, 1] = - -1.522435935714939 + 0.39835013150919807im … 0.4141311360777012 + 0.004023374643227939im - -0.15552313042347865 + 0.6930613095759147im 0.2712859583757918 - 0.5760089559637328im - -1.1301244008073683 - 0.6145201945669891im -2.80803597833304 + 1.1441397750376587im + 0.5389476235244849 + 0.39558277957322463im … -1.9532343608021434 - 0.5887023741870672im + -1.3671692085443063 - 0.167068360609316im 1.6505450769102197 - 0.3411321953812762im + 0.12213493260602895 + 1.4314859159255882im 1.9528990991550728 - 1.134307011418725im [:, :, 2] = - 2.1600639493886487 + 0.3125893032079236im … 2.64337356830132 - 0.5680761018016076im - -0.8383262614129987 + 0.42349533481468654im -0.6797972276626608 - 0.4326866466928886im - 3.6739314286058105 + 0.055411005101716926im 0.4854376938090873 + 0.5776124864265226im + -3.454793783665127 + 1.5533879895500569im … 2.244492824275042 - 1.2379466642363572im + -1.3858797827373575 + 1.4271538589825723im -0.8936535333232168 - 0.02367839900081542im + -2.203870116832015 + 0.8659878662487503im 0.38690925368448853 - 1.0184332589927063im [:, :, 3] = - 1.0921090011095174 + 0.6102607226263952im … 1.9649324416979013 + 0.05217982881223032im - 1.1637564835358218 - 1.0820275043429635im -0.25743036496660193 + 0.11162935215748218im - 2.8520643679533277 - 1.628128378157141im 1.0115075140560197 + 0.13216846232011759im + 2.1158874453260657 - 1.326735927934513im … -0.018647876331188917 - 0.5022489156643221im + 1.0074310257351782 - 0.8640187631297024im -1.016021720182735 + 0.7409252674833471im + 1.927302406132074 - 0.42809874483919663im 0.9599117391732293 - 0.361228586023269im [:, :, 4] = - 2.2697176383625886 - 0.17209258632276975im … -1.4277494406957218 + 0.5881139773376548im - -0.27032791956043756 - 1.0068329455505325im -1.4689118029590715 + 0.6994571209547857im - -0.21159140272905552 + 0.516058094472154im -0.8668803383177146 - 0.2760609642243753im
    julia> scalarBA = dot(B,A)4.865597466226893 + 5.576007397363329im
    julia> scalarAA = dot(A,A)20.655212631998765 + 0.0im
    julia> normA² = norm(A)^220.655212631998765
    julia> U,S,Vd = tsvd(A,(1,3),(2,));
    julia> @tensor A′[a,b,c] := U[a,c,d]*S[d,e]*Vd[e,b];
    julia> A′ ≈ Atrue
    julia> permute(A,(1,3),(2,)) ≈ U*S*Vdtrue

    However, trying the following

    julia> @tensor D[a,b,c,d] := A[a,b,e]*B[d,c,e]ERROR: SpaceMismatch("ProductSpace((ℂ^4)') ≠ ProductSpace(ℂ^4)")
    julia> @tensor d = A[a,b,c]*A[a,b,c]ERROR: SpaceMismatch("((ℂ^3)' ⊗ (ℂ^2)' ⊗ (ℂ^4)') ≠ (ℂ^3 ⊗ ℂ^2 ⊗ ℂ^4)")

    we obtain SpaceMismatch errors. The reason for this is that, with ComplexSpace, an index in a space ℂ^n can only be contracted with an index in the dual space dual(ℂ^n) == (ℂ^n)'. Because of the complex Euclidean inner product, the dual space is equivalent to the complex conjugate space, but not the the space itself.

    julia> dual(ℂ^3) == conj(ℂ^3) == (ℂ^3)'true
    julia> (ℂ^3)' == ℂ^3false
    julia> @tensor d = conj(A[a,b,c])*A[a,b,c]20.65521263199877 + 0.0im
    julia> d ≈ normA²true

    This might seem overly strict or puristic, but we believe that it can help to catch errors, e.g. unintended contractions. In particular, contracting two indices both living in ℂ^n would represent an operation that is not invariant under arbitrary unitary basis changes.

    It also makes clear the isomorphism between linear maps ℂ^n → ℂ^m and tensors in ℂ^m ⊗ (ℂ^n)':

    julia> m = randn(ComplexF64, ℂ^3, ℂ^4)TensorMap(ℂ^3 ← ℂ^4):
    - -1.0605557121446225 - 0.3658547035832714im  …  -0.15522712393737514 - 1.774760490691347im
    - -0.3158778949023674 + 0.7298137322500218im     -0.03982859821253985 + 0.25574081055902im
    -  0.5446092015120788 - 0.5794665538355597im       1.6299444122810023 - 0.14526343340282125im
    julia> m2 = permute(m, (1,2), ())TensorMap((ℂ^3 ⊗ (ℂ^4)') ← ProductSpace{ComplexSpace, 0}()): - -1.0605557121446225 - 0.3658547035832714im … -0.15522712393737514 - 1.774760490691347im - -0.3158778949023674 + 0.7298137322500218im -0.03982859821253985 + 0.25574081055902im - 0.5446092015120788 - 0.5794665538355597im 1.6299444122810023 - 0.14526343340282125im
    julia> codomain(m2)(ℂ^3 ⊗ (ℂ^4)')
    julia> space(m, 1)ℂ^3
    julia> space(m, 2)(ℂ^4)'

    Hence, spaces become their corresponding dual space if they are 'permuted' from the domain to the codomain or vice versa. Also, spaces in the domain are reported as their dual when probing them with space(A, i). Generalizing matrix vector and matrix matrix multiplication to arbitrary tensor contractions require that the two indices to be contracted have spaces which are each others dual. Knowing this, all the other functionality of tensors with CartesianSpace indices remains the same for tensors with ComplexSpace indices.

    Symmetries

    So far, the functionality that we have illustrated seems to be just a convenient (or inconvenient?) wrapper around dense multidimensional arrays, e.g. Julia's Base Array. More power becomes visible when involving symmetries. With symmetries, we imply that there is some symmetry action defined on every vector space associated with each of the indices of a TensorMap, and the TensorMap is then required to be equivariant, i.e. it acts as an intertwiner between the tensor product representation on the domain and that on the codomain. By Schur's lemma, this means that the tensor is block diagonal in some basis corresponding to the irreducible representations that can be coupled to by combining the different representations on the different spaces in the domain or codomain. For Abelian symmetries, this does not require a basis change and it just imposes that the tensor has some block sparsity. Let's clarify all of this with some examples.

    We start with a simple $ℤ₂$ symmetry:

    julia> V1 = ℤ₂Space(0=>3,1=>2)Rep[ℤ₂](0=>3, 1=>2)
    julia> dim(V1)5
    julia> V2 = ℤ₂Space(0=>1,1=>1)Rep[ℤ₂](0=>1, 1=>1)
    julia> dim(V2)2
    julia> A = randn(V1*V1*V2')TensorMap((Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>1, 1=>1)') ← ProductSpace{GradedSpace{Z2Irrep, Tuple{Int64, Int64}}, 0}()): + 2.697722147587161 - 1.7430559551164069im … -2.983413271293557 + 0.29732183176339066im + 0.6663934736805226 - 0.8035725633569435im 0.27751941160655125 + 0.40646516947850897im + -3.0266379356759527 + 0.6434055122693813im 0.15486967884763736 + 0.21953593388904802im
    julia> scalarBA = dot(B,A)-0.7822857690627916 + 5.0007793942934065im
    julia> scalarAA = dot(A,A)19.153941546701397 + 0.0im
    julia> normA² = norm(A)^219.153941546701397
    julia> U,S,Vd = tsvd(A,(1,3),(2,));
    julia> @tensor A′[a,b,c] := U[a,c,d]*S[d,e]*Vd[e,b];
    julia> A′ ≈ Atrue
    julia> permute(A,(1,3),(2,)) ≈ U*S*Vdtrue

    However, trying the following

    julia> @tensor D[a,b,c,d] := A[a,b,e]*B[d,c,e]ERROR: SpaceMismatch("ProductSpace((ℂ^4)') ≠ ProductSpace(ℂ^4)")
    julia> @tensor d = A[a,b,c]*A[a,b,c]ERROR: SpaceMismatch("((ℂ^3)' ⊗ (ℂ^2)' ⊗ (ℂ^4)') ≠ (ℂ^3 ⊗ ℂ^2 ⊗ ℂ^4)")

    we obtain SpaceMismatch errors. The reason for this is that, with ComplexSpace, an index in a space ℂ^n can only be contracted with an index in the dual space dual(ℂ^n) == (ℂ^n)'. Because of the complex Euclidean inner product, the dual space is equivalent to the complex conjugate space, but not the the space itself.

    julia> dual(ℂ^3) == conj(ℂ^3) == (ℂ^3)'true
    julia> (ℂ^3)' == ℂ^3false
    julia> @tensor d = conj(A[a,b,c])*A[a,b,c]19.153941546701393 + 0.0im
    julia> d ≈ normA²true

    This might seem overly strict or puristic, but we believe that it can help to catch errors, e.g. unintended contractions. In particular, contracting two indices both living in ℂ^n would represent an operation that is not invariant under arbitrary unitary basis changes.

    It also makes clear the isomorphism between linear maps ℂ^n → ℂ^m and tensors in ℂ^m ⊗ (ℂ^n)':

    julia> m = randn(ComplexF64, ℂ^3, ℂ^4)TensorMap(ℂ^3 ← ℂ^4):
    + -0.12636899745057198 - 0.9373722752591889im   …  -0.12482291671189516 - 0.07949709894601868im
    +  -0.3827277192373495 - 1.1703092234475012im        0.0710181422308982 - 0.09078262779358937im
    +  0.23805211432130408 - 0.20453439528161035im       0.8683503871565796 - 0.13761557865630228im
    julia> m2 = permute(m, (1,2), ())TensorMap((ℂ^3 ⊗ (ℂ^4)') ← ProductSpace{ComplexSpace, 0}()): + -0.12636899745057198 - 0.9373722752591889im … -0.12482291671189516 - 0.07949709894601868im + -0.3827277192373495 - 1.1703092234475012im 0.0710181422308982 - 0.09078262779358937im + 0.23805211432130408 - 0.20453439528161035im 0.8683503871565796 - 0.13761557865630228im
    julia> codomain(m2)(ℂ^3 ⊗ (ℂ^4)')
    julia> space(m, 1)ℂ^3
    julia> space(m, 2)(ℂ^4)'

    Hence, spaces become their corresponding dual space if they are 'permuted' from the domain to the codomain or vice versa. Also, spaces in the domain are reported as their dual when probing them with space(A, i). Generalizing matrix vector and matrix matrix multiplication to arbitrary tensor contractions require that the two indices to be contracted have spaces which are each others dual. Knowing this, all the other functionality of tensors with CartesianSpace indices remains the same for tensors with ComplexSpace indices.

    Symmetries

    So far, the functionality that we have illustrated seems to be just a convenient (or inconvenient?) wrapper around dense multidimensional arrays, e.g. Julia's Base Array. More power becomes visible when involving symmetries. With symmetries, we imply that there is some symmetry action defined on every vector space associated with each of the indices of a TensorMap, and the TensorMap is then required to be equivariant, i.e. it acts as an intertwiner between the tensor product representation on the domain and that on the codomain. By Schur's lemma, this means that the tensor is block diagonal in some basis corresponding to the irreducible representations that can be coupled to by combining the different representations on the different spaces in the domain or codomain. For Abelian symmetries, this does not require a basis change and it just imposes that the tensor has some block sparsity. Let's clarify all of this with some examples.

    We start with a simple $ℤ₂$ symmetry:

    julia> V1 = ℤ₂Space(0=>3,1=>2)Rep[ℤ₂](0=>3, 1=>2)
    julia> dim(V1)5
    julia> V2 = ℤ₂Space(0=>1,1=>1)Rep[ℤ₂](0=>1, 1=>1)
    julia> dim(V2)2
    julia> A = randn(V1*V1*V2')TensorMap((Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>1, 1=>1)') ← ProductSpace{GradedSpace{Z2Irrep, Tuple{Int64, Int64}}, 0}()): * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](0), Irrep[ℤ₂](0)) ← (): [:, :, 1] = - 0.6693214883446814 0.10194308346478685 -0.23776597760564114 - -0.08365006026698008 -1.1371366797993276 0.7214543003394215 - 0.8141243915493214 0.5612601924357646 -1.7691827686048194 + -1.1823082518978476 -1.7735520852955082 0.7596439547754076 + -0.40697654079200135 0.5028187169727945 0.20919631196353208 + -2.084242532007782 1.4209490097309692 -0.38066028986972067 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](1), Irrep[ℤ₂](0)) ← (): [:, :, 1] = - -1.700745863316957 -2.095253296995244 - -0.7701326869463031 -0.510216334784507 + 0.43113717709629684 0.3275466017588099 + 1.0266862194311535 0.6645576619669863 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](0), Irrep[ℤ₂](1)) ← (): [:, :, 1] = - -0.7791929099137024 1.462398571950043 0.9595536163432734 - -0.364450696410993 0.3015005272196933 -0.4763719977105689 + 0.6684892710883682 0.020959559063352458 0.8600564563952391 + -0.8173446796637716 -0.5769079107590274 -0.87068852374812 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](1), Irrep[ℤ₂](1)) ← (): [:, :, 1] = - -1.060196357364677 -0.888600750881492 - -0.7035229597888983 2.7270874353473804 - -0.9639949440002757 -0.013630302184442326
    julia> convert(Array, A)5×5×2 Array{Float64, 3}: + 0.7711170655339067 0.16698575852885852 + 0.4144895869939875 0.7277975632115068 + 1.1996846675772603 1.7640673598503007
    julia> convert(Array, A)5×5×2 Array{Float64, 3}: [:, :, 1] = - 0.669321 0.101943 -0.237766 0.0 0.0 - -0.0836501 -1.13714 0.721454 0.0 0.0 - 0.814124 0.56126 -1.76918 0.0 0.0 - 0.0 0.0 0.0 -1.70075 -2.09525 - 0.0 0.0 0.0 -0.770133 -0.510216 + -1.18231 -1.77355 0.759644 0.0 0.0 + -0.406977 0.502819 0.209196 0.0 0.0 + -2.08424 1.42095 -0.38066 0.0 0.0 + 0.0 0.0 0.0 0.431137 0.327547 + 0.0 0.0 0.0 1.02669 0.664558 [:, :, 2] = - 0.0 0.0 0.0 -1.0602 -0.888601 - 0.0 0.0 0.0 -0.703523 2.72709 - 0.0 0.0 0.0 -0.963995 -0.0136303 - -0.779193 1.4624 0.959554 0.0 0.0 - -0.364451 0.301501 -0.476372 0.0 0.0

    Here, we create a 5-dimensional space V1, which has a three-dimensional subspace associated with charge 0 (the trivial irrep of $ℤ₂$) and a two-dimensional subspace with charge 1 (the non-trivial irrep). Similar for V2, where both subspaces are one- dimensional. Representing the tensor as a dense Array, we see that it is zero in those regions where the charges don't add to zero (modulo 2). Of course, the Tensor(Map) type in TensorKit.jl won't store these zero blocks, and only stores the non-zero information, which we can recognize also in the full Array representation.

    From there on, the resulting tensors support all of the same operations as the ones we encountered in the previous examples.

    julia> B = randn(V1'*V1*V2);
    julia> @tensor C[a,b] := A[a,c,d]*B[c,b,d]TensorMap((Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>3, 1=>2)) ← ProductSpace{GradedSpace{Z2Irrep, Tuple{Int64, Int64}}, 0}()): + 0.0 0.0 0.0 0.771117 0.166986 + 0.0 0.0 0.0 0.41449 0.727798 + 0.0 0.0 0.0 1.19968 1.76407 + 0.668489 0.0209596 0.860056 0.0 0.0 + -0.817345 -0.576908 -0.870689 0.0 0.0

    Here, we create a 5-dimensional space V1, which has a three-dimensional subspace associated with charge 0 (the trivial irrep of $ℤ₂$) and a two-dimensional subspace with charge 1 (the non-trivial irrep). Similar for V2, where both subspaces are one- dimensional. Representing the tensor as a dense Array, we see that it is zero in those regions where the charges don't add to zero (modulo 2). Of course, the Tensor(Map) type in TensorKit.jl won't store these zero blocks, and only stores the non-zero information, which we can recognize also in the full Array representation.

    From there on, the resulting tensors support all of the same operations as the ones we encountered in the previous examples.

    julia> B = randn(V1'*V1*V2);
    julia> @tensor C[a,b] := A[a,c,d]*B[c,b,d]TensorMap((Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>3, 1=>2)) ← ProductSpace{GradedSpace{Z2Irrep, Tuple{Int64, Int64}}, 0}()): * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](0)) ← (): - 3.348733595830133 -1.8652795035853345 -1.9899416157542584 - 0.4701754707294163 6.794506493547622 -3.718172765336213 - 1.441395628460052 -2.4522303591061254 -0.24825400822816243 + 1.5738471411669155 -0.32087204852631745 0.9579564418622362 + 1.4371316147217839 0.4701725343538042 1.2834474399035183 + 4.74925361086297 1.7341375943013395 4.087784639555667 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](1)) ← (): - -2.2450908260052445 -4.168780747476167 - -0.986555771418695 -1.0876298025627023
    julia> U,S,V = tsvd(A,(1,3),(2,));
    julia> U'*U # should be the identity on the corresponding domain = codomainTensorMap(Rep[ℤ₂](0=>3, 1=>2) ← Rep[ℤ₂](0=>3, 1=>2)): + -0.6767355576393719 -1.1289509927948995 + -3.4171632880873606 0.674108727788072
    julia> U,S,V = tsvd(A,(1,3),(2,));
    julia> U'*U # should be the identity on the corresponding domain = codomainTensorMap(Rep[ℤ₂](0=>3, 1=>2) ← Rep[ℤ₂](0=>3, 1=>2)): * Data for sector (Irrep[ℤ₂](0),) ← (Irrep[ℤ₂](0),): - 0.9999999999999992 1.1717433256436806e-16 -3.4803149396955763e-16 - 1.1717433256436806e-16 0.9999999999999997 7.280408857775248e-17 - -3.4803149396955763e-16 7.280408857775248e-17 0.9999999999999999 + 1.0000000000000004 3.8666392773376817e-16 -1.3450106552779298e-16 + 3.8666392773376817e-16 1.0000000000000004 -1.5938164400165584e-16 + -1.3450106552779298e-16 -1.5938164400165584e-16 0.9999999999999997 * Data for sector (Irrep[ℤ₂](1),) ← (Irrep[ℤ₂](1),): - 1.0000000000000002 -2.510401468681025e-16 - -2.510401468681025e-16 1.0
    julia> U'*U ≈ one(U'*U)true
    julia> P = U*U' # should be a projectorTensorMap((Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>1, 1=>1)') ← (Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>1, 1=>1)')): + 1.0 -9.839563961152471e-17 + -9.839563961152471e-17 1.0000000000000002
    julia> U'*U ≈ one(U'*U)true
    julia> P = U*U' # should be a projectorTensorMap((Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>1, 1=>1)') ← (Rep[ℤ₂](0=>3, 1=>2) ⊗ Rep[ℤ₂](0=>1, 1=>1)')): * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](0)): [:, :, 1, 1] = - 0.5525856635813017 - -0.05641878744563947 - 0.17111176068575795 + 0.9827286241702162 + 0.026144475766585247 + 0.002140930835949136 [:, :, 2, 1] = - -0.05641878744563947 - 0.3996363720532797 - -0.360536758703795 + 0.026144475766585247 + 0.13573806926360965 + 0.2635086858435954 [:, :, 3, 1] = - 0.17111176068575795 - -0.360536758703795 - 0.6884381347637959 + 0.002140930835949136 + 0.2635086858435954 + 0.9134529066250878 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](0)): [:, :, 1, 1] = - 0.017127537256291958 - -0.46311886296223365 + 0.09551596954464993 + 0.08462657839427307 [:, :, 2, 1] = - -0.296007161793741 - -0.1383367933657254 + 0.12132556841390177 + -0.18020263909585948 [:, :, 3, 1] = - -0.19860071364380338 - 0.1256297611447798 + -0.09785096528509628 + 0.006361721164804477 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](1)): [:, :, 1, 1] = - 0.017127537256291958 - -0.296007161793741 - -0.19860071364380338 + 0.09551596954464993 + 0.12132556841390177 + -0.09785096528509628 [:, :, 2, 1] = - -0.46311886296223365 - -0.1383367933657254 - 0.1256297611447798 + 0.08462657839427307 + -0.18020263909585948 + 0.006361721164804477 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](1)): [:, :, 1, 1] = - 0.8494857000774668 - -0.02244793121610601 + 0.38602651878047795 + -0.4512117465031991 [:, :, 2, 1] = - -0.02244793121610601 - 0.5098541295241548 + -0.4512117465031991 + 0.5820538811606087 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](0)): [:, :, 1, 1] = - 0.6168857468670436 - 0.231157695782265 + 0.06356382397137804 + 0.16315868245060697 [:, :, 2, 1] = - 0.231157695782265 - 0.09973806875547891 + 0.16315868245060697 + 0.43107276290358876 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](0)): [:, :, 1, 1] = - 0.3386792781938782 - -0.13964435808380482 - 0.22067995295767784 + 0.1564692848651379 + 0.01893635198492606 + 0.08978577872798689 [:, :, 2, 1] = - 0.13939893431698505 - 0.055180257628814955 - 0.11781163930761074 + 0.4461480161102528 + 0.0047244181505269305 + 0.13984959035071126 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](1)): [:, :, 1, 1] = - 0.3386792781938782 - 0.13939893431698505 + 0.1564692848651379 + 0.4461480161102528 [:, :, 2, 1] = - -0.13964435808380482 - 0.055180257628814955 + 0.01893635198492606 + 0.0047244181505269305 [:, :, 3, 1] = - 0.22067995295767784 - 0.11781163930761074 + 0.08978577872798689 + 0.13984959035071126 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](1)): [:, :, 1, 1] = - 0.1978307201036882 - 0.0256835859650338 - 0.15459095488508887 + 0.546675977537337 + -0.11259993366419428 + -0.10775861299418785 [:, :, 2, 1] = - 0.0256835859650338 - 0.91259089665632 - 0.2378312381326108 + -0.11259993366419428 + 0.16259187225662763 + 0.3508505447026884 [:, :, 3, 1] = - 0.15459095488508887 - 0.2378312381326108 - 0.17295456761746955
    julia> P*P ≈ Ptrue

    We also support other abelian symmetries, e.g.

    julia> V = U₁Space(0=>2,1=>1,-1=>1)Rep[U₁](0=>2, 1=>1, -1=>1)
    julia> dim(V)4
    julia> A = randn(V*V, V)TensorMap((Rep[U₁](0=>2, 1=>1, -1=>1) ⊗ Rep[U₁](0=>2, 1=>1, -1=>1)) ← Rep[U₁](0=>2, 1=>1, -1=>1)): + -0.10775861299418785 + 0.3508505447026884 + 0.796095563331069
    julia> P*P ≈ Ptrue

    We also support other abelian symmetries, e.g.

    julia> V = U₁Space(0=>2,1=>1,-1=>1)Rep[U₁](0=>2, 1=>1, -1=>1)
    julia> dim(V)4
    julia> A = randn(V*V, V)TensorMap((Rep[U₁](0=>2, 1=>1, -1=>1) ⊗ Rep[U₁](0=>2, 1=>1, -1=>1)) ← Rep[U₁](0=>2, 1=>1, -1=>1)): * Data for sector (Irrep[U₁](0), Irrep[U₁](0)) ← (Irrep[U₁](0),): [:, :, 1] = - -0.24608917698764987 0.751850425423202 - 0.3065861455169373 -1.229811893155227 + 2.380511430490086 -0.6439493067719836 + 0.4833891131946091 -1.056202342959373 [:, :, 2] = - -0.8906192320420853 0.8090947824048695 - 0.942926527611733 -1.6929431026592525 + 0.07966316638775751 -0.40677274819981624 + -1.882944812365632 1.020003934854592 * Data for sector (Irrep[U₁](-1), Irrep[U₁](1)) ← (Irrep[U₁](0),): [:, :, 1] = - 1.347108704256457 + 0.968761720867509 [:, :, 2] = - 1.5639425215690483 + -0.4533046592558804 * Data for sector (Irrep[U₁](1), Irrep[U₁](-1)) ← (Irrep[U₁](0),): [:, :, 1] = - 0.942005208570807 + -1.5195633418950065 [:, :, 2] = - 0.9692773167919295 + -0.21875279342924367 * Data for sector (Irrep[U₁](1), Irrep[U₁](0)) ← (Irrep[U₁](1),): [:, :, 1] = - -0.9723557934880234 0.665129597269248 + -1.9233988294149624 1.494857317138535 * Data for sector (Irrep[U₁](0), Irrep[U₁](1)) ← (Irrep[U₁](1),): [:, :, 1] = - 0.09380841329990887 - 1.2087024042946288 + -0.3997533941831525 + -0.31784377960283705 * Data for sector (Irrep[U₁](-1), Irrep[U₁](0)) ← (Irrep[U₁](-1),): [:, :, 1] = - -1.2697522232110658 -0.043122664635387026 + 0.8939493029921453 0.7352773317634566 * Data for sector (Irrep[U₁](0), Irrep[U₁](-1)) ← (Irrep[U₁](-1),): [:, :, 1] = - -0.5449454528721441 - -2.1332321697579033
    julia> dim(A)20
    julia> convert(Array, A)4×4×4 Array{Float64, 3}: + 0.16131916408360394 + 0.9032092621543721
    julia> dim(A)20
    julia> convert(Array, A)4×4×4 Array{Float64, 3}: [:, :, 1] = - -0.246089 0.75185 0.0 0.0 - 0.306586 -1.22981 0.0 0.0 - 0.0 0.0 0.0 0.942005 - 0.0 0.0 1.34711 0.0 + 2.38051 -0.643949 0.0 0.0 + 0.483389 -1.0562 0.0 0.0 + 0.0 0.0 0.0 -1.51956 + 0.0 0.0 0.968762 0.0 [:, :, 2] = - -0.890619 0.809095 0.0 0.0 - 0.942927 -1.69294 0.0 0.0 - 0.0 0.0 0.0 0.969277 - 0.0 0.0 1.56394 0.0 + 0.0796632 -0.406773 0.0 0.0 + -1.88294 1.02 0.0 0.0 + 0.0 0.0 0.0 -0.218753 + 0.0 0.0 -0.453305 0.0 [:, :, 3] = - 0.0 0.0 0.0938084 0.0 - 0.0 0.0 1.2087 0.0 - -0.972356 0.66513 0.0 0.0 - 0.0 0.0 0.0 0.0 + 0.0 0.0 -0.399753 0.0 + 0.0 0.0 -0.317844 0.0 + -1.9234 1.49486 0.0 0.0 + 0.0 0.0 0.0 0.0 [:, :, 4] = - 0.0 0.0 0.0 -0.544945 - 0.0 0.0 0.0 -2.13323 - 0.0 0.0 0.0 0.0 - -1.26975 -0.0431227 0.0 0.0
    julia> V = Rep[U₁×ℤ₂]((0, 0) => 2, (1, 1) => 1, (-1, 0) => 1)Rep[U₁ × ℤ₂]((0, 0)=>2, (1, 1)=>1, (-1, 0)=>1)
    julia> dim(V)4
    julia> A = randn(V*V, V)TensorMap((Rep[U₁ × ℤ₂]((0, 0)=>2, (1, 1)=>1, (-1, 0)=>1) ⊗ Rep[U₁ × ℤ₂]((0, 0)=>2, (1, 1)=>1, (-1, 0)=>1)) ← Rep[U₁ × ℤ₂]((0, 0)=>2, (1, 1)=>1, (-1, 0)=>1)): + 0.0 0.0 0.0 0.161319 + 0.0 0.0 0.0 0.903209 + 0.0 0.0 0.0 0.0 + 0.893949 0.735277 0.0 0.0
    julia> V = Rep[U₁×ℤ₂]((0, 0) => 2, (1, 1) => 1, (-1, 0) => 1)Rep[U₁ × ℤ₂]((0, 0)=>2, (1, 1)=>1, (-1, 0)=>1)
    julia> dim(V)4
    julia> A = randn(V*V, V)TensorMap((Rep[U₁ × ℤ₂]((0, 0)=>2, (1, 1)=>1, (-1, 0)=>1) ⊗ Rep[U₁ × ℤ₂]((0, 0)=>2, (1, 1)=>1, (-1, 0)=>1)) ← Rep[U₁ × ℤ₂]((0, 0)=>2, (1, 1)=>1, (-1, 0)=>1)): * Data for sector (Irrep[U₁ × ℤ₂](0, 0), Irrep[U₁ × ℤ₂](0, 0)) ← (Irrep[U₁ × ℤ₂](0, 0),): [:, :, 1] = - 1.0217823288857728 0.11086819343211474 - 0.725178152337816 0.2850420112776637 + 0.15601439206324566 1.552940876965171 + -0.08298435281379188 0.2988909016059328 [:, :, 2] = - 0.49361154202390556 -1.3307053865074914 - 0.10140305267844633 -1.5449068514529063 + 1.3419093384190721 -0.1727826195340987 + -1.1009985956391872 -0.17045152716395037 * Data for sector (Irrep[U₁ × ℤ₂](1, 1), Irrep[U₁ × ℤ₂](0, 0)) ← (Irrep[U₁ × ℤ₂](1, 1),): [:, :, 1] = - 0.43317924833593324 1.055328484119421 + 1.1163231393191315 1.3314916306586877 * Data for sector (Irrep[U₁ × ℤ₂](0, 0), Irrep[U₁ × ℤ₂](1, 1)) ← (Irrep[U₁ × ℤ₂](1, 1),): [:, :, 1] = - -0.9179568203856365 - 0.17956603593462486 + 1.7432267651988902 + 0.38133199441852733 * Data for sector (Irrep[U₁ × ℤ₂](-1, 0), Irrep[U₁ × ℤ₂](0, 0)) ← (Irrep[U₁ × ℤ₂](-1, 0),): [:, :, 1] = - -0.7951084798475763 -0.3092777487700986 + -0.06127058588540012 0.868299838071228 * Data for sector (Irrep[U₁ × ℤ₂](0, 0), Irrep[U₁ × ℤ₂](-1, 0)) ← (Irrep[U₁ × ℤ₂](-1, 0),): [:, :, 1] = - 1.3632530237198877 - 0.0463410267133591
    julia> dim(A)16
    julia> convert(Array, A)4×4×4 Array{Float64, 3}: + -0.3236064395192719 + 0.7668629134277618
    julia> dim(A)16
    julia> convert(Array, A)4×4×4 Array{Float64, 3}: [:, :, 1] = - 1.02178 0.110868 0.0 0.0 - 0.725178 0.285042 0.0 0.0 - 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 + 0.156014 1.55294 0.0 0.0 + -0.0829844 0.298891 0.0 0.0 + 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 [:, :, 2] = - 0.493612 -1.33071 0.0 0.0 - 0.101403 -1.54491 0.0 0.0 - 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 + 1.34191 -0.172783 0.0 0.0 + -1.101 -0.170452 0.0 0.0 + 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 [:, :, 3] = - 0.0 0.0 -0.917957 0.0 - 0.0 0.0 0.179566 0.0 - 0.433179 1.05533 0.0 0.0 - 0.0 0.0 0.0 0.0 + 0.0 0.0 1.74323 0.0 + 0.0 0.0 0.381332 0.0 + 1.11632 1.33149 0.0 0.0 + 0.0 0.0 0.0 0.0 [:, :, 4] = - 0.0 0.0 0.0 1.36325 - 0.0 0.0 0.0 0.046341 - 0.0 0.0 0.0 0.0 - -0.795108 -0.309278 0.0 0.0

    Here, the dim of a TensorMap returns the number of linearly independent components, i.e. the number of non-zero entries in the case of an abelian symmetry. Also note that we can use × (obtained as \times+TAB) to combine different symmetry groups. The general space associated with symmetries is a GradedSpace, which is parametrized to the type of symmetry. For a group G, the fully specified type can be obtained as Rep[G], while for more general sectortypes I it can be constructed as Vect[I]. Furthermore, ℤ₂Space (also Z2Space as non-Unicode alternative) and U₁Space (or U1Space) are just convenient synonyms, e.g.

    julia> Rep[U₁](0=>3,1=>2,-1=>1) == U1Space(-1=>1,1=>2,0=>3)true
    julia> V = U₁Space(1=>2,0=>3,-1=>1)Rep[U₁](0=>3, 1=>2, -1=>1)
    julia> for s in sectors(V) + 0.0 0.0 0.0 -0.323606 + 0.0 0.0 0.0 0.766863 + 0.0 0.0 0.0 0.0 + -0.0612706 0.8683 0.0 0.0

    Here, the dim of a TensorMap returns the number of linearly independent components, i.e. the number of non-zero entries in the case of an abelian symmetry. Also note that we can use × (obtained as \times+TAB) to combine different symmetry groups. The general space associated with symmetries is a GradedSpace, which is parametrized to the type of symmetry. For a group G, the fully specified type can be obtained as Rep[G], while for more general sectortypes I it can be constructed as Vect[I]. Furthermore, ℤ₂Space (also Z2Space as non-Unicode alternative) and U₁Space (or U1Space) are just convenient synonyms, e.g.

    julia> Rep[U₁](0=>3,1=>2,-1=>1) == U1Space(-1=>1,1=>2,0=>3)true
    julia> V = U₁Space(1=>2,0=>3,-1=>1)Rep[U₁](0=>3, 1=>2, -1=>1)
    julia> for s in sectors(V) @show s, dim(V, s) end(s, dim(V, s)) = (Irrep[U₁](0), 3) (s, dim(V, s)) = (Irrep[U₁](1), 2) (s, dim(V, s)) = (Irrep[U₁](-1), 1)
    julia> U₁Space(-1=>1,0=>3,1=>2) == GradedSpace(Irrep[U₁](1)=>2, Irrep[U₁](0)=>3, Irrep[U₁](-1)=>1)true
    julia> supertype(GradedSpace)ElementarySpace

    Note that GradedSpace is not immediately parameterized by some group G, but actually by the set of irreducible representations of G, denoted as Irrep[G]. Indeed, GradedSpace also supports a grading that is derived from the fusion ring of a (unitary) pre-fusion category. Note furthermore that the order in which the charges and their corresponding subspace dimensionality are specified is irrelevant, and that the charges, henceforth called sectors (which is a more general name for charges or quantum numbers) are of a specific type, in this case Irrep[U₁] == U1Irrep. However, the Vect[I] constructor automatically converts the keys in the list of Pairs it receives to the correct type. Alternatively, we can directly create the sectors of the correct type and use the generic GradedSpace constructor. We can probe the subspace dimension of a certain sector s in a space V with dim(V, s). Finally, note that GradedSpace is also a subtype of EuclideanSpace, which implies that it still has the standard Euclidean inner product and we assume all representations to be unitary.

    TensorKit.jl also allows for non-abelian symmetries such as SU₂. In this case, the vector space is characterized via the spin quantum number (i.e. the irrep label of SU₂) for each of its subspaces, and is created using SU₂Space (or SU2Space or Rep[SU₂] or Vect[Irrep[SU₂]])

    julia> V = SU₂Space(0=>2,1/2=>1,1=>1)Rep[SU₂](0=>2, 1/2=>1, 1=>1)
    julia> dim(V)7
    julia> V == Vect[Irrep[SU₂]](0=>2, 1=>1, 1//2=>1)true

    Note that now V has a two-dimensional subspace with spin zero, and two one-dimensional subspaces with spin 1/2 and spin 1. However, a subspace with spin j has an additional 2j+1 dimensional degeneracy on which the irreducible representation acts. This brings the total dimension to 2*1 + 1*2 + 1*3. Creating a tensor with SU₂ symmetry yields

    julia> A = randn(V*V, V)TensorMap((Rep[SU₂](0=>2, 1/2=>1, 1=>1) ⊗ Rep[SU₂](0=>2, 1/2=>1, 1=>1)) ← Rep[SU₂](0=>2, 1/2=>1, 1=>1)):
     * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 0), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()):
     [:, :, 1] =
    - 1.461283006161726    -0.4371318960407443
    - 0.22146427956949738   0.640713963323066
    + 0.8636518768084456  1.8847079201247599
    + 1.2793882420974445  1.7410365050324763
     
     [:, :, 2] =
    - -0.3200124584855685   -0.08280584310206462
    - -0.34897429363348353   0.4383678792532445
    + 1.3447168669256453   1.0355036704304403
    + 1.1476663362659951  -0.8241014459940338
     * Data for fusiontree FusionTree{Irrep[SU₂]}((1/2, 1/2), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()):
     [:, :, 1] =
    - 0.7307779958945061
    + 0.4939460545822949
     
     [:, :, 2] =
    - -0.7698422815618449
    + 0.21461827413514592
     * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()):
     [:, :, 1] =
    - -0.30909787234480857
    + -0.3842436178083889
     
     [:, :, 2] =
    - -0.17065044787351796
    + 0.653029153305151
     * Data for fusiontree FusionTree{Irrep[SU₂]}((1/2, 0), 1/2, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()):
     [:, :, 1] =
    - 0.1933363919109695  -0.6516229923775428
    + 0.8131970652389522  -1.1152027182030118
     * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 1/2), 1/2, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()):
     [:, :, 1] =
    - -0.07761366532267823
    - -0.10929562235663666
    + -0.8975622180932868
    + -0.3764961990380583
     * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1/2), 1/2, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()):
     [:, :, 1] =
    - 0.07882459971636681
    + 0.05936587797820207
     * Data for fusiontree FusionTree{Irrep[SU₂]}((1/2, 1), 1/2, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()):
     [:, :, 1] =
    - -0.03290753578393434
    + -0.059757966111475914
     * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 0), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()):
     [:, :, 1] =
    - -0.7572753141594163  0.21480970837401706
    + -0.1994739476767708  1.3773094969444406
     * Data for fusiontree FusionTree{Irrep[SU₂]}((1/2, 1/2), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()):
     [:, :, 1] =
    - 1.2758575210185172
    + -0.6202084575704468
     * Data for fusiontree FusionTree{Irrep[SU₂]}((0, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()):
     [:, :, 1] =
    -  0.431804973007683
    - -0.7944981325773982
    + 0.4003962069983471
    + 2.1380494913956865
     * Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()):
     [:, :, 1] =
    - 0.05573414772197442
    julia> dim(A)24
    julia> convert(Array, A)7×7×7 Array{Float64, 3}: + -0.31000512678704534
    julia> dim(A)24
    julia> convert(Array, A)7×7×7 Array{Float64, 3}: [:, :, 1] = - 1.46128 -0.437132 0.0 0.0 0.0 0.0 0.0 - 0.221464 0.640714 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.516738 0.0 0.0 0.0 - 0.0 0.0 -0.516738 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 -0.178458 - 0.0 0.0 0.0 0.0 0.0 0.178458 0.0 - 0.0 0.0 0.0 0.0 -0.178458 0.0 0.0 + 0.863652 1.88471 0.0 0.0 0.0 0.0 0.0 + 1.27939 1.74104 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.349273 0.0 0.0 0.0 + 0.0 0.0 -0.349273 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 -0.221843 + 0.0 0.0 0.0 0.0 0.0 0.221843 0.0 + 0.0 0.0 0.0 0.0 -0.221843 0.0 0.0 [:, :, 2] = - -0.320012 -0.0828058 0.0 … 0.0 0.0 0.0 - -0.348974 0.438368 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.544361 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 -0.0985251 - 0.0 0.0 0.0 … 0.0 0.0985251 0.0 - 0.0 0.0 0.0 -0.0985251 0.0 0.0 + 1.34472 1.0355 0.0 0.0 0.0 0.0 0.0 + 1.14767 -0.824101 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.151758 0.0 0.0 0.0 + 0.0 0.0 -0.151758 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.377027 + 0.0 0.0 0.0 0.0 0.0 -0.377027 0.0 + 0.0 0.0 0.0 0.0 0.377027 0.0 0.0 [:, :, 3] = - 0.0 0.0 -0.0776137 0.0 0.0 0.0 0.0 - 0.0 0.0 -0.109296 0.0 0.0 0.0 0.0 - 0.193336 -0.651623 0.0 0.0 0.0 -0.0189992 0.0 - 0.0 0.0 0.0 0.0 0.0268689 0.0 0.0 - 0.0 0.0 0.0 0.06436 0.0 0.0 0.0 - 0.0 0.0 -0.0455094 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 -0.897562 0.0 0.0 0.0 0.0 + 0.0 0.0 -0.376496 0.0 0.0 0.0 0.0 + 0.813197 -1.1152 0.0 0.0 0.0 -0.0345013 0.0 + 0.0 0.0 0.0 0.0 0.0487922 0.0 0.0 + 0.0 0.0 0.0 0.048472 0.0 0.0 0.0 + 0.0 0.0 -0.0342749 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 [:, :, 4] = - 0.0 0.0 0.0 -0.0776137 0.0 0.0 0.0 - 0.0 0.0 0.0 -0.109296 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 -0.0268689 - 0.193336 -0.651623 0.0 0.0 0.0 0.0189992 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0455094 0.0 0.0 0.0 - 0.0 0.0 -0.06436 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 -0.897562 0.0 0.0 0.0 + 0.0 0.0 0.0 -0.376496 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 -0.0487922 + 0.813197 -1.1152 0.0 0.0 0.0 0.0345013 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0342749 0.0 0.0 0.0 + 0.0 0.0 -0.048472 0.0 0.0 0.0 0.0 [:, :, 5] = - 0.0 0.0 0.0 0.0 0.431805 0.0 0.0 - 0.0 0.0 0.0 0.0 -0.794498 0.0 0.0 - 0.0 0.0 1.27586 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - -0.757275 0.21481 0.0 0.0 0.0 0.03941 0.0 - 0.0 0.0 0.0 0.0 -0.03941 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.400396 0.0 0.0 + 0.0 0.0 0.0 0.0 2.13805 0.0 0.0 + 0.0 0.0 -0.620208 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + -0.199474 1.37731 0.0 0.0 0.0 -0.219207 0.0 + 0.0 0.0 0.0 0.0 0.219207 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 [:, :, 6] = - 0.0 0.0 0.0 0.0 0.0 0.431805 0.0 - 0.0 0.0 0.0 0.0 0.0 -0.794498 0.0 - 0.0 0.0 0.0 0.902168 0.0 0.0 0.0 - 0.0 0.0 0.902168 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.03941 - -0.757275 0.21481 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 -0.03941 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.400396 0.0 + 0.0 0.0 0.0 0.0 0.0 2.13805 0.0 + 0.0 0.0 0.0 -0.438554 0.0 0.0 0.0 + 0.0 0.0 -0.438554 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 -0.219207 + -0.199474 1.37731 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.219207 0.0 0.0 [:, :, 7] = - 0.0 0.0 0.0 0.0 0.0 0.0 0.431805 - 0.0 0.0 0.0 0.0 0.0 0.0 -0.794498 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 1.27586 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.03941 - -0.757275 0.21481 0.0 0.0 0.0 -0.03941 0.0
    julia> norm(A) ≈ norm(convert(Array, A))true

    In this case, the full Array representation of the tensor has again many zeros, but it is less obvious to recognize the dense blocks, as there are additional zeros and the numbers in the original tensor data do not match with those in the Array. The reason is of course that the original tensor data now needs to be transformed with a construction known as fusion trees, which are made up out of the Clebsch-Gordan coefficients of the group. Indeed, note that the non-zero blocks are also no longer labeled by a list of sectors, but by pair of fusion trees. This will be explained further in the manual. However, the Clebsch-Gordan coefficients of the group are only needed to actually convert a tensor to an Array. For working with tensors with SU₂Space indices, e.g. contracting or factorizing them, the Clebsch-Gordan coefficients are never needed explicitly. Instead, recoupling relations are used to symbolically manipulate the basis of fusion trees, and this only requires what is known as the topological data of the group (or its representation theory).

    In fact, this formalism extends beyond the case of group representations on vector spaces, and can also deal with super vector spaces (to describe fermions) and more general (unitary) fusion categories. Support for all of these generalizations is present in TensorKit.jl. Indeed, all of these concepts will be explained throughout the remainder of this manual, including several details regarding their implementation. However, to just use tensors and their manipulations (contractions, factorizations, ...) in higher level algorithms (e.g. tensoer network algorithms), one does not need to know or understand most of these details, and one can immediately refer to the general interface of the TensorMap type, discussed on the last page. Adhering to this interface should yield code and algorithms that are oblivious to the underlying symmetries and can thus work with arbitrary symmetric tensors.

    + 0.0 0.0 0.0 0.0 0.0 0.0 0.400396 + 0.0 0.0 0.0 0.0 0.0 0.0 2.13805 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 -0.620208 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 -0.219207 + -0.199474 1.37731 0.0 0.0 0.0 0.219207 0.0
    julia> norm(A) ≈ norm(convert(Array, A))true

    In this case, the full Array representation of the tensor has again many zeros, but it is less obvious to recognize the dense blocks, as there are additional zeros and the numbers in the original tensor data do not match with those in the Array. The reason is of course that the original tensor data now needs to be transformed with a construction known as fusion trees, which are made up out of the Clebsch-Gordan coefficients of the group. Indeed, note that the non-zero blocks are also no longer labeled by a list of sectors, but by pair of fusion trees. This will be explained further in the manual. However, the Clebsch-Gordan coefficients of the group are only needed to actually convert a tensor to an Array. For working with tensors with SU₂Space indices, e.g. contracting or factorizing them, the Clebsch-Gordan coefficients are never needed explicitly. Instead, recoupling relations are used to symbolically manipulate the basis of fusion trees, and this only requires what is known as the topological data of the group (or its representation theory).

    In fact, this formalism extends beyond the case of group representations on vector spaces, and can also deal with super vector spaces (to describe fermions) and more general (unitary) fusion categories. Support for all of these generalizations is present in TensorKit.jl. Indeed, all of these concepts will be explained throughout the remainder of this manual, including several details regarding their implementation. However, to just use tensors and their manipulations (contractions, factorizations, ...) in higher level algorithms (e.g. tensoer network algorithms), one does not need to know or understand most of these details, and one can immediately refer to the general interface of the TensorMap type, discussed on the last page. Adhering to this interface should yield code and algorithms that are oblivious to the underlying symmetries and can thus work with arbitrary symmetric tensors.