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] =
 1.1909467621675014  -0.1980216848385685
 0.3866062942154062  -0.6255610704781983
 1.5458363611046138  -1.1241406485095589

[:, :, 2] =
 -2.09838497636538    -0.0544886749778529
  1.4088603268667461   0.3620835076807569
 -1.0821770177731378   1.1320137245647257

[:, :, 3] =
 0.5915603350950904    0.6540088016204548
 0.17161897685222333   0.2403434629834277
 0.4154575392608715   -1.0037728536801098

[:, :, 4] =
 -0.2564722406120777   -0.6100144305310413
  0.7551451814224991   -0.4726087578258292
 -0.40547340393636305  -0.5748697919432528

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] = 0.4359466270301894 -1.5307218657794777 -3.29681721138514 0.8855091074704398 -0.7701995109798603 0.4264999095958658 [:, :, 2] = -0.8225454179628131 -2.4189234864769578 -0.7733330594235909 5.928215089477795 -1.3780307037632782 -1.21093523181715 [:, :, 3] = -0.22852265225535928 -1.8283021224531453 -0.5743998208571554 -2.3766590727012296 3.0988690421333875 -0.5577523484427551 [:, :, 4] = -3.474512291406764 2.7208071105433564 3.25664653749426 3.4040937104741746 -0.13049805926169525 2.630706848223707

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)-4.148344455315364
julia> scalarAA = dot(A,A)18.397839655274442
julia> normA² = norm(A)^218.39783965527444

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.04700419310827954  0.6870472608474799
 -0.9437058486361786   0.6549302249716469
  0.25885036909677867  1.1543408507076438

[:, :, 2, 1] =
  0.5050163031767352  -1.1366202329627773
 -0.8032051012175725  -0.7673620314591589
 -0.6989185313821851  -0.2695920542626215

[:, :, 1, 2] =
 -0.8738350325221586  -0.5665673918655978
 -0.5481800414575368   0.051542873175201204
 -2.0950521622888356   0.5032581819482097

[:, :, 2, 2] =
 -5.217348054341856   -1.7616351533615593
  4.352302540016897   -0.39569106079983507
 -2.7522043947991577   2.228931634632314

[:, :, 1, 3] =
  0.6440850445187914    0.8791756827426592
 -0.48999538721941466   0.5291989756837567
 -0.12313306144636614  -0.8625298656019742

[:, :, 2, 3] =
 1.6498290793792592    -0.7662322007205956
 0.029102911877970553  -1.061751533620897
 0.8978779698539631    -1.8977158655405697
julia> @tensor d = A[a,b,c]*A[a,b,c]18.397839655274442
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.32092849589137457 0.5309423075028004 … 0.013267064869229806 -0.15220651837552018 -0.3286241404338652 -0.2331871345480866 -0.49075698275873775 0.3730833883552678 0.05430622372858005 [:, :, 2] = 0.08895893393725302 -0.3350811044087888 … -0.3076985352877026 -0.2192641258403974 0.36887794646989924 -0.09702436374213919 -0.2678827271383427 0.3400801566973747 -0.3142438042482053

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}()):
 -0.5548336893525098
  0.9857413301971545
  0.40805003294147346
julia> M₁ = randn(ℝ^4, ℝ^3)TensorMap(ℝ^4 ← ℝ^3): 2.0480324140828707 -0.3636798891683039 -0.8804984134166259 -0.028989636203640944 0.1829681399745311 0.49268567445176975 -0.008980230542526843 1.7662389659206337 1.5118624651809476 -0.5574004175333803 -2.135370856201019 0.6285338967069902
julia> M₂ = randn(ℝ^4 → ℝ^2) # alternative syntax for randn(ℝ^2, ℝ^4)TensorMap(ℝ^2 ← ℝ^4): 0.7186473627767316 -2.862457753046752 … 0.6025543662974815 -1.2674409793784032 1.3789870402323376 0.10304536033072394
julia> w = M₁ * v # matrix vector productTensorMap(ℝ^4 ← ProductSpace{CartesianSpace, 0}()): -1.854099084533413 0.39748409017988334 2.362952810875861 -1.5391855008930344
julia> M₃ = M₂ * M₁ # matrix matrix productTensorMap(ℝ^2 ← ℝ^3): 1.2064484476056223 0.3832357818794188 0.43710079524400436 -2.684538357452373 -1.2052651067614397 0.4062934347051874
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.3078404228405173   -0.5469224989805378   -0.22639990521733086
 -0.5469224989805378    0.9716859700588556    0.40223178225872075
 -0.22639990521733086   0.40223178225872075   0.16650482938353758
julia> M₁′ = M₁ ⊗ M₁TensorMap((ℝ^4 ⊗ ℝ^4) ← (ℝ^3 ⊗ ℝ^3)): [:, :, 1, 1] = 4.194436769134112 -0.05937171461752695 … -1.141574122731689 -0.05937171461752695 0.0008403990072194497 0.01615883532405026 -0.01839180323703198 0.0002603336164526783 0.005005584253950477 -1.141574122731689 0.01615883532405026 0.3106952254663867 [:, :, 2, 1] = -0.7448282013667523 0.01054294768156959 … 0.20271532207090606 0.37472468141229154 -0.0053041798147185105 -0.10198651761710961 3.6173146532216687 -0.05120262507073414 -0.9845023370678871 -4.373308729587579 0.061903624281124815 1.1902566068350597 [:, :, 3, 1] = -1.8032892912257898 0.02552532868283103 … 0.49079018327590623 1.0090362312315053 -0.01428277846510228 -0.2746232006521315 3.096343334325816 -0.04382834285553544 -0.8427127693449058 1.2872577938057308 -0.018220969007192483 -0.3503450564583589 [:, :, 1, 2] = -0.7448282013667523 0.37472468141229154 … -4.373308729587579 0.01054294768156959 -0.0053041798147185105 0.061903624281124815 0.00326592924841198 -0.001643096078908611 0.019176122582478087 0.20271532207090606 -0.10198651761710961 1.1902566068350597 [:, :, 2, 2] = 0.13226306178546982 -0.06654183286726818 … 0.7765914363164128 -0.06654183286726818 0.033477340245739604 -0.3907048337149224 -0.6423455913707558 0.32316545834503757 -3.771575212913546 0.7765914363164128 -0.3907048337149224 4.5598086935126725 [:, :, 3, 2] = 0.32021956540422597 -0.16110315695336577 … 1.8801906509410993 -0.17917987147943068 0.09014578144653766 -1.052066630492052 -0.5498339737747258 0.2766226631514673 -3.228387046731623 -0.22858513789292037 0.11500167799142205 -1.3421529651625685 [:, :, 1, 3] = -1.8032892912257898 1.0090362312315053 … 1.2872577938057308 0.02552532868283103 -0.01428277846510228 -0.018220969007192483 0.007907078744810411 -0.00442443094157722 -0.005644379296221526 0.49079018327590623 -0.2746232006521315 -0.3503450564583589 [:, :, 2, 3] = 0.32021956540422597 -0.17917987147943068 … -0.22858513789292037 -0.16110315695336577 0.09014578144653766 0.11500167799142205 -1.55517060720774 0.8702006361676038 1.1101410597658208 1.8801906509410993 -1.052066630492052 -1.3421529651625685 [:, :, 3, 3] = 0.7752774560291955 -0.43380895466788355 … -0.5534230988290744 -0.43380895466788355 0.24273917380999524 0.30966964681488246 -1.3311925018959732 0.7448729783359904 0.9502568065252173 -0.5534230988290744 0.30966964681488246 0.39505485930967343
julia> w′ = M₁′ * v′TensorMap((ℝ^4 ⊗ ℝ^4) ← ProductSpace{CartesianSpace, 0}()): 3.437683415267641 -0.7369748877191185 … 2.853802428132878 -0.7369748877191183 0.15799360194612963 -0.6118017484405358 -4.381148643440589 0.9392361481689897 -3.6370227057945654 2.8538024281328775 -0.6118017484405358 2.369092006159341
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): 0.9999999999999999 -5.2479905361894054e-17 -5.2479905361894054e-17 0.9999999999999999
julia> U'*U ≈ one(U'*U)true
julia> P = U*U' # should be a projectorTensorMap((ℝ^3 ⊗ ℝ^4) ← (ℝ^3 ⊗ ℝ^4)): [:, :, 1, 1] = 0.11090879140235257 -0.20020297398269263 … -0.031630312846623466 0.029341906121673113 0.1382798399739615 0.06620521238729604 0.13366733849851994 -0.08947992247390124 -0.045383208521060835 [:, :, 2, 1] = 0.029341906121673113 -0.007341614639438059 … 0.06544791660942113 0.07124358111655119 -0.030862964204936737 0.05676656418066914 0.13343348370791308 -0.13135310187921262 0.060636631799394464 [:, :, 3, 1] = 0.13366733849851994 -0.17080120478748212 … 0.07591621804405568 0.13343348370791308 0.06245856133945917 0.1404293657270816 0.3126035716255358 -0.27419487780836527 0.057529328766277844 [:, :, 1, 2] = -0.20020297398269263 0.3941790804282118 … 0.11014801106462827 -0.007341614639438059 -0.2980844891182606 -0.09129788433965103 -0.17080120478748212 0.084131320610639 0.1341306327193235 [:, :, 2, 2] = 0.1382798399739615 -0.2980844891182606 … -0.11786308161745453 -0.030862964204936737 0.2440647650677466 0.04084077359635217 0.06245856133945917 0.0028438620292853428 -0.13376394529498076 [:, :, 3, 2] = -0.08947992247390124 0.084131320610639 … -0.09969244458125318 -0.13135310187921262 0.0028438620292853428 -0.11999430710294565 -0.27419487780836527 0.2548457276459585 -0.0866073322324709 [:, :, 1, 3] = 0.06401930808412591 -0.1767188323632816 … -0.11720477988418324 -0.0681564008252583 0.17022701126076498 -0.01439940220481046 -0.05430306332053366 0.09269062448484972 -0.12357116213308088 [:, :, 2, 3] = 0.019173416447196865 -0.056466162923550174 … -0.04082942838961266 -0.025337837043404813 0.05621505486946289 -0.00735799214883745 -0.02387260097323118 0.03611506455360001 -0.042645125087182385 [:, :, 3, 3] = 0.027596937070992665 0.026161453309900343 … 0.11505477783196491 0.113015296866305 -0.07790995517109754 0.08183857623429179 0.196576056189072 -0.20158421112981081 0.10967959501200822 [:, :, 1, 4] = -0.031630312846623466 0.11014801106462827 … 0.0948544036284419 0.06544791660942113 -0.11786308161745453 0.026760545769958222 0.07591621804405568 -0.09969244458125318 0.09741284248341828 [:, :, 2, 4] = 0.06620521238729604 -0.09129788433965103 … 0.026760545769958222 0.05676656418066914 0.04084077359635217 0.06378996687831438 0.1404293657270816 -0.11999430710294565 0.017825792467696566 [:, :, 3, 4] = -0.045383208521060835 0.1341306327193235 … 0.09741284248341828 0.060636631799394464 -0.13376394529498076 0.017825792467696566 0.057529328766277844 -0.0866073322324709 0.10169833444404297
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] =
 1.1909467621675014  -0.1980216848385685
 0.3866062942154062  -0.6255610704781983
 1.5458363611046138  -1.1241406485095589

[:, :, 2] =
 -2.09838497636538    -0.0544886749778529
  1.4088603268667461   0.3620835076807569
 -1.0821770177731378   1.1320137245647257

[:, :, 3] =
 0.5915603350950904    0.6540088016204548
 0.17161897685222333   0.2403434629834277
 0.4154575392608715   -1.0037728536801098

[:, :, 4] =
 -0.2564722406120777   -0.6100144305310413
  0.7551451814224991   -0.4726087578258292
 -0.40547340393636305  -0.5748697919432528
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.3157412793787837 - 0.06873511891044494im  …   0.11194942152437505 - 0.46678295352588595im
 0.12420865722596929 - 0.8375337424540156im        0.8014931776620361 - 1.1719182732689537im
 0.07624212221340586 - 0.5092166142455125im      -0.27785615681636644 - 1.5634379706629542im

[:, :, 2] =
 0.29569846731302935 + 0.18433484107229958im  …  -0.663265038189869 - 0.27019282881266893im
 0.45190712626771823 + 0.1637757290987184im      1.2028929725914832 - 0.6283225453112063im
  0.3814878160880682 - 0.3910303172001888im      0.8064648224683753 + 0.1930241801033864im

[:, :, 3] =
   0.178486261390612 + 0.4882943729471115im  …  0.8946513580772116 + 0.17651212126843877im
 0.31900581070474804 - 0.9840568410559533im     0.3026901671767895 - 0.8690666162931345im
  0.5506388511952487 - 1.866959671322739im      0.7978709272219702 - 0.37877547823340807im

[:, :, 4] =
  0.1770419942326956 - 1.3485636648552453im  …    0.19676920130877382 + 0.19515841347790214im
 -0.5498676668182686 - 1.0291646750126122im        1.1020486326912713 + 0.23008940884938503im
   1.503422671647823 - 0.5092542458460407im     -0.007732459611605651 - 1.5980770835691216im

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.8371758888659597 + 0.45722210159873655im  …  -0.42892061572841217 + 0.26187746603342316im
  0.4656821056863552 - 0.6028605494820921im       0.20978072024984024 - 0.20165259191460477im
 -0.6412937712781317 + 0.745789579549079im         0.1268716643675242 + 0.02744152040198703im

[:, :, 2] =
 0.07552970170803962 + 0.7670425114953349im  …  0.27606583124442546 + 0.12762722415994235im
  0.5944056338604664 + 0.7312394829395793im     -0.9600843707692267 - 1.3373437631236722im
 0.32826534636304294 + 0.7938687194632202im     -1.3714549673421053 + 1.0334351692419843im

[:, :, 3] =
  -0.9596325671417242 + 0.2638946576256072im   …  -0.44354812547739497 - 0.7550471749083206im
  -0.3797626039240774 + 0.12141999394099065im      -0.5614213390837959 + 0.3661952971264541im
 -0.06982715354536077 + 0.5723636439618596im      -0.19528114489600257 - 0.21753350513551545im

[:, :, 4] =
 -0.7727553534053477 + 0.064414863142544im    …  -0.17210637832830963 + 1.2897707014525663im
  0.7543807859336761 + 0.15435634539741033im     -0.26787677006786165 + 1.4469223197588215im
 0.43018379991120526 + 0.08616604386639397im     -0.12687087916323148 - 0.28410405220152934im

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] = 0.6976566523935508 + 0.4676146875884277im … -0.3345924886340118 - 0.4056518084962238im 2.077425818066141 - 0.006178780260540417im 1.770054298666509 - 0.29210782591076917im -1.7406343256297405 - 0.32294345253232004im 3.3165823563742256 - 0.943215976200864im [:, :, 2] = -2.793361993688426 + 0.7239519360098288im … -3.2434778710378573 + 1.273138038245358im -1.9145118604565816 + 0.9730527946659072im -2.4931929864963935 + 0.26568738910919454im 5.581119543179624 - 1.7117308976826673im -1.9606561245016039 - 1.074744261659027im [:, :, 3] = -2.820542153688423 - 0.1415053684016232im … 1.1380155450882201 - 0.5660980039349628im 0.8110287044975093 - 0.6781461874243975im 2.353706904499233 - 1.4317900436040158im -0.5967238018400993 - 0.06203190302432404im -0.871861187392051 + 0.15332515671281866im [:, :, 4] = 2.3176088263592503 - 1.535002934045922im … -0.38702751670917757 - 0.460984197446194im -1.308309174143249 + 1.1236456911323445im -0.5831434364471437 - 0.5442860127275986im 3.303417772239276 - 0.6544830212426092im -0.5090827360219431 + 0.12694889306827972im
julia> scalarBA = dot(B,A)2.991619357042424 + 3.3348497525901344im
julia> scalarAA = dot(A,A)18.908986832255266 + 0.0im
julia> normA² = norm(A)^218.908986832255266
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]18.908986832255266 + 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.8142704813864723 - 0.9205008523676153im   …  -0.8374533410977945 - 0.8259815684586572im
 -0.22663602475228942 + 1.6040250860962597im      0.17096016487531412 + 0.4176473123077263im
   0.8902806059877699 - 0.30148631453382857im      0.8919535831979466 - 0.9998046424649578im
julia> m2 = permute(m, (1,2), ())TensorMap((ℂ^3 ⊗ (ℂ^4)') ← ProductSpace{ComplexSpace, 0}()): -0.8142704813864723 - 0.9205008523676153im … -0.8374533410977945 - 0.8259815684586572im -0.22663602475228942 + 1.6040250860962597im 0.17096016487531412 + 0.4176473123077263im 0.8902806059877699 - 0.30148631453382857im 0.8919535831979466 - 0.9998046424649578im
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.41722668076016617 0.17803662741322004 0.947623790482961 -1.0642046878166653 2.2121631350973745 0.954223566821838 -0.424599971528189 0.09887984309830826 -1.6878506863674323 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](1), Irrep[ℤ₂](0)) ← (): [:, :, 1] = -0.7795964190730319 1.466917223533158 0.9934464231001897 -0.260574957790613 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](0), Irrep[ℤ₂](1)) ← (): [:, :, 1] = -1.6226148805505491 0.2189384811010889 -0.6397792546253543 1.5086260326042817 -0.05374915165532544 -1.1505042297614492 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](1), Irrep[ℤ₂](1)) ← (): [:, :, 1] = -0.45027822921375005 1.940950002815016 -1.811199824913386 -0.9281705700982836 -0.6154533106302291 1.2730224193785438
julia> convert(Array, A)5×5×2 Array{Float64, 3}: [:, :, 1] = -0.417227 0.178037 0.947624 0.0 0.0 -1.0642 2.21216 0.954224 0.0 0.0 -0.4246 0.0988798 -1.68785 0.0 0.0 0.0 0.0 0.0 -0.779596 1.46692 0.0 0.0 0.0 0.993446 -0.260575 [:, :, 2] = 0.0 0.0 0.0 -0.450278 1.94095 0.0 0.0 0.0 -1.8112 -0.928171 0.0 0.0 0.0 -0.615453 1.27302 -1.62261 0.218938 -0.639779 0.0 0.0 1.50863 -0.0537492 -1.1505 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)) ← (): 2.1494840568492415 3.1233139559301923 1.1551252403745509 3.0618616003344075 0.8077289393435669 -0.4883430155816573 0.24289761199910903 3.7467170188302505 2.4195774624128177 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](1)) ← (): 0.31037489049384054 3.434144208091934 1.964679596098829 -1.8731936062011316
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),): 1.0 1.1835815245657489e-16 -4.179636093188065e-16 1.1835815245657489e-16 0.9999999999999999 1.122801716029087e-16 -4.179636093188065e-16 1.122801716029087e-16 0.9999999999999994 * Data for sector (Irrep[ℤ₂](1),) ← (Irrep[ℤ₂](1),): 1.0000000000000004 -1.2162608267632146e-16 -1.2162608267632146e-16 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.16087918951096303 0.07348899353539533 -0.24262308706990388 [:, :, 2, 1] = 0.07348899353539533 0.992983187279827 0.006967538312115965 [:, :, 3, 1] = -0.24262308706990388 0.006967538312115965 0.5786688077385277 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](0)): [:, :, 1, 1] = -0.015777895542947278 -0.26548359526592036 [:, :, 2, 1] = 0.016641516235438983 0.035234333196922984 [:, :, 3, 1] = 0.3706832524019573 0.21792346387721628 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](1)): [:, :, 1, 1] = -0.015777895542947278 0.016641516235438983 0.3706832524019573 [:, :, 2, 1] = -0.26548359526592036 0.035234333196922984 0.21792346387721628 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](1)): [:, :, 1, 1] = 0.598742810809664 -0.319871695608935 [:, :, 2, 1] = -0.319871695608935 0.6687260046610175 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](0)): [:, :, 1, 1] = 0.309973065112414 -0.14342649709897587 [:, :, 2, 1] = -0.14342649709897587 0.18087852416906866 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](1), Irrep[ℤ₂](0)): [:, :, 1, 1] = 0.35006614567047006 0.04204808124384406 0.2626866126514236 [:, :, 2, 1] = -0.0841257338503025 -0.3281187796022809 -0.11336335814478907 * Data for sector (Irrep[ℤ₂](1), Irrep[ℤ₂](0)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](1)): [:, :, 1, 1] = 0.35006614567047006 -0.0841257338503025 [:, :, 2, 1] = 0.04204808124384406 -0.3281187796022809 [:, :, 3, 1] = 0.2626866126514236 -0.11336335814478907 * Data for sector (Irrep[ℤ₂](0), Irrep[ℤ₂](1)) ← (Irrep[ℤ₂](0), Irrep[ℤ₂](1)): [:, :, 1, 1] = 0.44827249882082115 -0.16235669242872544 0.30222694222211854 [:, :, 2, 1] = -0.16235669242872544 0.8376773929823574 0.013576009290798774 [:, :, 3, 1] = 0.30222694222211854 0.013576009290798774 0.22319851891533946
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] = 1.0231316138258526 -0.6302019550706186 0.5528129624245822 -1.4633442373017786 [:, :, 2] = 1.365411535987356 0.16042688363152444 0.9285662942875414 0.2233983453581968 * Data for sector (Irrep[U₁](-1), Irrep[U₁](1)) ← (Irrep[U₁](0),): [:, :, 1] = 0.9047753347574209 [:, :, 2] = 0.8453437942767752 * Data for sector (Irrep[U₁](1), Irrep[U₁](-1)) ← (Irrep[U₁](0),): [:, :, 1] = -0.06929199499476302 [:, :, 2] = 1.0093055878856887 * Data for sector (Irrep[U₁](1), Irrep[U₁](0)) ← (Irrep[U₁](1),): [:, :, 1] = -1.3629108220590873 -0.32090029104866813 * Data for sector (Irrep[U₁](0), Irrep[U₁](1)) ← (Irrep[U₁](1),): [:, :, 1] = -1.2548933353711242 0.5514664406787824 * Data for sector (Irrep[U₁](-1), Irrep[U₁](0)) ← (Irrep[U₁](-1),): [:, :, 1] = -1.0752489575269488 0.3259448578274938 * Data for sector (Irrep[U₁](0), Irrep[U₁](-1)) ← (Irrep[U₁](-1),): [:, :, 1] = -1.5858941821211692 1.4924258974976108
julia> dim(A)20
julia> convert(Array, A)4×4×4 Array{Float64, 3}: [:, :, 1] = 1.02313 -0.630202 0.0 0.0 0.552813 -1.46334 0.0 0.0 0.0 0.0 0.0 -0.069292 0.0 0.0 0.904775 0.0 [:, :, 2] = 1.36541 0.160427 0.0 0.0 0.928566 0.223398 0.0 0.0 0.0 0.0 0.0 1.00931 0.0 0.0 0.845344 0.0 [:, :, 3] = 0.0 0.0 -1.25489 0.0 0.0 0.0 0.551466 0.0 -1.36291 -0.3209 0.0 0.0 0.0 0.0 0.0 0.0 [:, :, 4] = 0.0 0.0 0.0 -1.58589 0.0 0.0 0.0 1.49243 0.0 0.0 0.0 0.0 -1.07525 0.325945 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] = 0.33078206624120104 0.9448503210830907 -1.3260332035389075 0.5913820403830272 [:, :, 2] = 0.0573741312314979 -0.37680409267844683 -0.9911093422273227 -0.9424455071256382 * Data for sector (Irrep[U₁ × ℤ₂](1, 1), Irrep[U₁ × ℤ₂](0, 0)) ← (Irrep[U₁ × ℤ₂](1, 1),): [:, :, 1] = 1.1323585613000189 -2.4489902232376854 * Data for sector (Irrep[U₁ × ℤ₂](0, 0), Irrep[U₁ × ℤ₂](1, 1)) ← (Irrep[U₁ × ℤ₂](1, 1),): [:, :, 1] = 0.9615262064815245 0.5620279243693597 * Data for sector (Irrep[U₁ × ℤ₂](-1, 0), Irrep[U₁ × ℤ₂](0, 0)) ← (Irrep[U₁ × ℤ₂](-1, 0),): [:, :, 1] = 0.11134178053068508 -0.3579536569525758 * Data for sector (Irrep[U₁ × ℤ₂](0, 0), Irrep[U₁ × ℤ₂](-1, 0)) ← (Irrep[U₁ × ℤ₂](-1, 0),): [:, :, 1] = 0.3432968133784187 -0.330055114112539
julia> dim(A)16
julia> convert(Array, A)4×4×4 Array{Float64, 3}: [:, :, 1] = 0.330782 0.94485 0.0 0.0 -1.32603 0.591382 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 [:, :, 2] = 0.0573741 -0.376804 0.0 0.0 -0.991109 -0.942446 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.961526 0.0 0.0 0.0 0.562028 0.0 1.13236 -2.44899 0.0 0.0 0.0 0.0 0.0 0.0 [:, :, 4] = 0.0 0.0 0.0 0.343297 0.0 0.0 0.0 -0.330055 0.0 0.0 0.0 0.0 0.111342 -0.357954 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] =
 -0.9465263749910445   0.8085177535859962
 -1.323783399004792   -1.138466202179086

[:, :, 2] =
 0.0834912113020143   0.1428184888550303
 1.6217491564492026  -1.1739366686684505
* Data for fusiontree FusionTree{Irrep[SU₂]}((1/2, 1/2), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()):
[:, :, 1] =
 -0.8543647035142158

[:, :, 2] =
 0.39763469921816974
* Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 0, (false, false), ()) ← FusionTree{Irrep[SU₂]}((0,), 0, (false,), ()):
[:, :, 1] =
 0.12258129276642278

[:, :, 2] =
 -0.8260060213933317
* Data for fusiontree FusionTree{Irrep[SU₂]}((1/2, 0), 1/2, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()):
[:, :, 1] =
 0.5134757159389628  -0.37204195126446726
* Data for fusiontree FusionTree{Irrep[SU₂]}((0, 1/2), 1/2, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()):
[:, :, 1] =
 0.05993478612750462
 0.6448834968861337
* Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1/2), 1/2, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()):
[:, :, 1] =
 0.2072710483449986
* Data for fusiontree FusionTree{Irrep[SU₂]}((1/2, 1), 1/2, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1/2,), 1/2, (false,), ()):
[:, :, 1] =
 -0.17888131541225394
* Data for fusiontree FusionTree{Irrep[SU₂]}((1, 0), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()):
[:, :, 1] =
 0.08175978481044663  -0.8441153472900973
* Data for fusiontree FusionTree{Irrep[SU₂]}((1/2, 1/2), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()):
[:, :, 1] =
 -0.6900267464698909
* Data for fusiontree FusionTree{Irrep[SU₂]}((0, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()):
[:, :, 1] =
  0.7079195927061385
 -0.044285281838190346
* Data for fusiontree FusionTree{Irrep[SU₂]}((1, 1), 1, (false, false), ()) ← FusionTree{Irrep[SU₂]}((1,), 1, (false,), ()):
[:, :, 1] =
 -0.7697934578814211
julia> dim(A)24
julia> convert(Array, A)7×7×7 Array{Float64, 3}: [:, :, 1] = -0.946526 0.808518 0.0 0.0 0.0 0.0 0.0 -1.32378 -1.13847 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.604127 0.0 0.0 0.0 0.0 0.0 0.604127 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0707723 0.0 0.0 0.0 0.0 0.0 -0.0707723 0.0 0.0 0.0 0.0 0.0 0.0707723 0.0 0.0 [:, :, 2] = 0.0834912 0.142818 0.0 0.0 0.0 0.0 0.0 1.62175 -1.17394 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.28117 0.0 0.0 0.0 0.0 0.0 -0.28117 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.476895 0.0 0.0 0.0 0.0 0.0 0.476895 0.0 0.0 0.0 0.0 0.0 -0.476895 0.0 0.0 [:, :, 3] = 0.0 0.0 0.0599348 0.0 0.0 0.0 0.0 0.0 0.0 0.644883 0.0 0.0 0.0 0.0 0.513476 -0.372042 0.0 0.0 0.0 -0.103277 0.0 0.0 0.0 0.0 0.0 0.146056 0.0 0.0 0.0 0.0 0.0 0.169236 0.0 0.0 0.0 0.0 0.0 -0.119668 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.0599348 0.0 0.0 0.0 0.0 0.0 0.0 0.644883 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.146056 0.513476 -0.372042 0.0 0.0 0.0 0.103277 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.119668 0.0 0.0 0.0 0.0 0.0 -0.169236 0.0 0.0 0.0 0.0 [:, :, 5] = 0.0 0.0 0.0 0.0 0.70792 0.0 0.0 0.0 0.0 0.0 0.0 -0.0442853 0.0 0.0 0.0 0.0 -0.690027 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0817598 -0.844115 0.0 0.0 0.0 -0.544326 0.0 0.0 0.0 0.0 0.0 0.544326 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.70792 0.0 0.0 0.0 0.0 0.0 0.0 -0.0442853 0.0 0.0 0.0 0.0 -0.487923 0.0 0.0 0.0 0.0 0.0 -0.487923 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.544326 0.0817598 -0.844115 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.544326 0.0 0.0 [:, :, 7] = 0.0 0.0 0.0 0.0 0.0 0.0 0.70792 0.0 0.0 0.0 0.0 0.0 0.0 -0.0442853 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.690027 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.544326 0.0817598 -0.844115 0.0 0.0 0.0 0.544326 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.