States

FiniteMPS

A FiniteMPS is - at its core - a chain of mps tensors.

Usage

A FiniteMPS can be created by passing in a vector of tensormaps:

L = 10
data = [rand(ComplexF64, ℂ^1 ⊗ ℂ^2  ← ℂ^1) for _ in 1:L];
state = FiniteMPS(data)
10-site FiniteMPS (ComplexF64, TensorKit.ComplexSpace):
┌ C[10]: TensorMap(ℂ^1 ← ℂ^1)
├── AL[10]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^1)
├── AL[9]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^1)
├── AL[8]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^1)
├── AL[7]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^1)
├── AL[6]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^1)
├── AL[5]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^1)
├── AL[4]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^1)
├── AL[3]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^1)
├── AL[2]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^1)
└── AL[1]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^1)

Or alternatively by specifying its structure

max_bond_dimension = ℂ^4
physical_space = ℂ^2
state = FiniteMPS(rand, ComplexF64, L, physical_space, max_bond_dimension)
10-site FiniteMPS (ComplexF64, TensorKit.ComplexSpace):
┌ C[10]: TensorMap(ℂ^1 ← ℂ^1)
├── AL[10]: TensorMap((ℂ^2 ⊗ ℂ^2) ← ℂ^1)
├── AL[9]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^2)
├── AL[8]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
├── AL[7]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
├── AL[6]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
├── AL[5]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
├── AL[4]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
├── AL[3]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
├── AL[2]: TensorMap((ℂ^2 ⊗ ℂ^2) ← ℂ^4)
└── AL[1]: TensorMap((ℂ^1 ⊗ ℂ^2) ← ℂ^2)

You can take dot products, renormalize!, expectation values,....

Gauging and canonical forms

An MPS representation is not unique: for every virtual bond we can insert $C \cdot C^{-1}$ without altering the state. Then, by redefining the tensors on both sides of the bond to include one factor each, we can change the representation.

There are two particularly convenient choices for the gauge at a site, the so-called left and right canonical form. For the left canonical form, all tensors to the left of a site are gauged such that they become left-isometries. By convention, we call these tensors AL.

al = state.AL[3]
al' * al ≈ id(right_virtualspace(al))
true

Similarly, the right canonical form turns the tensors into right-isometries. By convention, these are called AR.

ar = state.AR[3]
repartition(ar, 1, 2) * repartition(ar, 1, 2)' ≈ id(left_virtualspace(ar))
true

It is also possible to mix and match these two forms, where all tensors to the left of a given site are in the left gauge, while all tensors to the right are in the right gauge. In this case, the final gauge transformation tensor can no longer be absorbed, since that would spoil the gauge either to the left or the right. This center-gauged tensor is called C, which is also the gauge transformation to relate left- and right-gauged tensors. Finally, for convenience it is also possible to leave a single MPS tensor in the center gauge, which we call AC = AL * C

c = state.C[3] # to the right of site 3
c′ = state.C[2] # to the left of site 3
al * c ≈ state.AC[3] ≈ repartition(c′ * repartition(ar, 1, 2), 2, 1)
true

These forms are often used throughout MPS algorithms, and the FiniteMPS object acts as an automatic manager for this. It will automatically compute and cache the different forms, and detect when to recompute whenever needed. For example, in order to compute the overlap of an MPS with itself, we can choose any site and bring that into the center gauge. Since then both the left and right side simplify to the identity, this simply becomes the overlap of the gauge tensors:

d = dot(state, state)
all(c -> dot(c, c) ≈ d, state.C)
true

Implementation details

Behind the scenes, a FiniteMPS has 4 fields

ALs::Vector{Union{Missing,A}}
ARs::Vector{Union{Missing,A}}
ACs::Vector{Union{Missing,A}}
Cs::Vector{Union{Missing,B}}

and calling AL, AR, C or AC returns lazy views over these vectors that instantiate the tensors whenever they are requested. Similarly, changing a tensor will poison the ARs to the left of that tensor, and the ALs to the right. The idea behind this construction is that one never has to worry about how the state is gauged, as this gets handled automagically.

Warning

While a FiniteMPS can automatically detect when to recompute the different gauges, this requires that one of the tensors is set using an indexing operation. In particular, in-place changes to the different tensors will not trigger the recomputation.

InfiniteMPS

An InfiniteMPS can be thought of as being very similar to a finite mps, where the set of tensors is repeated periodically.

It can also be created by passing in a vector of TensorMaps:

data = [rand(ComplexF64, ℂ^4 ⊗ ℂ^2  ← ℂ^4) for _ in 1:2]
state = InfiniteMPS(data)
2-site InfiniteMPS:
│   ⋮
│ C[2]: TensorMap(ℂ^4 ← ℂ^4)
├── AL[2]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
├── AL[1]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
│   ⋮

or by initializing it from given spaces

phys_spaces = fill(ℂ^2, 2)
virt_spaces = [ℂ^4, ℂ^5] # by convention to the right of a site
state = InfiniteMPS(phys_spaces, virt_spaces)
2-site InfiniteMPS:
│   ⋮
│ C[2]: TensorMap(ℂ^5 ← ℂ^5)
├── AL[2]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^5)
├── AL[1]: TensorMap((ℂ^5 ⊗ ℂ^2) ← ℂ^4)
│   ⋮

Note that the code above creates an InfiniteMPS with a two-site unit cell, where the given virtual spaces are located to the right of their respective sites.

Gauging and canonical forms

Much like for FiniteMPS, we can again query the gauged tensors AL, AR, C and AC. Here however, the implementation is much easier, since they all have to be recomputed whenever a single tensor changes. This is a result of periodically repeating the tensors, every AL is to the right of the changed site, and every AR is to the left. As a result, the fields are simply

AL::PeriodicArray{A,1}
AR::PeriodicArray{A,1}
C::PeriodicArray{B,1}
AC::PeriodicArray{A,1}

WindowMPS

A WindowMPS or segment MPS can be seen as a mix between an InfiniteMPS and a FiniteMPS. It represents a window of mutable tensors (a finite MPS), embedded in an infinite environment (two infinite MPSs). It can therefore be created accordingly, ensuring that the edges match:

infinite_state = InfiniteMPS(ℂ^2, ℂ^4)
finite_state = FiniteMPS(5, ℂ^2, ℂ^4; left=ℂ^4, right=ℂ^4)
window = WindowMPS(infinite_state, finite_state, infinite_state)
WindowMPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 2, 1, Vector{ComplexF64}}, TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 1, Vector{ComplexF64}}}(│   ⋮
│ C[1]: TensorMap(ℂ^4 ← ℂ^4)
├── AL[1]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
│   ⋮
, ┌ C[5]: TensorMap(ℂ^4 ← ℂ^4)
├── AL[5]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
├── AL[4]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
├── AL[3]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
├── AL[2]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
└── AL[1]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
, │   ⋮
│ C[1]: TensorMap(ℂ^4 ← ℂ^4)
├── AL[1]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
│   ⋮
)

Algorithms will then act on this window of tensors, while leaving the left and right infinite states invariant.

MultilineMPS

A two-dimensional classical partition function can often be represented by an infinite tensor network. There are many ways to evaluate such a network, but here we focus on the so-called boundary MPS methods. These first reduce the problem from contracting a two-dimensional network to the contraction of a one-dimensional MPS, by finding the fixed point of the row-to-row (or column-to-column) transfer matrix. In these cases however, there might be a non-trivial periodicity in both the horizontal as well as vertical direction. Therefore, in MPSKit they are represented by MultilineMPS, which are simply a repeating set of InfiniteMPS.

state = MultilineMPS(fill(infinite_state, 2))
MultilineMPS{InfiniteMPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 2, 1, Vector{ComplexF64}}, TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 1, Vector{ComplexF64}}}}(InfiniteMPS{TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 2, 1, Vector{ComplexF64}}, TensorKit.TensorMap{ComplexF64, TensorKit.ComplexSpace, 1, 1, Vector{ComplexF64}}}[│   ⋮
│ C[1]: TensorMap(ℂ^4 ← ℂ^4)
├── AL[1]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
│   ⋮
, │   ⋮
│ C[1]: TensorMap(ℂ^4 ← ℂ^4)
├── AL[1]: TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4)
│   ⋮
])

They offer some convenience functionality for using cartesian indexing (row - column):

You can access properties by calling

row = 2
col = 2
al = state.AL[row, col];
TensorMap((ℂ^4 ⊗ ℂ^2) ← ℂ^4):
[:, :, 1] =
   0.4993385518005107 + 0.5039467560992162im     …     0.5666694638564805 + 0.40274961953947197im
  0.07277340580931106 + 0.06057961265455346im        0.052306378857341185 + 0.029044721589467956im
 0.019534694290421584 - 0.0032494844257118724im       0.01949871532879461 + 0.0003962610650575212im
 0.004048531702222025 + 0.004971863894832554im      0.0031250901909705576 + 0.0028866298399368804im

[:, :, 2] =
   0.08465576974058202 - 0.6786588513554553im    …   0.03913391932516956 + 0.7020918896671328im
  -0.04331509665122679 - 0.125391410086677im         0.03283147832929415 + 0.13097550674686192im
 -0.023366172351686924 + 0.011619258511375378im     0.029617014176422686 - 0.0024191701765835802im
 -0.006748281821895453 - 0.01566583585638581im      0.008746185888906265 + 0.005284667451390805im

[:, :, 3] =
 -0.010536200465513228 + 0.15769749052497747im   …  -0.009148401134966067 - 0.08016289786544678im
  -0.12125511882855068 - 0.8474532917584939im           0.260598747645854 + 0.3382641317984576im
   0.07619631379215153 + 0.18153892642597952im      -0.048517941582935084 - 0.0890863415239789im
 -0.007653803597992986 - 0.028428937382105748im       0.05574978180932981 + 0.016402454908212006im

[:, :, 4] =
 -0.013461118176483104 - 0.025497033573642233im  …  -0.10331927363371057 + 0.029748542658526972im
    0.4010648954467331 + 0.10994082099404923im        0.8092602336931201 - 0.1577730037943597im
  -0.11359168014787097 - 0.1184090415441578im        -0.2978043029902884 - 0.07925961910166016im
   0.09594146911252621 - 0.0277931892064887im       -0.04271873565979338 + 0.034336455582860415im

These objects are also used extensively in the context of PEPSKit.jl.