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 end

which is actually a too restricted name. All instances of subtypes of VectorSpace will represent objects in $𝕜$-linear monoidal categories, but this can go beyond normal vector spaces (i.e. objects in the category $\mathbf{Vect}$) and even beyond objects of $\mathbf{SVect}$. However, in order not to make the remaining discussion to abstract or complicated, we will simply refer to subtypes of VectorSpace instead of specific categories, and to spaces (i.e. VectorSpace instances) instead of objects from these categories. In particular, we define two abstract subtypes

abstract type ElementarySpace <: VectorSpace end
const IndexSpace = ElementarySpace

abstract type CompositeSpace{S<:ElementarySpace} <: VectorSpace end

Here, 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 (linear categories) 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 ∈ ℝ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

  • dim(::ElementarySpace) -> ::Int returns the dimension of the space as an Int

  • dual(::S) where {S<:ElementarySpace} -> ::S returns the dual space dual(V), using an instance of the same concrete type (i.e. not via type parameters); this should satisfy dual(dual(V))==V

  • conj(::S) where {S<:ElementarySpace} -> ::S returns the complex conjugate space conj(V), using an instance of the same concrete type (i.e. not via type parameters); this should satisfy conj(conj(V))==V and we automatically have conj(V::ElementarySpace{ℝ}) = V.

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
end

We furthermore define the trait types

abstract type InnerProductStyle end
struct NoInnerProduct <: InnerProductStyle end
abstract type HasInnerProduct <: InnerProductStyle end
struct EuclideanInnerProduct <: HasInnerProduct end

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 metrics is currently implemented.

Spaces with the EuclideanInnerProduct style have the natural isomorphisms dual(V) == V (for 𝔽 == ℝ) or dual(V) == conj(V) (for 𝔽 == ℂ). In the language of the previous section on categories, this trait represents dagger or unitary categories, and these vector spaces support an adjoint operation.

In particular, the two concrete types

struct CartesianSpace <: ElementarySpace
    d::Int
end
struct ComplexSpace <: ElementarySpace
  d::Int
  dual::Bool
end

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)10
julia> (ℝ^10)' == ℝ^10true
julia> isdual((ℂ^5))false
julia> isdual((ℂ^5)')true
julia> isdual((ℝ^5)')false
julia> dual(ℂ^5) == (ℂ^5)' == conj(ℂ^5) == ComplexSpace(5; dual = true)true
julia> field(ℂ^5)
julia> field(ℝ^3)
julia> typeof(ℝ^3)CartesianSpace
julia> spacetype(ℝ^3)CartesianSpace
julia> InnerProductStyle(ℝ^3)EuclideanInnerProduct()
julia> InnerProductStyle(ℂ^5)EuclideanInnerProduct()
Note

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 purist 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 instance of ElementarySpace is the GradedSpace, which is used to represent a graded complex vector space with Euclidean inner product, where the grading is provided by the irreducible representations of a group, or more generally, the simple objects of a fusion category. We refer to the subsection on graded spaces on the next page for further information about GradedSpace.

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}
end

Given 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ℂ^2
julia> V2 = ℂ^3ℂ^3
julia> V1 ⊗ V2 ⊗ V1' == V1 * V2 * V1' == ProductSpace(V1,V2,V1') == ProductSpace(V1,V2) ⊗ V1'true
julia> V1^3(ℂ^2 ⊗ ℂ^2 ⊗ ℂ^2)
julia> dim(V1 ⊗ V2)6
julia> dims(V1 ⊗ V2)(2, 3)
julia> dual(V1 ⊗ V2)((ℂ^3)' ⊗ (ℂ^2)')
julia> spacetype(V1 ⊗ V2)ComplexSpace
julia> spacetype(ProductSpace{ComplexSpace,3})ComplexSpace

Here, the new function dims maps dim to the individual spaces in a ProductSpace and returns the result as a tuple. Note that the rationale for the last result was explained in the subsection on duality in the introduction to category theory.

Following Julia's Base library, the function one applied to a ProductSpace{S,N} returns the multiplicative identity, which is ProductSpace{S,0}(()). The same result is obtained when acting on an instance V of S::ElementarySpace directly, however note that V ⊗ one(V) will yield a ProductSpace{S,1}(V) and not V itself. The same result can be obtained with ⊗(V). Similar to Julia Base, one also works in the type domain.

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 domains, like general relativity, might also benefit from tensors living in a subspace with certain symmetries under specific index permutations.

Space of morphisms

Given that we define tensor maps as morphisms in a $𝕜$-linear monoidal category, i.e. linear maps, we also define a type to denote the corresponding space. Indeed, in a $𝕜$-linear category $C$, the set of morphisms $\mathrm{Hom}(W,V)$ for $V,W ∈ C$ is always an actual vector space, irrespective of whether or not $C$ is a subcategory of $\mathbf{(S)Vect}$.

We introduce the type

struct HomSpace{S<:ElementarySpace, P1<:CompositeSpace{S}, P2<:CompositeSpace{S}}
    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 Index operations 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.