Skip to content

A library for Abelian symmetry preserving tensors in Python 3

License

Notifications You must be signed in to change notification settings

mhauru/abeliantensors

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

76 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Introduction

Readthedocs status badge Travis status badge Codecov status badge

abeliantensors is a Python 3 package that implements U(1) and ℤₙ symmetry preserving tensors, as described by Singh et al. in arXiv: 0907.2994 and arXiv: 1008.4774. abeliantensors has been designed for use in tensor network algorithms, and works well with the ncon function.

Installation

If you just want to use the library:

pip install --user abeliantensors

If you also want to modify and develop the library

git clone https://github.com/mhauru/abeliantensors
pip install --user -e abeliantensors/[tests,doc]

Usage

For reference documentation see here.

abeliantensors exports classes TensorU1, TensorZ2, and TensorZ3. Other cyclic groups ℤₙ can be implemented with one-liners, see the file symmetrytensors.py for examples. abeliantensors also exports a class called Tensor, that is just a wrapper around regular NumPy ndarrays, but that implements the exact same interface as the symmetric tensor classes. This allows for easy switching between utilizing and not utilizing the symmetry preserving tensors by simply changing the class that is imported.

Each symmetric tensor has, in addition to its tensor elements, the following pieces of what we call form data:

  • shape describes the dimensions of the tensors, just like with NumPy arrays. The difference is that for symmetric tensors the dimension of each index isn't just a number, but a list of numbers, that sets how the vector space is partitioned by the irreducible representations (irreps) of the symmetry. So for instance shape=[[2,3], [5,4]] could be the shape of a Z2 symmetric matrix of dimensions 5 x 9, where the first 2 rows and 5 columns are associated with one of the two irreps of Z2, and the remaining 3 rows and 4 columns with the other.
  • qhape is like shape, but lists the irrep charges instead of the dimensions. Irrep charges are often also called quantum numbers, hence the q. In the above example qhape=[[0,1], [0,1]] would mark the first part of both the row and column space to belong to the trivial irrep of charge 0, and the second part to the irrep with charge 1. For ℤₙ the possible charges are 0, 1, ..., n, for U(1) they are all positive and negative integers.
  • dirs is a list of 1s and -1s, that gives a direction to each index: either 1 for outgoing or -1 for ingoing.
  • charge is an integer, the irrep charge associated to the tensor. In most cases you want charge=0, which is also the default when creating new tensors.

Note that each element of the tensor is associated with one irrep charge for each of the indices. The symmetry property is then that an element can only be non-zero if the charges from each index, multiplied by the direction of that index, add up to the charge of the tensor. Addition of charges for ℤₙ tensors is modulo n. For instance for a charge=0 TensorZ2 object this means that the charges on each leg must add up to an even number for an element to be non-zero. The whole point of this library is to store and use such symmetric tensors in an efficient way, where we don't waste memory or computation time on the elements we know are zero by symmetry, and can't accidentally let them be non-zero.

Here's a simple nonsense example of how abeliantensors can be used:

import numpy as np
from abeliantensors import TensorZ2

# Create a symmetric tensor from an ndarray. All elements that should be zero
# by symmetry are simply discarded, whether they are zero or not.
sigmaz = np.array([[1, 0], [0, -1]])
sigmaz = TensorZ2.from_ndarray(
    sigmaz, shape=[[1, 1], [1, 1]], qhape=[[0, 1], [0, 1]], dirs=[1, -1]
)

# Create a random symmetric tensor.
a = TensorZ2.random(
    shape=[[3, 2], [2, 4], [4, 4], [1, 1]],
    qhape=[[0, 1]] * 4,
    dirs=[-1, 1, 1, -1],
)

# Do a singular value decomposition of a tensor, thinking of it as a matrix
# with some of the indices combined to a single matrix index, like one does
# with numpy.reshape. Here we combine indices 0 and 2 to form the left matrix
# index, and 1 and 3 to form the right one. The indices are reshaped back to
# the original form after the SVD, so U and V are in this case order-3 tensors.
U, S, V = a.svd([0, 2], [1, 3])

# You can also do a truncated SVD, in this case to truncating to dimension 4.
U, S, V = a.svd([0, 2], [1, 3], chis=4)

# We can contract tensors together easily using the ncon package.
# Note that conjugation flips the direction of each index, as well as the
# charge of the tensor, which in this case though is 0.
from ncon import ncon
aadg = ncon((a, a.conjugate()), ([1, 2, -1, -2], [1, 2, -11, -12]))

# Finally, knowing that aadg is Hermitian, do an eigenvalue
# decomposition of it, this time truncating not to a specific dimension, but
# to a maximum relative truncation error of 1e-5.
E, U = aadg.eig([0, 1], [2, 3], hermitian=True, eps=1e-5)

There are many other user-facing methods and features, for more, see the reference documentation.

Demo and performance

The folder demo has an implementation of Levin and Nave's TRG algorithm, and a script that runs it on the square lattice Ising model, using both symmetric tensors of the TensorZ2 class and dense Tensors, and compares the run times. Below is a plot of how long it takes to run a single TRG step at various bond dimensions for both of them.

Running time of a single TRG step as a function of bond dimension, compared between using and not using symmetric tensors

Note that both axes are logarithmic.

At low bond dimensions the simple Tensor class outperforms TensorZ2, because keeping track of the symmetry structure imposes an overhead. The time complexity of the overhead is subleading as a function of bond dimension, and as one goes to higher bond dimensions the symmetric tensors become faster. Asymptotically both have the same scaling as a function of bond dimension, but the prefactor is smaller for TensorZ2 by a factor of 1/4. This is because instead of multiplying or decomposing an m x m matrix at cost m**3, we are multiplying two m/2 by m/2 matrices, at a total cost of 2*(m/2)**3 = (m**3)/4. For larger symmetry groups, the asymptotic benefit would be greater. For instance for TensorZ3, we should see an approximately 9-fold speed-up.

Similar results can be obtained for other algorithms, although the cross-over point in bond dimension will be different.

Design and structure

The implementation is built on top of NumPy. The block-wise sparse structure of the symmetry preserving tensors is implemented with Python dictionaries, the values of which are the NumPy ndarrays for the non-zero blocks.

Here's a class diagram for the library: Class diagram

The user-facing classes that one would instantiate are the ones at the bottom. TensorU1, TensorZ2, and TensorZ3 implement symmetric tensors for the three different symmetry groups, Tensor is the wrapper class around NumPy arrays.

Implementation-wise, all the fun is in AbelianTensor, that is the parent of all the symmetric tensor classes. Its methods include implementations of various common tensor operations, such as contractions and decompositions, preserving and making use of the block-wise sparse structure these tensors have. TensorCommon is a parent class of all the other classes that implements some higher-level features using the lower-level methods.

The wrapper class Tensor is designed so that any call to a method of the AbelianTensor class is also a valid call to a similarly named method of the Tensor class. All the symmetry-related information is simply discarded and some underlying NumPy function is called. Even if one doesn't use symmetry preserving tensors, the Tensor class provides some neat convenience functions, such as an easy-to-read one-liner for the transpose-reshape-decompose-reshape-transpose procedure for singular value and eigenvalue decompositions of tensors. Note that Tensor is a subclass of NumPy's ndarray, so anything you can do with ndarrays, you can also do with Tensors.

Tests

The tests folder has plenty of tests for the various classes. They can be run by calling pytest, provided abeliantensors was installed with the extras option tests.

Most of the tests are based on generating a random instance of one of the "fancy" tensor classes in this package, and confirming that the following diagram commutes:

Fancy tensor ─── map to numpy ndarray ───> ndarray
    │                                         │
    │                                         │
Do the thing                             Do the thing
    │                                         │
    │                                         │
    V                                         V
Fancy tensor ─── map to numpy ndarray ───> ndarray

Two command line arguments can be provided, --n_iters which sets how many times each test is run, with different random tensors each time (100 by default), and --tensorclass which can be used to specify which tensorclass(es) the tests are run on (by default all of them). Here's an example of how one might run a specific test repeatedly:

pytest tests/test_tensors.py::test_to_and_from_ndarray --tensorclass TensorZ2 --n_iters 1000

About

A library for Abelian symmetry preserving tensors in Python 3

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages