Vector spaces
From the Introduction, it should be clear that an important aspect in the definition of a tensor (map) is specifying the vector spaces and their structure in the domain and codomain of the map. The starting point is an abstract type VectorSpace
abstract type VectorSpace endTechnically speaking, this name does not capture the full generality that TensorKit.jl supports, as instances of subtypes of VectorSpace can encode general objects in linear monoidal categories, which are not necessarily vector spaces. However, in order not to make the remaining discussion to abstract or complicated, we will simply use the nomenclature of vector spaces. In particular, we define two abstract subtypes
abstract type ElementarySpace <: VectorSpace end
const IndexSpace = ElementarySpace
abstract type CompositeSpace{S<:ElementarySpace} <: VectorSpace endHere, ElementarySpace is a super type for all vector spaces (objects) that can be associated with the individual indices of a tensor, as hinted to by its alias IndexSpace.
On the other hand, subtypes of CompositeSpace{S} where S<:ElementarySpace are composed of a number of elementary spaces of type S. So far, there is a single concrete type ProductSpace{S,N} that represents the tensor product of N vector spaces of a homogeneous type S. Its properties are discussed in the section on Composite spaces, together with possible extensions for the future.
Throughout TensorKit.jl, the function spacetype returns the type of ElementarySpace associated with e.g. a composite space or a tensor. It works both on instances and in the type domain. Its use will be illustrated below.
Fields
Vector spaces (and linear categories more generally) are defined over a field of scalars $𝔽$. We define a type hierarchy to specify the scalar field, but so far only support real and complex numbers, via
abstract type Field end
struct RealNumbers <: Field end
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 ∈ ℝtruejulia> 5.0 ∈ ℂtruejulia> 5.0 + 1.0 * im ∈ ℝfalsejulia> Float64 ⊆ ℝtruejulia> ComplexF64 ⊆ ℂtruejulia> ℝ ⊆ ℂtruejulia> ℂ ⊆ ℝ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.field — Functionfield(a) -> Type{𝔽<:Field}
field(::Type{T}) -> Type{𝔽<:Field}Return the type of field over which object a (e.g. a vector space or a tensor) is defined. This also works in type domain.
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 associated vector spaces, such as 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
TensorKitSectors.dim — Methoddim(V::VectorSpace) -> IntReturn the total dimension of the vector space V as an Int.
dim(P::ProductSpace{S, N}, s::NTuple{N, sectortype(S)}) where {S<:ElementarySpace}
-> IntReturn 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))`.
TensorKit.field — Methodfield(a) -> Type{𝔽<:Field}
field(::Type{T}) -> Type{𝔽<:Field}Return the type of field over which object a (e.g. a vector space or a tensor) is defined. This also works in type domain.
TensorKitSectors.dual — Methoddual(V::VectorSpace) -> VectorSpaceReturn the dual space of V; also obtained via V'. This should satisfy dual(dual(V)) == V. It is assumed that typeof(V) == typeof(V').
Base.conj — Methodconj(V::S) where {S<:ElementarySpace} -> SReturn 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)).
For convenience, the dual of a space V can also be obtained as V'. Furthermore, it is sometimes necessary to test whether a space is a dual or conjugate space, for which the methods isdual(::ElementarySpace) and isconj(::ElementarySpace) should be implemented.
We furthermore define a trait type
TensorKit.InnerProductStyle — TypeInnerProductStyle(V::VectorSpace) -> ::InnerProductStyle
InnerProductStyle(S::Type{<:VectorSpace}) -> ::InnerProductStyleReturn the type of inner product for vector spaces, which can be either
NoInnerProduct(): no mapping fromdual(V)toconj(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.
to denote for a vector space V whether it has an inner product and thus a canonical mapping from dual(V) to V (for real fields 𝔽 ⊆ ℝ) or from dual(V) to conj(V) (for complex fields). This mapping is provided by the metric, but no further support for working with vector spaces with general metrics is currently implemented.
A number of concrete elementary spaces are implemented in TensorKit.jl. 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
endHowever, as the InnerProductStyle of GeneralSpace is currently set to NoInnerProduct(), this type of vector space is currently quite limited, though it supports constructing tensors and contracting them. However, most tensor factorizations will depend on the presence of an Euclidean inner product.
Spaces with the EuclideanInnerProduct() style, i.e. with a standard Euclidean metric, have the natural isomorphisms dual(V) == V (for 𝔽 == ℝ) or dual(V) == conj(V) (for 𝔽 == ℂ). In the language of the appendix on categories, this trait represents dagger or unitary categories, and these vector spaces support an adjoint operation.
In particular, two concrete types are provided:
struct CartesianSpace <: ElementarySpace
d::Int
end
struct ComplexSpace <: ElementarySpace
d::Int
dual::Bool
endThey represent the Euclidean spaces $ℝ^d$ or $ℂ^d$ without further inner structure. They can be created using the syntax CartesianSpace(d) == ℝ^d and ComplexSpace(d) == ℂ^d, or ComplexSpace(d, true) == ComplexSpace(d; dual = true) == (ℂ^d)' for the dual space of the latter. Note that the brackets are required because of the precedence rules, since d' == d for d::Integer.
Some examples:
julia> dim(ℝ^10)10julia> (ℝ^10)' == ℝ^10truejulia> isdual((ℂ^5))falsejulia> isdual((ℂ^5)')truejulia> isdual((ℝ^5)')falsejulia> dual(ℂ^5) == (ℂ^5)' == conj(ℂ^5) == ComplexSpace(5; dual = true)truejulia> field(ℂ^5)ℂjulia> field(ℝ^3)ℝjulia> typeof(ℝ^3)CartesianSpacejulia> spacetype(ℝ^3)CartesianSpacejulia> InnerProductStyle(ℝ^3)EuclideanInnerProduct()julia> InnerProductStyle(ℂ^5)EuclideanInnerProduct()
For ℂ^n the dual space is equal (or naturally isomorphic) to the conjugate space, but not to the space itself. This means that even for ℂ^n, arrows matter in the diagrammatic notation for categories or for tensors, and in particular that a contraction between two tensor indices will check that one is living in the space and the other in the dual space. This is in contrast with several other software packages, especially in the context of tensor networks, where arrows are only introduced when discussing symmetries. We believe that our more puristic approach can be useful to detect errors (e.g. unintended contractions). Only with ℝ^n will their be no distinction between a space and its dual. When creating tensors with indices in ℝ^n that have complex data, a one-time warning will be printed, but most operations should continue to work nonetheless.
One more important concrete implementation of ElementarySpace with a EuclideanInnerProduct() is the GradedSpace type, which is used to represent a graded complex vector space, where the grading is provided by the irreducible representations of a group, or more generally, the simple objects of a unitary fusion category. We refer to the subsection on graded spaces on the next page for further information about GradedSpace.
Operations with elementary spaces
Instances of ElementarySpace support a number of useful operations. Firstly, we define the direct sum of two vector spaces V1 and V2 of the same spacetype (and with the same value of isdual) as V1 ⊕ V2, where ⊕ is obtained by typing \oplus+TAB. zerospace(V) corresponds to the identity or zero element with respect to this direct sum operation, i.e. it corresponds to a zero-dimensional space. Furthermore, unitspace(V) applied to an elementary space returns a one-dimensional space, that is isomorphic to the scalar field underlying the space itself. Finally, we have also introduced the non-standard convention V1 ⊖ V2 (obtained by typing \ominus+TAB.) in order to obtain a space that is isomorphic to the quotient space of V1 by V2, or thus, a particular choice of complement of V2 in V1 such that V1 == V2 ⊕ (V1 ⊖ V2) is satisfied.
Some examples illustrate this better.
julia> ℝ^5 ⊕ ℝ^3ℝ^8julia> ℂ^5 ⊕ ℂ^3ℂ^8julia> ℂ^5 ⊕ (ℂ^3)'ERROR: SpaceMismatch("Direct sum of a vector space and its dual does not exist")julia> zerospace(ℂ^3)ℂ^0julia> unitspace(ℝ^3)ℝ^1julia> ℂ^5 ⊕ unitspace(ComplexSpace)ℂ^6julia> unitspace((ℂ^3)')ℂ^1julia> (ℂ^5) ⊕ unitspace((ℂ^5))ℂ^6julia> (ℂ^5)' ⊕ unitspace((ℂ^5)')ERROR: SpaceMismatch("Direct sum of a vector space and its dual does not exist")julia> (ℝ^5) ⊖ (ℝ^3)ℝ^2julia> (ℂ^5) ⊖ (ℂ^3)ℂ^2julia> (ℂ^5)' ⊖ (ℂ^3)'(ℂ^2)'julia> ℂ^5 == ((ℂ^5) ⊖ (ℂ^3)) ⊕ (ℂ^3) == (ℂ^3) ⊕ ((ℂ^5) ⊖ (ℂ^3))true
Note, finally, that we have defined oplus and ominus as ASCII alternatives for ⊕ and ⊖ respectively.
A second type of operation with elementary spaces is the function flip(V::ElementarySpace), which returns a space that is isomorphic to V but has isdual(flip(V)) == isdual(V'), i.e., if V is a normal space, then 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).
While we provide some trivial examples here, we refer to the section on graded spaces for examples where flip acts non-trivially and produces results that are different than dual.
julia> flip(ℂ^4)(ℂ^4)'julia> flip(ℂ^4) ≅ ℂ^4truejulia> flip(ℂ^4) == ℂ^4false
Finally, we provide two methods infimum(V1, V2) and supremum(V1, V2) for elementary spaces V1 and V2 with the same spacetype and value of isdual. The former returns the "largest" elementary space V::ElementarySpace with the same value of isdual such that we can construct surjective morphisms from both V1 and V2 to V. Similarly, the latter returns the "smallest" elementary space W::ElementarySpace with the same value of isdual such that we can construct injective morphisms from both V1 and V2 to W. For CartesianSpace and ComplexSpace, this simply amounts to the space with minimal or maximal dimension, but it is again more interesting in the case of GradedSpace, as discussed on the next page. It is that case where infimum(V1, V2) might be different from either V1 or V2, and similar for supremum(V1, V2), which justifies the choice of these names over simply min and max. Also note that these methods are a direct consequence of the partial order that we can define between vector spaces of the same spacetype more generally, as discussed below in the subsection "More operations with vector spaces".
Some examples:
julia> infimum(ℝ^5, ℝ^3)ℝ^3julia> supremum(ℂ^5, ℂ^3)ℂ^5julia> supremum(ℂ^5, (ℂ^3)')ERROR: SpaceMismatch("Supremum of space and dual space does not exist")julia> supremum((ℂ^5)', (ℂ^3)')(ℂ^5)'
Composite spaces
Composite spaces are vector spaces that are built up out of individual elementary vector spaces of the same type. The most prominent (and currently only) example is a tensor product of N elementary spaces of the same type S, which is implemented as
struct ProductSpace{S<:ElementarySpace, N} <: CompositeSpace{S}
spaces::NTuple{N, S}
endGiven some V1::S, V2::S, V3::S of the same type S<:ElementarySpace, we can easily construct ProductSpace{S,3}((V1, V2, V3)) as ProductSpace(V1, V2, V3) or using V1 ⊗ V2 ⊗ V3, where ⊗ is simply obtained by typing \otimes+TAB. In fact, for convenience, also the regular multiplication operator * acts as tensor product between vector spaces, and as a consequence so does raising a vector space to a positive integer power, i.e.
julia> V1 = ℂ^2ℂ^2julia> V2 = ℂ^3ℂ^3julia> V1 ⊗ V2 ⊗ V1' == V1 * V2 * V1' == ProductSpace(V1, V2, V1') == ProductSpace(V1, V2) ⊗ V1'truejulia> V1^3(ℂ^2 ⊗ ℂ^2 ⊗ ℂ^2)julia> dim(V1 ⊗ V2)6julia> dims(V1 ⊗ V2)(2, 3)julia> dual(V1 ⊗ V2 ⊗ V1')(ℂ^2 ⊗ (ℂ^3)' ⊗ (ℂ^2)')julia> spacetype(V1 ⊗ V2)ComplexSpacejulia> spacetype(ProductSpace{ComplexSpace,3})ComplexSpace
Here, the newly introduced function dims maps dim to the individual spaces in a ProductSpace and returns the result as a tuple. The rationale for the dual space of a ProductSpace being the tensor product of the dual spaces in reverse order is explained in the subsection on duality in the appendix on category theory.
Following Julia's Base library, the function one applied to an instance of ProductSpace{S,N} or of S<:ElementarySpace itself returns the multiplicative identity for these objects. Similar to Julia Base, one also works in the type domain. The multiplicative identity for vector spaces corresponds to the (monoidal) unit, which is represented as ProductSpace{S,0}(()) and simply printed as one(S) for the specific type S. Note, however, that one(S) is strictly speaking only the multiplicative identity when multiplied with ProductSpace{S,N} instances. For elementary spaces V::S, V ⊗ one(V) will yield ProductSpace{S,1}(V) and not V itself. However, even though V ⊗ one(V) is not strictly equal to V, the object ProductSpace(V), which can also be created as ⊗(V), does mathematically encapsulate the same vector space as V.
julia> one(V1)one(ComplexSpace)julia> typeof(one(V1))ProductSpace{ComplexSpace, 0}julia> V1 * one(V1) == ProductSpace(V1) == ⊗(V1)truejulia> V1 * one(V1) == V1falsejulia> P = V1 * V2;julia> one(P)one(ComplexSpace)julia> one(typeof(P))one(ComplexSpace)julia> P * one(P) == P == one(P) ⊗ Ptrue
In the future, other CompositeSpace types could be added. For example, the wave function of an N-particle quantum system in first quantization would require the introduction of a SymmetricSpace{S,N} or a AntiSymmetricSpace{S,N} for bosons or fermions respectively, which correspond to the symmetric (permutation invariant) or antisymmetric subspace of V^N, where V::S represents the Hilbert space of the single particle system. Other scientific fields, like general relativity, might also benefit from tensors living in subspace with certain symmetries under specific index permutations.
More operations with 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 ⊗ ....
Space of morphisms
As mentioned in the introduction, we define tensor maps as linear maps from a ProductSpace domain to a ProductSpace codomain. The set of all tensor maps with a fixed domain and codomain constitutes a vector space, which we represent with the HomSpace type.
struct HomSpace{S<:ElementarySpace, P1<:CompositeSpace{S}, P2<:CompositeSpace{S}}
codomain::P1
domain::P2
endAside from the standard constructor, a HomSpace instance can be created 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 encode all spaces and generalizations thereof (i.e. objects in linear monoidal categories) that are associated with the indices and the domain and codomain of a tensor map. Even when these generalizations are no longer strictly vector spaces and have unconventional properties (such as non-integer dimensions), the space of tensor maps (homomorphisms) between a given domain and codomain, represented by a HomSpace instance, is always a vector space in the strict mathematical sense (with in particular an integer dimension). Because HomSpace and the different subtypes of VectorSpace represent very different mathematical concepts that do not directly interact, we have chosen to keep them separate in the type hierarchy.
Furthermore, on these HomSpace instances, we define a number of useful methods that are a precursor to the corresponding methods that we will define to manipulate the actual tensors, as illustrated in the following example:
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)ComplexSpacejulia> spacetype(typeof(W))ComplexSpacejulia> W[1]ℂ^3julia> W[2](ℂ^4)'julia> W[3](ℂ^2)'julia> W[4](ℂ^3)'julia> dim(W)72julia> domain(W)(ℂ^2 ⊗ ℂ^3)julia> codomain(W)(ℂ^3 ⊗ (ℂ^4)')julia> numin(W)2julia> numout(W)2julia> numind(W)4julia> numind(W) == numin(W) + numout(W)truejulia> permute(W, ((2, 3), (1, 4)))((ℂ^4)' ⊗ (ℂ^2)') ← ((ℂ^3)' ⊗ ℂ^3)julia> flip(W, 3)(ℂ^3 ⊗ (ℂ^4)') ← ((ℂ^2)' ⊗ ℂ^3)julia> insertleftunit(W, 3)(ℂ^3 ⊗ (ℂ^4)') ← (ℂ^1 ⊗ ℂ^2 ⊗ ℂ^3)julia> insertrightunit(W, 2)(ℂ^3 ⊗ (ℂ^4)' ⊗ ℂ^1) ← (ℂ^2 ⊗ ℂ^3)julia> removeunit(insertrightunit(W, 2), 3)(ℂ^3 ⊗ (ℂ^4)') ← (ℂ^2 ⊗ ℂ^3)julia> TensorKit.compose(W, adjoint(W))(ℂ^3 ⊗ (ℂ^4)') ← (ℂ^3 ⊗ (ℂ^4)')
Note that indexing W follows an order that first targets 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 the actual morphisms living in such a HomSpace. Also note that dim(W) is here given by 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, or thus, the number of independent components that a corresponding TensorMap object will have.
A complete list of methods defined on HomSpace instances together with the corresponding documentation is provided in the library section on Vector spaces.