Xped
Loading...
Searching...
No Matches

Xped is c++ library for the definition and manipulation of symmetry preserving tensors for tensor network related algorithms.

Theory

Tensor

Multiple definitions exist for the term tensor (see e.g. wikipedia). For the purpose of this library, a tensor \(T\) is a multidimensional linear map between the tensor product of \(d\) vector spaces \(\mathbb{C}^{n_1}\otimes\cdots \otimes\mathbb{C}^{n_d}\) which form the domain \(\mathcal{D}\) and a tensor product of \(c\) vector spaces \(\mathcal{\mathbb{C}^{m_1}\otimes\cdots \otimes\mathbb{C}^{m_c}}\) which form the codomain \(\mathcal{C}\):

\begin{align*} T:\mathbb{C}^{n_1}\otimes\cdots \otimes\mathbb{C}^{n_d} &\to \mathbb{C}^{m_1}\otimes\cdots \otimes\mathbb{C}^{m_c} \\ v_1\otimes\cdots\otimes v_d &\mapsto w_1\otimes\cdots\otimes w_c \end{align*}

The rank of the tensor \(T\) is \(r=d+c\). The dimension of the domain is \(dim_{\mathcal{D}}=n_1\cdots n_d\) and of the codomain \(dim_{\mathcal{C}}=m_1\cdots m_c\). Since the map is multi-linear, its action is entirely defined by the action on the basis vectors. This defines the components of the tensor with respect to given bases:

\[ j_1\otimes\cdots\otimes j_c = \sum_{i_1,\dots,i_d}T_{i_1,\dots,i_d}^{j_1,\dots,j_c} i_1\otimes\cdots\otimes i_d \]

By a canonical isomorphism \(\pi_\mathcal{D}\), one can define the composite vector space \(\mathbb{C}^{dim_\mathcal{D}}\) for the domain of the map:

\[ \mathbb{C}^{dim_\mathcal{D}}\cong\mathbb{C}^{n_1}\otimes\cdots \otimes\mathbb{C}^{n_d} \]

Analogously, one can define a composite space \(\mathbb{C}^{dim_\mathcal{C}}\) for the codomain. The tensor T is then an ordinary linear map:

\[ T: \mathbb{C}^{dim_\mathcal{D}} \to \mathbb{C}^{dim_\mathcal{C}} \]

This map can be represented by a matrix which will be useful when storing the tensor in memory.

Symmetric tensor

A symmetric tensor is a tensor which respects a given symmetry. To formalize this, lets assume a symmetry described by a group \(G\). \(G\) is represented on all individual vector spaces of the domain by \(\mathcal{R}^i_G\) and codomain by \(\mathcal{Q}^i_G\) of a tensor. Then a tensor \(T\) is symmetric if:

\[ T(v_1\otimes\cdots\otimes v_d) = \mathcal{Q}^1_G\otimes\cdots\otimes \mathcal{Q}^c_G\left( T(\mathcal{R}^1_G(v_1)\otimes\cdots\otimes \mathcal{R}^d_G(v_d))\right) \]

That means the action of the tensor stays the same when the symmetry acts on the individual spaces. If the tensor is transformed with the isomorphisms \(\pi_\mathcal{D}\) and \(\pi_\mathcal{C}\) it becomes an ordinary linear map. Then Schur's lemma (wikipedia) implies that this map is a constant if the composite representation of the symmetry on domain and codomain is irreducible. Generally, it will be reducible but may be decomposed into a direct sum of irreducible representations (irreps). The underlying matrix becomes then block diagonal.

Construction of isomorphism

The block diagonal form of the underlying matrix is only manifest if the isomorphism \(\pi_\mathcal{D}\) maps to a basis which is the decomposed into the direct sum of irreps. Hence, the crucial task is to find an appropriate isomorphism. A straight-forward method for obtaining \(\pi_\mathcal{D}\) is the use of fusion trees which perform the mapping by a sequential pairwise fusion of two vector spaces into their product state. For a given binary fusion of two spaces, the Clebsch-Gordan expansion is used to find the decomposition of the product state into irreps. Each intermediate space is then combined with next unpaired space until all spaces are combined to the final domain/codomain of the tensor. It is important to store all the intermediate quantum numbers since for general symmetries, several different paths exist which lead to the same output. An example fusion tree of four quantum numbers \(q_1,\dots,q_4\) coupled to \(q_f\) looks as follows:

dot_inline_dotgraph_1.png
Example of a canonical fusion tree

The triangular shapes correspond to the binary Clebsch-Gordan decomposition. They carry another (Greek) label which is important for symmetries with outermultiplicity in their Clebsch-Gordan expansion (e.g. SU(3)). For Abelian symmetries, the complete fusion tree is entirely determined by the incoming quantum numbers and the intermediate values do not have to be stored.

The fusion trees construct the isomorphisms \(\pi_\mathcal{D}\) and \(\pi_\mathcal{C}\).

Note
The isomorphisms \(\pi_\mathcal{D}\) and \(\pi_\mathcal{C}\) are not constructed explicitly but only implicitly by storing all fusion trees.

Tensor operations

For tensor network related algorithms, several tensor operations are ubiquitous. One needs to contract tensors together to obtain final results or needs to decompose a tensor with a singular value decomposition to truncate certain bonds in the network. A fundamental operation is the permutation of the legs of an individual tensor. It is needed for other high-level operations.

Permutations

A permutation is a reshuffling of the individual legs of the tensor. This corresponds to a reordering of the individual vector spaces which form \(\mathcal{D}\) and \(\mathcal{C}\). For nonsymmetric tensors, this can be achieved by choosing appropriate new isomorphisms \(\pi_\mathcal{D}^\prime\) and \(\pi_\mathcal{C}^\prime\) such that the permuted input is mapped to the correct state. This leads to a new view to the original data which can be left untouched. However, the data layout becomes strided and noncontiguous. For some operations, it is therefore more efficient to copy the underlying data so that the layout is again contiguous.

In the case of symmetric tensors, the situation is more complicated. A permutation requires a reordering of the incoming legs of the fusion trees. If the incoming legs are shuffled, the resultant fusion tree is not canonical anymore and therefore not useful for further operations which expects a canonical fusion tree. In order to get the reordered fusion tree in canonical form, one needs the recoupling symbols \(F\) of the symmetry group \(G\).

dot_inline_dotgraph_2.png
Recoupling symbols

The sum goes over all possible intermediate quantum numbers \(q_{23}\) and in the case of outermultiplicity also over \(\gamma\) and \(\delta\). The recoupling symbols define a unitary transformation between the composite spaces of different fusion orders. They are entirely determined by the Clebsch Gordan coefficients.

To obtain the reordered fusion tree in canonical form, one needs a second basis operation which exchanges two incoming quantum numbers on the same node. This operations involves the swap symbol for the group.

With these two operations, one can implement arbitrary permutations on a single fusion tree. A tensor consists of two fusion trees, one for the domain and one for the codomain. For permutations which shuffle across the domain and codomain, a third basic operation is needed. This involves the bending of a line pointing inwards to pointing outwards and is quantified by the turn symbol of the group.

The permutation of the legs of a symmetric tensor is then performed by first permuting the fusion tree pair (consisting of domain tree and codomain tree) and second by assigning the old data to the new tensor with careful attention to the recoupling-, swap- and turn symbols.

Contractions

A tensor contraction boils down to the composition of the associated maps. I.e., if \(T_1: \mathcal{D}\to\mathcal{I}\) and \(T_2: \mathcal{I}\to\mathcal{C}\) than the contraction \(T\) of \(T_1\) and \(T_2\) over all the elementary spaces in \(\mathcal{I}\) is the composition:

\[ T = T_2 \circ T_1 \]

The composition of tensors corresponds to the multiplication of the representing matrices which arise after the application of the isomorphisms \(\pi_\mathcal{D}\) and \(\pi_\mathcal{C}\).

In the generic case, the indices of a tensor which should be contracted with indices of another tensor are located in its codomain. Therefore one has to permute the indices of both tensors so that an ordinary composition performs the contraction. After the composition, another permute might bring the indices in the desired order for the final result.

Decompositions

A tensor decomposition can be seen as the inverse operation to a contraction. Because of that it can be handled similarly. An often encountered decomposition is the singular value decomposition (svd). It can be applied to any linear map. Hence, the tensor is again interpreted as an ordinary linear map with the help of the isomorphisms \(\pi_\mathcal{D}\) and \(\pi_\mathcal{C}\). Then, the svd reads:

\[ T = U \Sigma V^\dagger \]

\(U\) has the original domain of the tensor and \(V\) has the original codomain, i.e. the cut is taken through the domain and codomain.

For a general partition of indices, one can apply a permute operation to get an appropriate domain and codomain.

Implementation

Quick tour

To get a feeling of how the library works, lets look at some simple examples for the core part of the library. All entities of the library are defined in namespace Xped.

#include <type_traits>
int main()
{
// Make an alias for the symmetry. In this case a U1 symmetry for a spin.
// Initialize random basis objects which will be used to initialize tensors
B1.setRandom(10);
B2.setRandom(10);
// Initialize a random rank-3 tensor with two incoming and one outgoing leg with double precision numbers constrained by the symmetry declared
// above
Xped::Tensor<double, /*Rank=*/2, /*CoRank=*/1, Symmetry> T({{B1, B2}}, {{B1.combine(B2).forgetHistory()}});
T.setRandom();
// Unary and binary coefficientwise operations:
auto S = 3. * T; // this is an expression which is lazely evaluated
Xped::Tensor<double, 2, 1, Symmetry> Q = S.eval() + T; //.eval() can be used to force evaluation and assignment to tensor also forces evaluation
// makes a truncated singular value decomposition between domain and codomain keeping at most 50 singular values and discarding all singular
// values less than 1.e-10:
auto [U, Sigma, Vdag] = S.tSVD(50, 1.e-10);
static_assert(std::is_same_v<decltype(U), Xped::Tensor<double, 2, 1, Symmetry>>);
static_assert(std::is_same_v<decltype(S), Xped::Tensor<double, 1, 1, Symmetry>>);
static_assert(std::is_same_v<decltype(Vdag), Xped::Tensor<double, 1, 1, Symmetry>>);
// Permutation of the indices. shift specifies how much the domain is decreasing (and the codomain is increasing). The following indices
// describes the permutation.
auto X = T.permute</*shift1=*/+1, 2, 1, 0>(); // this gets evaluated immediately
// This is an SVD with a different partitioning of the legs
auto [U_, Sigma_, Vdag_] = X.tSVD(50, 1.e-10);
static_assert(std::is_same_v<decltype(U_), Xped::Tensor<double, 1, 1, Symmetry>>);
static_assert(std::is_same_v<decltype(S_), Xped::Tensor<double, 1, 1, Symmetry>>);
static_assert(std::is_same_v<decltype(Vdag_), Xped::Tensor<double, 1, 2, Symmetry>>);
// Multiplication of tensors is the contraction over the matching codomain/domain of the two tensors
auto prod = T * T.adjoint(); // this gets also evaluated immediately
// Arbitrary contractions are also possible:
auto res = T.contract<std::array{-1, -2, 1}, std::array{1, -4, -3}, /*ResRank=*/2>(X);
static_assert(std::is_same_v<decltype(res), Xped::Tensor<double, 2, 2, Symmetry>>);
}
int main(int argc, char *argv[])
Definition: bench.cpp:102
Definition: Qbasis.hpp:39
void setRandom(const std::size_t &fullSize, const std::size_t &max_sectorSize=5ul)
Definition: Qbasis.cpp:91
Qbasis< Symmetry, depth+1, AllocationPolicy > combine(const Qbasis< Symmetry, 1, AllocationPolicy > &other, bool CONJ=false) const
Definition: Qbasis.cpp:439
Definition: Tensor.hpp:40
@ T
Definition: functions.hpp:35
@ S
Definition: functions.hpp:33
Definition: U1.hpp:39

Symmetries

Each supported symmetry has its own class which supplies all methods related to the symmetry. Currently implemented are the following groups:

  • \(Z_n\) \(\rightarrow\) template<std::size_t N, Xped::Sym::Kind> struct Xped::Sym::Zn;
  • \(\text{U}(1)\) \(\rightarrow\) template<Xped::Sym::Kind> struct Xped::Sym::U1;
  • \(\text{SU}(2)\) \(\rightarrow\) template<Xped::Sym::Kind> struct Xped::Sym::SU2;
  • trivial \(\rightarrow\) struct Xped::Sym::U0;

Partial support exists also for general \(\text{SU}(N)\) symmetry.

All symmetry classes have a CRTP base class template<typename Derived> struct Xped::Sym::SymBase; to avoid duplicated code. The individual symmetry classes are stateless and methods are static. Some basic objects which are implemented in the symmetry classes are:

  • using qType = ... the type for the quantum numbers of the symmetry
  • static std::vector<qType> basis_combine(qType ql, qType qr) returns the decomposition of the direct product of two irreps into irreps.
  • static constexpr qType qvacuum(); returns the quantum number of the trivial representation.

To see the full list of required functions check Symmetry/SU2.hpp

All symmetry classes take Xped::Sym::Kind as a template parameter. It describes to which particles the symmetry relates to. Possible values are:

The difference between the first two is only when formatting related quantum numbers because spin quantum numbers can be half-integers but particle quantum numbers are only integers. The latter however, makes a crucial difference. If a symmetry is set up with Xped::Kind::Fermion, the fermionic commutation rules and respective signs are taken care off automatically for tensor operations. This is controlled by the methods

static Scalar coeff_swap(qType ql, qType qr);
static Scalar coeff_twist(qType q);

The first methods adds a minus sign if two odd fermion-parity states are swapped. The second includes a sign if a odd parity state is twisted.

Fusion trees

The struct FusionTree is a simple static storage of all relevant data for a single fusion path of Rank quantum numbers of a given Symmetry:

template<std::size_t Rank, typename Symmetry>
struct FusionTree;

It has the following members:

std::array<qType, Rank> q_uncoupled{};
qType q_coupled{};
std::size_t dim{};
std::array<size_t, Rank> dims{};
std::array<qType, util::inter_dim(Rank)> q_intermediates{};
std::array<size_t, util::mult_dim(Rank)> multiplicities = std::array<size_t, util::mult_dim(Rank)>(); // only for non-Abelian symmetries with outermultiplicity.
std::array<bool, Rank> IS_DUAL{};
Warning
This class does not check whether all the quantum numbers do fit together but it expects that it is constructed correctly.

To display a FusionTree, there is the handy function Xped::FusionTree::draw(). It generates an ascii drawing of the tree. Here is an example for a rank=4 tree for an SU(2) symmetry:

2 0 1/2 1/2
\ / / /
\ / / /
μ / /
\ / /
\ 2 / /
\ / /
μ /
\ /
\ 3/2 /
\ /
μ
|
|
1

Fusion tree manipulations

Fusion tree manipulations build the basis for tensor operations. The fundamental operation is the permutation of two adjacent incoming quantum numbers. Unless it involves the first two incoming quantum numbers which enters to the same binary fusion, this involves recoupling operations. Here, especially the \(6j\)-symbol of the symmetry group enters. At the end, a fusion tree can be written as a weighted sum over fusion trees with two adjacent quantum numbers exchanged. The respective method is Xped::FusionTree::swap:

/*Swaps quantum numbers pos and pos+1*/
std::unordered_map<FusionTree<Rank, Symmetry>, typename Symmetry::Scalar> swap(const std::size_t& pos) const;

With the exchange of adjacent quantum numbers, one can implement arbitrary permutations of the incoming quantum numbers by decomposing the permutation into a chain of adjacent swaps. This is performed with Xped::util::Permutation::decompose(). The high-level function which performs the general permutation is Xped::FusionTree::permute().

Hilbert bases

To describe a Hilbert basis, the class Qbasis is used:

template<typename Symmetry, std::size_t depth, typename AllocationPolicy = HeapPolicy>

This class stores a basis which is properly sorted with respect to a given Symmetry, i.e. it contains several multiplets of the symmetry. Furthermore, the basis can be a composite basis of several individual bases. This is controlled via the template parameter depth. To handle the composition, Qbasis uses FusionTree to remember the exact fusion process. Generally, bases are constructed with depth=1 and composite bases are obtained using the method Xped::Qbasis::combine().

The most important member of Qbasis is:

std::vector<std::tuple<qType, std::size_t, Xped::Basis>> data_;

The actual type is more complicated because of AllocationPolicy but this does not matter here. Each entry of data_ corresponds to a quantum number of type qType (first entry in the tuple). For each quantum number, the global number of the first plain state corresponds to the second element in the tuple. The third element of the tuple belongs to the plain basis states of this quantum number. The plain basis state are described by the simple class Xped::Basis.

A few lines of code illustrate the use of the class:

#include <type_traits>
int main()
{
// Add two states with spin 1/2 and one state with spin 3 into the basis.
B.push_back({2}, 2); // One has to use D=2s+1
B.push_back({7}, 1);
// Print the basis (it has three multiplets with a total dimension of 11 states)
Log::debug("{}", B.print()); // Basis(SUâ‚‚, dim=3[11]): q=1/2[2], q=3[1]
// Qbasis objects can be combined into a product basis
auto Bsq = B.combine(B);
Log::debug("Squared: {}",
Bsq.print()); // Squared: Basis(SUâ‚‚, dim=23[121]): q=0[5], q=1[5], q=2[1], q=5/2[4], q=3[1], q=7/2[4], q=4[1], q=5[1], q=6[1]
static_assert(std::is_same_v<decltype(Bsq), Qbasis<Symmetry, 2>>); // Bsq has depth 2 because it is the result of a combination of two bases
Log::debug("{}", Bsq.printTrees()); // Bsq contains also FusionTrees (13 in total) from the combination of both bases
/* Here are the trees for the fusion quantum S=7/2
2 Fusion trees for Q=7/2
1/2 3
\ /
\ /
μ
|
|
7/2
3 1/2
\ /
\ /
μ
|
|
7/2
*/
// This would turn Bsq into an empty basis
Bsq.clear();
}
constexpr void debug(Verbosity policy, Args &&... args)
Definition: Logging.hpp:120
@ B
Definition: SubLattice.hpp:11
Definition: SU2.hpp:43

One of the key methods of Qbasis are Xped::Qbasis::leftOffset() and Xped::Qbasis::rightOffset(). If several bases are combined into a composite basis, each basis state in the composite basis belongs to a specific fusion. A rigorous order of all the fused states with same quantum number is fundamental to construct the isomorphism \(\pi_\mathcal{D}\). The exact signature of leftOffset() is:

std::size_t leftOffset(const FusionTree<Symmetry, depth>& tree) const;
size_t leftOffset(const FusionTree< depth, Symmetry > &tree, const std::array< size_t, depth > &plain=std::array< std::size_t, depth >()) const
Definition: Qbasis.cpp:206
const auto & tree(const qType &q) const
Definition: Qbasis.hpp:240

It takes a FusionTree as a parameter and returns the number of basis states which come before this tree. These methods are fundamental for getting tensor views from the large block diagonal matrix which is stored in memory. See the implementation of Xped::Tensor::subBlock():

template <typename Scalar, std::size_t Rank, std::size_t CoRank, typename Symmetry, typename AllocationPolicy>
typename PlainInterface::TType<Scalar, Rank + CoRank>
Tensor<Scalar, Rank, CoRank, Symmetry, false, AllocationPolicy>::subBlock(const FusionTree<Rank, Symmetry>& f1,
const FusionTree<CoRank, Symmetry>& f2,
std::size_t block_number) const
{
assert(block_number < sector().size());
assert(f1.q_coupled == f2.q_coupled);
assert(sector(block_number) == f1.q_coupled);
const auto left_offset_domain = coupledDomain().leftOffset(f1);
const auto left_offset_codomain = coupledCodomain().leftOffset(f2);
std::array<IndexType, Rank + CoRank> dims;
for(size_t i = 0; i < Rank; ++i) { dims[i] = uncoupledDomain()[i].inner_dim(f1.q_uncoupled[i]); }
for(size_t i = 0; i < CoRank; ++i) { dims[i + Rank] = uncoupledCodomain()[i].inner_dim(f2.q_uncoupled[i]); }
return PlainInterface::template tensor_from_matrix_block<Rank + CoRank>(
block(block_number), left_offset_domain, left_offset_codomain, f1.dim, f2.dim, dims);
}
std::vector< std::size_t > dims() const
Definition: Qbasis.hpp:122

Tensor class

The tensor class is the central object of the library:

template<typename Scalar, std::size_t Rank, std::size_t CoRank, typename Symmetry, bool AD=false, typename AllocationPolicy=HeapPolicy>
class Tensor;

It takes 6 template parameters but the last two are optional. The first four describe the underlying Scalar type, the rank and corank of the tensor ( \(d\) and \(c\)) and the symmetry under which the tensor is invariant. The fifth parameter decides whether the tensor can be used for automatic differentiation (AD). It is a bool parameter and both choices (true and false) corresponds to different explicit specializations of the general class template. For the AD part see below. The last parameter controls the allocation procedure.

The tensor class has only a single member variable:

Storage storage_;

The used storage is currently determined at configuration time by a cmake parameter. In the future, this can become an additional template parameter.

The most important methods are:

  • Xped::Tensor::permute();
  • Xped::Tensor::contract();
  • Xped::Tensor::tSVD();

Storage

For an efficient storage of the tensor data in the case of a symmetry constrain, the tensor is seen as an ordinary linear map between two vector spaces. This procedure was described in the Theory section above. In the case of a symmetry, the representing matrix of the linear map is block-diagonal. Therefore, the most compact storage is obtained when storing only the nonzero block-matrices of the large block-diagonal matrix. Each block is associated to a quantum number of the symmetry and contains all elements of the symmetry which can be fused to this quantum number.

Two specific storage implementations are available:

  1. VecOfMat see Core/storage/StorageType_vecofmat.hpp
  2. Contiguous see Core/storage/StorageType_contiguous.hpp

The first option stores a std::vector of matrices to describe the block-diagonal matrix. For this option the entire tensor is not contiguous in memory.

The second option stores a large contiguous buffer (std::vector<Scalar>). When a specific block of the block-diagonal matrix is requested, a view into this large buffer is returned which corresponds to the requested block.

The Storage class must have two specific constructors.

StorageType(const std::array<Xped::Qbasis<Symmetry, 1, AllocationPolicy>, Rank> basis_domain,
const std::array<Xped::Qbasis<Symmetry, 1, AllocationPolicy>, CoRank> basis_codomain,
const mpi::XpedWorld& world)
StorageType(const std::array<Qbasis<Symmetry, 1, AllocationPolicy>, Rank> basis_domain,
const std::array<Qbasis<Symmetry, 1, AllocationPolicy>, CoRank> basis_codomain,
const Scalar* data,
std::size_t size,
const mpi::XpedWorld& world)

Both take the individual bases of the domain and codomain as an std::array and the world to which the storage is associated in the case of distributed parallelism. The first constructor does not allocate actual memory but only sets the meta data of the storage. The second constructor allocates the needed storage and copies the contiguous data data into the tensor. If size does not match the total elements in the tensor, an assertion is raised.

Additionally, the storage implementations need to include the following public methods.

  • const MatrixType& block(std::size_t i) const; returns a const reference to the ith block of the block-diagonal matrix
  • MatrixType& block(std::size_t i); returns a nonconst reference to the ith block of the block-diagonal matrix
  • const MatrixType& block(qType q) const; returns a const reference to the block belonging to the fused quantum number q.
  • MatrixType& block(qType q); returns a nonconst reference to the block belonging to the fused quantum number q.
  • std::size_t plainSize() const; returns the total number of scalars in the tensor summed over all blocks.
  • const mpi::XpedWorld& world() const returns the mpi world, the data is living in. This is only relevant for distributed tensors.

For the complete interface check out Core/storage/StorageType_contiguous.hpp.

Automatic differentiation

The Tensor class supports build in automatic differentiation (AD), i.e. for algorithms build on top of the Tensor class, the derivative can be computed automatically. To declare a tensor with AD support, one needs to set the respective template argument to true. The source code for the AD support for Tensor can be found in include/Xped/AD/ADTensor.hpp (and not in Tensor.hpp itself). It is an explicit specialization and of the Tensor class template. In order to obtain the AD functionality, core features of the third-party library stan/math are used.

Let's look at an example usage of the AD functionality:

#include "Xped/Core/ADTensor.hpp"
int main()
{
Xped::Qbasis<Symmetry, /*depth=*/1> B1;
B1.setRandom(10);
Xped::Qbasis<Symmetry, /*depth=*/1> B2;
B2.setRandom(10);
// This starts the autodiff procedure
stan::math::nested_rev_autodiff nested;
// Initialize a random rank-3 tensor for use with AD
Xped::Tensor<double, /*Rank=*/2, /*CoRank=*/1, Symmetry, /*AD=*/true> T({{B1, B2}}, {{B1.combine(B2).forgetHistory()}});
T.setRandom();
std::cout << "T=\n" << T << std::endl;
// Do some computation with T that produces a scalar output.
// auto is important here, since the return type is stan::math::var_value<double> and not double
auto res = ((7. * T) * (3. * T.adjoint())).norm();
std::cout << "res=" << res.val() << std::endl;
// This starts the backpropagation to compute the gradient of the input (in this case T)
stan::math::grad(res.vi_);
std::cout << "grad(T)=\n" << T.adj() << std::endl;
}

Backends

The Xped library is designed to be a high-level library for symmetric tensors. All operations for plain nonsymmetric tensors are reimplemented. Instead, one can choose between different backends that deliver the functionality for plain tensors (or matrices). It is also possible to add a backend by implementing the required interface.

Currently, three backends are available. The Eigen backend is tested most thoroughly.

Eigen

Eigen is a famous c++ library for linear algebra. It is designed with an elegant API and highly optimized matrix operations. Additionally, it has a tensor module which is the basis for the TensorFlow framework by google. This part supports arbitrary ranked tensors and a wealth of operations including contractions and reductions.

The Eigen library also supports the dispatch of fundamental matrix operations to external BLAS and LAPACK implementations. This feature can be used within Xped at configuration time using the XPED_USE_BLAS and similar options. If the intel math kernel library should be integrated, one can switch on the parameter XPED_USE_MKL.

Cyclops tensor framework (ctf)

ctf is a tensor framework for distributed parallelism. This allows the computations to spread over several different compute cores and allows to use the hardware more efficient. It has an overhead for small tensors but allows to scale the compute power with very large tensors.

Note
The ctf backend is most useful for tensors without symmetry or with tensors with small internal symmetries.

array

Todo:
Add documentation for array backend.
Warning
The array backend needs some adaptions to be consistent with the code base.

Algorithms

On top of the core library, Xped also provides high-level tensor network algorithms.

Matrix product states (MPS)

MPSs represent a one-dimensional tensor network tailored for strongly interacting one-dimensional physical lattice systems. For detailed information, check the following references:

The MPS code is located in the directory Xped/MPS and currently provides a class MPS and basic operations for them. An implementation of matrix product operators (MPO)s and algorithms like the density matrix renormalization-group (DMRG) or the time-dependent variational principle (TDVP) are future development goals.

A small example demonstrates the current capability:

#include "Xped/MPS/Mps.hpp"
int main()
{
// Alias for the symmetry
// Number of sites in the MPS
std::size_t L = 100;
// Amount of quantum number blocks
std::size_t Qinit = 10;
// Amount of basis states per quantum number block
std::size_t Minit = 100;
// Target quantum number of the MPS
Symmetry::qType Qtarget = Symmetry::qvacuum();
// local (physical) basis of the MPS. In this case one spin 1/2
qloc.push_back({2}, 1);
// Contruct the MPS (with random entries)
Xped::Mps<double, Symmetry> Psi(L, qloc, Qtot, Minit, Qinit);
// Compute the norm
auto norm = Xped::dot(Psi, Psi);
// Sweep from left to put all A-tensors into right-canonical form
for(std::size_t l = 0; l < L; ++l) { Psi.rightSweepStep(l, Xped::DMRG::BROOM::SVD); }
}
Definition: Mps.hpp:27
void push_back(const qType &q, const size_t &inner_dim)
Definition: Qbasis.cpp:32
Symmetry::Scalar dot(XPED_CONST Mps< Scalar, Symmetry > &Bra, XPED_CONST Mps< Scalar, Symmetry > &Ket, const DMRG::DIRECTION DIR=DMRG::DIRECTION::RIGHT)
Definition: MpsAlgebra.cpp:15

Projected entangled pair states (PEPS)

PEPS is a generalization of MPS for two-dimensional systems. For detailed information, check the following references:

The PEPS code of Xped is located in Xped/PEPS and has the following capabilities:

  • CTMRG algorithm for arbitrary unit cells with custom pattern (AB/BA)
  • Imaginary time evolution using the simple update method with nearest and next-nearest Hamiltonian terms
  • Direct energy-minimization using automatic differentiation

For PEPS simulations, the configuration is parsed as a toml file. The toml file has sections for CTM, imaginary time evolution and nonlinear optimization. The used model is specified in the section [model] and needs to be present in all simulations: Here is an example for the Hubbard model:

[model]
name = "Hubbard"
# specifies on which bonds the two-site gates of the Hamiltonian are active. In the case of Hubbard model, this is the hopping term.
# V: vertical, H: horizontal, D1: diagonal top-left -> bottom-down, D2: diagonal bottom-left -> top-right
bonds = ["V", "H", "D1"] # This leads to a triangular lattice
[model.params]
U = 8.0
t = 1.0
tprime = 1.0
mu = 0.0

The unit cell and PEPS bond dimension are specified in the section [ipeps]:

[ipeps]
D = 2
# The cell can have a nontrivial pattern if numbers appear several times
pattern = [[1,2],
[2,1]] # 2x2 unit cell with pattern
#
# Alternative: specify cell as [Lx, Ly] without pattern
# cell = [3, 6]
#
# Charges can be used to force the total filling or total magnetization
# This is only possible for Abelian symmetries
# The charges must be consistent with the pattern of the cell!
charges = [[[+1], [-1]],
[[-1], [+1]]] # This charge pattern forces a Neel state with zero total magnetization

CTM

The CTM procedure can be configured using toml configuration section [ctm]

[ctm]
chi = 30
max_presteps = 30 # Maximum amount of steps without AD backtracking
track_steps = 4 # Fixed number of steps for AD backtracking after the presteps are run. (Only used for gradient based optimization)
tol_E = 1e-10
reinit_env_tol = 1e-3
init = "FROM_A" # Controls how the C and T-tensors are initialized
verbosity = "PER_ITERATION"

Simple update

Imaginary time evolution can be configured through the toml section [Imag].

[Imag]
update = "SIMPLE" # Currently this is the only supported update. After implemented, this can be set to e.g. FULL for full update
tol = 0.1
resume = false
dts = [0.1, 0.05, 0.02, 0.01]
t_steps = [60, 60, 60, 60]
Ds = [2, 3, 4, 5]
chis = [[20, 30], [30], [40], [50]]
load = "path/to/previous/state"
load_format = "NATIVE" # NATIVE: State saved within Xped, MATLAB: State saved from matlab simulation
working_directory = "<wd>"
obs_directory = "obs"
logging_directory = "log"
seed = 10
id = 1
verbosity = "PER_ITERATION"

Energy minimization

Simulations for direct energy minimization using nonlinear optimization procedures are configured through the toml section [optim].

[optim]
algorithm = "L_BFGS" # Could also be CONJUGATE_GRADIENT or what a custom optimization library supports
linesearch = "WOLFE"
bfgs_scaling = false
grad_tol = 1.0e-4
cost_tol = 1.0e-8
step_tol = 1.0e-10
resume = false
load = "/path/to/other/state"
load_format = "NATIVE" # NATIVE: State saved within Xped, MATLAB: State saved from matlab simulation
max_steps = 50
log_format = ".log" # can also be set to .h5 for hdf5 log output
working_directory = "<wd>"
obs_directory = "obs"
logging_directory = "log"
seed = 1
id = 1
verbosity = "PER_ITERATION"