├── .gitignore ├── LICENSE ├── README.md ├── setup.py ├── test.py └── tncontract ├── __init__.py ├── label.py ├── matrices.py ├── onedim ├── __init__.py ├── onedim_core.py └── onedim_utils.py ├── qutip_conv.py ├── tensor.py ├── testing.py ├── tests ├── random_10site_mps.dat ├── random_10site_mps_py2.dat └── test_canonical_form_mps_conversion.py ├── tncon.py ├── twodim ├── __init__.py └── square_lattice.py └── version.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | # User-specific stuff: 92 | .idea/workspace.xml 93 | .idea/tasks.xml 94 | 95 | # Sensitive or high-churn files: 96 | .idea/dataSources.ids 97 | .idea/dataSources.xml 98 | .idea/dataSources.local.xml 99 | .idea/sqlDataSources.xml 100 | .idea/dynamic.xml 101 | .idea/uiDesigner.xml 102 | 103 | # Gradle: 104 | .idea/gradle.xml 105 | .idea/libraries 106 | .idea/* 107 | 108 | # Sensitive or high-churn files: 109 | tncontract/.idea/dataSources.ids 110 | tncontract/.idea/dataSources.xml 111 | tncontract/.idea/dataSources.local.xml 112 | tncontract/.idea/sqlDataSources.xml 113 | tncontract/.idea/dynamic.xml 114 | tncontract/.idea/uiDesigner.xml 115 | 116 | # Gradle: 117 | tncontract/.idea/gradle.xml 118 | tncontract/.idea/libraries 119 | tncontract/.idea/* 120 | 121 | # Mongo Explorer plugin: 122 | .idea/mongoSettings.xml 123 | 124 | ## File-based project format: 125 | *.iws 126 | 127 | ## Plugin-specific files: 128 | 129 | # IntelliJ 130 | /out/ 131 | 132 | # mpeltonen/sbt-idea plugin 133 | .idea_modules/ 134 | 135 | # JIRA plugin 136 | atlassian-ide-plugin.xml 137 | 138 | # Crashlytics plugin (for Android Studio and IntelliJ) 139 | com_crashlytics_export_strings.xml 140 | crashlytics.properties 141 | crashlytics-build.properties 142 | fabric.properties 143 | 144 | 145 | *.egg-info 146 | *.pyc 147 | __pycache__ 148 | 149 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Andrew Darmawan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tncontract 2 | **Note from developer: tncontract has reached a usable state, however is not currently being actively developed. Maintenance/support may occasionally be provided if time allows, however should not be expected.** 3 | 4 | **tncontract** is an open-source tensor-network library for Python. The goal of tncontract is to provide a simple and intuitive framework for writing tensor-network algorithms. The tncontract library uses the powerful NumPy library as a numerical backend. It can easily interface with many other Python libraries, and has built-in conversions for the popular quantum library: QuTiP. Currently, tncontract includes many algorithms for one-dimensional and two-dimensional tensor networks. 5 | 6 | ## Installation 7 | 8 | tncontract requires recent versions of Python, NumPy, and SciPy (note there appear to be problems with the Python2 support, so Python3 is recommended). To install tncontract, download the source code using the link above, then in the root directory of the package run 9 | 10 | ```shell 11 | $ python setup.py install 12 | ``` 13 | 14 | ## Code Examples 15 | 16 | Here are some simple examples showing how to define and contract tensors in tncontract. To define a Tensor object, the user provides an array-like object and a label for each index (axis) of the array. These labels are persistent, i.e. they will refer to the same indices after the tensor has been contracted with other tensors. Here we define a 2x2 tensor and assign labels "spam" and "eggs" to, respectively, the first and second indices of the tensor. 17 | ```python 18 | >>> import tncontract as tn 19 | >>> A = tn.Tensor([[1, 2], [3, 4]], labels = ["spam", "eggs"]) 20 | >>> print(A) 21 | Tensor object: shape = (2, 2), labels = ['spam', 'eggs'] 22 | ``` 23 | The data is stored as a numpy array. 24 | ```python 25 | >>> A.data 26 | array([[1, 2], 27 | [3, 4]]) 28 | ``` 29 | 30 | Here we define a 2x3x2x4 tensor with random entries with index labels given, respectively, by "i0", "i1", "i2" and "i3". 31 | ```python 32 | >>> B = tn.random_tensor(2, 3, 2, 4, labels = ['i0', 'i1', 'i2', 'i3']) 33 | >>> print(B) 34 | Tensor object: shape = (2, 3, 2, 4), labels = ['i0', 'i1', 'i2', 'i3'] 35 | ``` 36 | 37 | To perform a simple, pairwise tensor contraction we specify a pair of tensors and an index to contract from each tensor. Given A and B, defined above, we contract the "spam" index of tensor A with the "i2" index of tensor B. 38 | 39 | ```python 40 | >>> C = tn.contract(A, B, "spam", "i2") 41 | >>> print(C) 42 | Tensor object: shape = (2, 2, 3, 4), labels = ['eggs', 'i0', 'i1', 'i3'] 43 | ``` 44 | The indices of the resulting tensor C are the uncontracted indices of tensors A and B. You can see that their labels have been preserved. 45 | 46 | We can simultaneously contract multiple indices. For instance, to contract the "spam" index of A with the "i0" index of B and at the same time contract the "eggs" index of A with the "i2" index of B we would use 47 | ```python 48 | >>> D = tn.contract(A, B, ["spam", "eggs"], ["i0", "i2"]) 49 | >>> print(D) 50 | Tensor object: shape = (3, 4), labels = ['i1', 'i3'] 51 | ``` 52 | The following shorthand can be used to perform the same operation. 53 | ```python 54 | >>> D = A["spam", "eggs"]*B["i0", "i2"] 55 | >>> print(D) 56 | Tensor object: shape = (3, 4), labels = ['i1', 'i3'] 57 | ``` 58 | The following contracts a pair of indices within the same tensor. 59 | ```python 60 | >>> B.trace("i0", "i2") 61 | >>> print(B) 62 | Tensor object: shape = (3, 4), labels = ['i1', 'i3'] 63 | ``` 64 | ### Contract multiple tensors 65 | 66 | tncontract contains the function, con, to perform general contractions of multiple Tensor objects. It is similar in purpose to NCON, described in [arxiv.org/abs/1402.0939](arxiv.org/abs/1402.0939), but is designed to work with the Tensor objects of tncontract. 67 | 68 | For the examples below, we define three tensors 69 | 70 | ```python 71 | >>> A = tn.Tensor(np.random.rand(3,2,4), labels=["a", "b", "c"]) 72 | >>> B = tn.Tensor(np.random.rand(3,4), labels=["d", "e"]) 73 | >>> C = tn.Tensor(np.random.rand(5,5,2), labels=["f", "g", "h"]) 74 | ``` 75 | 76 | #### Contract a pair indices between two tensors 77 | The following contracts pairs of indices "a","d" and "c","e" of tensors 78 | `A` and `B`. It is identical to `A["a", "c"]*B["d", "e"]` 79 | 80 | ```python 81 | >>> tn.con(A, B, ("a", "d" ), ("c", "e")) 82 | Tensor object: shape = (2), labels = ["b"] 83 | ``` 84 | 85 | #### Contract a pair of indices beloning to one tensor (internal edges) 86 | The following contracts the "f" and "g" indices of tensor `C` 87 | 88 | ```python 89 | >>> tn.con(C, ("f", "g")) 90 | Tensor object: shape = (2), labels = ["h"] 91 | ``` 92 | 93 | #### Return the tensor product of a pair of tensors 94 | After all indices have been contracted, con will return the tensor 95 | product of the disconnected components of the tensor contraction. The 96 | following example returns the tensor product of `A` and `B`. 97 | 98 | ```python 99 | >>> tn.con(A, B) 100 | Tensor object: shape = (3, 2, 4, 3, 4), labels = ["a", "b", "c", "d", "e"] 101 | ``` 102 | 103 | #### Contract a network of several tensors 104 | 105 | It is possible to contract a network of several tensors. Internal edges are 106 | contracted first then edges connecting separate tensors, and then the 107 | tensor product is taken of the disconnected components resulting from the 108 | contraction. Edges between separate tensors are contracted in the order 109 | they appear in the argument list. The result of the example below is a 110 | scalar (since all indices will be contracted). 111 | 112 | ```python 113 | >>> tn.con(A, B, C, ("a", "d" ), ("c", "e"), ("f", "g"), ("h", "b")) 114 | ``` 115 | 116 | #### Notes 117 | 118 | Lists of tensors and index pairs for contraction may be used as arguments. 119 | The following example contracts 100 rank 2 tensors in a ring with periodic 120 | boundary conditions. 121 | 122 | ```python 123 | >>> N = 100 124 | >>> A = tn.Tensor(np.random.rand(2,2), labels=["left","right"]) 125 | >>> tensor_list = [A.suf(str(i)) for i in range(N)] 126 | >>> idx_pairs = [("right"+str(j), "left"+str(j+1)) for j in range(N-1)] 127 | >>> tn.con(tensor_list, idx_pairs, ("right"+str(N-1), "left0")) 128 | ``` 129 | 130 | ## Contributors 131 | 132 | [Andrew Darmawan](https://github.com/andrewdarmawan) Yukawa Institute for Theoretical Physics (YITP) — Kyoto University 133 | 134 | [Arne Grimsmo](https://github.com/arnelg) The University of Sydney 135 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | # Get version from tncontract/version.py 4 | exec(open("tncontract/version.py").read()) 5 | 6 | setup( 7 | name = "tncontract", 8 | version = __version__, 9 | packages = find_packages(), 10 | author = "Andrew Darmawan", 11 | license = "MIT", 12 | install_requires = ["numpy", "scipy"], 13 | ) 14 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tncontract/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | tncontract 3 | ========== 4 | 5 | A simple tensor-network library. 6 | 7 | Available subpackages 8 | --------------------- 9 | onedim 10 | Special purpose classes, methods and function for one dimensional 11 | tensor networs 12 | 13 | twodim 14 | Special purpose classes, methods and function for two dimensional 15 | tensor networs 16 | """ 17 | 18 | from tncontract.version import __version__ 19 | from tncontract.tensor import * 20 | from tncontract.label import * 21 | from tncontract.tncon import con 22 | import tncontract.testing 23 | import tncontract.matrices 24 | import tncontract.onedim 25 | import tncontract.twodim 26 | -------------------------------------------------------------------------------- /tncontract/label.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | """ 5 | label 6 | ========== 7 | 8 | Module for string-like labels 9 | """ 10 | 11 | __all__ = ['prime_label', 'unprime_label', 'noprime_label', 'prime_level', 12 | 'unique_label'] 13 | 14 | import uuid 15 | 16 | 17 | class Label(str): 18 | """Wrapper class for priming labels""" 19 | 20 | def __new__(cls, value, **kwargs): 21 | return str.__new__(cls, value) 22 | 23 | def __init__(self, label, parent=None): 24 | self._parent = parent 25 | if isinstance(label, Label): 26 | # if input is a Label object copy its properties 27 | if parent is None: 28 | self._parent = label._parent 29 | 30 | @property 31 | def parent(self): 32 | """Return parent label""" 33 | return self._parent 34 | 35 | @property 36 | def origin(self): 37 | """Return origin label""" 38 | origin = self 39 | while hasattr(origin, "parent"): 40 | origin = origin.parent 41 | return origin 42 | 43 | @property 44 | def parents(self): 45 | """Return number of parents for label""" 46 | tmp = self 47 | level = 0 48 | while hasattr(tmp, "parent"): 49 | if tmp.parent is not None: 50 | tmp = tmp.parent 51 | level += 1 52 | else: 53 | break 54 | return level 55 | 56 | 57 | def prime_label(label, prime="'"): 58 | """Put a prime on a label object""" 59 | return Label(str(label) + prime, parent=label) 60 | 61 | 62 | def unprime_label(label, prime="'"): 63 | """Remove one prime from label object""" 64 | try: 65 | parent = label.parent 66 | except AttributeError: 67 | raise ValueError("label is not primed") 68 | if str(parent) + prime == label: 69 | return parent 70 | else: 71 | raise ValueError("label is not primed with \"" + prime + "\"") 72 | 73 | 74 | def noprime_label(label): 75 | """Remove all primes from a label object""" 76 | try: 77 | return label.origin 78 | except AttributeError: 79 | return label 80 | 81 | 82 | def prime_level(label): 83 | """Return number of primes on label object""" 84 | try: 85 | return label.parents 86 | except AttributeError: 87 | return 0 88 | 89 | 90 | def unique_label(): 91 | """Generate a long, random string that is very likely to be unique.""" 92 | return str(uuid.uuid4()) 93 | -------------------------------------------------------------------------------- /tncontract/matrices.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | """ 5 | matrices 6 | ========== 7 | 8 | Often used matrices 9 | """ 10 | 11 | import numpy as np 12 | 13 | 14 | # 15 | # Pauli spin 1/2 operators: 16 | # 17 | def sigmap(): 18 | return np.matrix([[0., 1.], [0., 0.]]) 19 | 20 | 21 | def sigmam(): 22 | return np.matrix([[0., 0.], [1., 0.]]) 23 | 24 | 25 | def sigmax(): 26 | return sigmam() + sigmap() 27 | 28 | 29 | def sigmay(): 30 | return -1j * sigmap() + 1j * sigmam() 31 | 32 | 33 | def sigmaz(): 34 | return np.matrix([[1., 0.], [0., -1.]]) 35 | 36 | 37 | def destroy(dim): 38 | """ 39 | Destruction (lowering) operator. 40 | 41 | Parameters 42 | ---------- 43 | dim : int 44 | Dimension of Hilbert space. 45 | """ 46 | return np.matrix(np.diag(np.sqrt(range(1, dim)), 1)) 47 | 48 | 49 | def create(dim): 50 | """ 51 | Creation (raising) operator. 52 | 53 | Parameters 54 | ---------- 55 | dim : int 56 | Dimension of Hilbert space. 57 | """ 58 | return destroy(dim).getH() 59 | 60 | 61 | def identity(dim): 62 | """ 63 | Identity operator 64 | 65 | Parameters 66 | ---------- 67 | dim : int 68 | Dimension of Hilbert space. 69 | 70 | """ 71 | return np.matrix(np.identity(dim)) 72 | 73 | 74 | def basis(dim, i): 75 | """ 76 | dim x 1 column vector with all zeros except a one at row i 77 | """ 78 | vec = np.zeros(dim) 79 | vec[i] = 1.0 80 | return np.matrix(vec).T 81 | -------------------------------------------------------------------------------- /tncontract/onedim/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | onedimensional 3 | ========== 4 | 5 | Subpackage for one dimensional tensor networks 6 | """ 7 | from tncontract.onedim.onedim_core import * 8 | from tncontract.onedim.onedim_utils import * 9 | -------------------------------------------------------------------------------- /tncontract/onedim/onedim_core.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | """ 5 | onedim_core 6 | ========== 7 | 8 | Core module for onedimensional tensor networks 9 | """ 10 | 11 | __all__ = ['MatrixProductState', 'MatrixProductStateCanonical', 12 | 'MatrixProductOperator', 'OneDimensionalTensorNetwork', 13 | 'check_canonical_form_mps', 14 | 'contract_mps_mpo', 'contract_multi_index_tensor_with_one_dim_array', 15 | 'contract_virtual_indices', 'frob_distance_squared', 16 | 'inner_product_mps', 'ladder_contract', 'left_canonical_form_mps', 17 | 'mps_complex_conjugate', 'reverse_mps', 'right_canonical_form_mps', 18 | 'svd_compress_mps', 'variational_compress_mps', 'tensor_to_mpo', 19 | 'tensor_to_mps', 20 | 'right_canonical_to_canonical', 'left_canonical_to_canonical', 21 | 'canonical_to_right_canonical', 'canonical_to_left_canonical', 22 | ] 23 | 24 | import numpy as np 25 | 26 | from tncontract import tensor as tsr 27 | from tncontract.label import unique_label 28 | from tncontract.onedim.onedim_utils import init_mps_allzero 29 | 30 | 31 | class OneDimensionalTensorNetwork: 32 | """ 33 | A one-dimensional tensor network. MatrixProductState and 34 | MatrixProductOperator are subclasses of this class. 35 | 36 | An instance of `OneDimensionalTensorNetwork` contains a one-dimensional 37 | array of tensors in its `data` attribute. This one dimensional array is 38 | specified in the `tensors` argument when initialising the array. Each 39 | tensor in `data` requires a left index and a right index. The right index 40 | is taken to be contracted with the left index of the next tensor in the 41 | array, while the left index is taken to be contracted with the right index 42 | of the previous tensor in the array. All left indices are assumed to have 43 | the same label, and likewise for the right indices. They are specified in 44 | the initialisation of array (by default they are assumed to be "left" and 45 | "right" respectively) and will be stored in the attributes `left_label` and 46 | `right_label` of the OneDimensionalTensorNetwork instance. 47 | 48 | """ 49 | 50 | def __init__(self, tensors, left_label="left", right_label="right"): 51 | self.left_label = left_label 52 | self.right_label = right_label 53 | # Copy input tensors to the data attribute 54 | self.data = np.array([x.copy() for x in tensors]) 55 | # Every tensor will have three indices corresponding to "left", "right" 56 | # and "phys" labels. If only two are specified for left and right 57 | # boundary tensors (for open boundary conditions) an extra dummy index 58 | # of dimension 1 will be added. 59 | for x in self.data: 60 | if left_label not in x.labels: x.add_dummy_index(left_label) 61 | if right_label not in x.labels: x.add_dummy_index(right_label) 62 | 63 | # Container emulation 64 | 65 | def __iter__(self): 66 | return self.data.__iter__() 67 | 68 | def __len__(self): 69 | return self.data.__len__() 70 | 71 | def __getitem__(self, key): 72 | return self.data.__getitem__(key) 73 | 74 | def __setitem__(self, key, value): 75 | self.data.__setitem__(key, value) 76 | 77 | def copy(self): 78 | """Alternative the standard copy method, returning a 79 | OneDimensionalTensorNetwork that is not 80 | linked in memory to the previous ones.""" 81 | return OneDimensionalTensorNetwork([x.copy() for x in self], 82 | self.left_label, self.right_label) 83 | 84 | def reverse(self): 85 | self.data = self.data[::-1] 86 | temp = self.left_label 87 | self.left_label = self.right_label 88 | self.right_label = temp 89 | 90 | def complex_conjugate(self): 91 | """Will complex conjugate every entry of every tensor in array.""" 92 | for x in self.data: 93 | x.conjugate() 94 | 95 | def swap_gate(self, i, threshold=1e-15): 96 | """ 97 | Apply a swap gate swapping all "physical" (i.e., non-"left" and 98 | non-"right") indices for site i and i+1 of a 99 | OneDimensionalTensorNetwork. 100 | 101 | Parameters 102 | ---------- 103 | i : int 104 | threshold : float 105 | Lower bound on the magnitude of singular values to keep. Singular 106 | values less than or equal to this value will be truncated. 107 | 108 | Notes 109 | ----- 110 | The swap is implemented by SVD as described 111 | in Y.-Y. Shi et al, Phys. Rev. A 74, 022320 (2006). 112 | """ 113 | A = self[i] 114 | B = self[i + 1] 115 | A_phys_labels = [l for l in A.labels if l != self.left_label and 116 | l != self.right_label] 117 | B_phys_labels = [l for l in B.labels if l != self.left_label and 118 | l != self.right_label] 119 | A.prime_label(A_phys_labels) 120 | t = tsr.contract(A, B, self.right_label, self.left_label) 121 | U, V, _ = tsr.truncated_svd(t, [self.left_label] + B_phys_labels, 122 | chi=0, threshold=threshold, absorb_singular_values='both') 123 | U.replace_label('svd_in', self.right_label) 124 | self[i] = U 125 | V.unprime_label(A_phys_labels) 126 | V.replace_label('svd_out', self.left_label) 127 | self[i + 1] = V 128 | 129 | def replace_labels(self, old_labels, new_labels): 130 | """Run `Tensor.replace_label` method on every tensor in `self` then 131 | replace `self.left_label` and `self.right_label` appropriately.""" 132 | 133 | if not isinstance(old_labels, list): 134 | old_labels = [old_labels] 135 | if not isinstance(new_labels, list): 136 | new_labels = [new_labels] 137 | 138 | for x in self.data: 139 | x.replace_label(old_labels, new_labels) 140 | 141 | if self.left_label in old_labels: 142 | self.left_label = new_labels[old_labels.index(self.left_label)] 143 | if self.right_label in old_labels: 144 | self.right_label = new_labels[old_labels.index(self.right_label)] 145 | 146 | def standard_virtual_labels(self, suffix=""): 147 | """Replace `self.left_label` with "left"+`suffix` and 148 | `self.right_label` with "right"+`suffix`.""" 149 | 150 | self.replace_labels([self.left_label, self.right_label], 151 | ["left" + suffix, "right" + suffix]) 152 | 153 | def unique_virtual_labels(self): 154 | """Replace `self.left_label` and `self.right_label` with unique labels 155 | generated by tensor.unique_label().""" 156 | 157 | self.replace_labels([self.left_label, self.right_label], 158 | [unique_label(), unique_label()]) 159 | 160 | def leftdim(self, site): 161 | """Return left index dimesion for site""" 162 | return self.data[site].index_dimension(self.left_label) 163 | 164 | def rightdim(self, site): 165 | """Return right index dimesion for site""" 166 | return self.data[site].index_dimension(self.right_label) 167 | 168 | def bonddims(self): 169 | """Return list of all bond dimensions""" 170 | if self.nsites == 0: 171 | return [] 172 | bonds = [self.leftdim(0)] 173 | for i in range(self.nsites): 174 | bonds.append(self.rightdim(i)) 175 | return bonds 176 | 177 | @property 178 | def nsites(self): 179 | return len(self.data) 180 | 181 | @property 182 | def nsites_physical(self): 183 | return self.nsites 184 | 185 | 186 | class MatrixProductState(OneDimensionalTensorNetwork): 187 | """Matrix product state"is a list of tensors, each having and index 188 | labelled "phys" and at least one of the indices "left", "right" 189 | Input is a list of tensors, with three up to three index labels, If the 190 | labels aren't already specified as "left", "right", "phys" need to specify 191 | which labels correspond to these using arguments left_label, right_label, 192 | phys_label. The tensors input will be copied, and will not point in memory 193 | to the original ones.""" 194 | 195 | def __init__(self, tensors, left_label="left", right_label="right", 196 | phys_label="phys"): 197 | OneDimensionalTensorNetwork.__init__(self, tensors, 198 | left_label=left_label, right_label=right_label) 199 | self.phys_label = phys_label 200 | 201 | def __repr__(self): 202 | return ("MatrixProductState(tensors=%r, left_label=%r, right_label=%r," 203 | "phys_label=%r)" % (self.data, self.left_label, self.right_label, 204 | self.phys_label)) 205 | 206 | def __str__(self): 207 | return ("MatrixProductState object: " + 208 | "sites = " + str(len(self)) + 209 | ", left_label = " + self.left_label + 210 | ", right_label = " + self.right_label + 211 | ", phys_label = " + self.phys_label) 212 | 213 | def copy(self): 214 | """Return an MPS that is not linked in memory to the original.""" 215 | return MatrixProductState([x.copy() for x in self], self.left_label, 216 | self.right_label, self.phys_label) 217 | 218 | def left_canonise(self, start=0, end=-1, chi=None, threshold=1e-14, 219 | normalise=False, qr_decomposition=False): 220 | """ 221 | Perform left canonisation of MPS. 222 | 223 | Left canonisation refers to putting the MatrixProductState in a form 224 | where the tensors are isometric maps from the left and physical 225 | indices to the right index. This is achieved using successive 226 | singular-value decompositions and exploiting the gauge freedom of the 227 | MPS. For more details, see U. Schollwock, Ann. Phys. 326 (2011) 96-192. 228 | If no arguments are supplied, every tensor will be put in this form, 229 | i.e. the MPS will be put in left-canonical form. Canonisation of 230 | a segment of also possible by specifying the `start` and `end` 231 | parameters. Truncating singular values can be performed by specifying 232 | `chi` and `threshold`. If `normalise`=True and the entire MPS is to be 233 | left-canonised, the resulting MPS will represent a normalised state. If 234 | only a segment of the MPS is to be left canonised, then `normalise` 235 | will have no effect (the resulting state will have same norm as input). 236 | 237 | Parameters 238 | ---------- 239 | start : int 240 | end : int 241 | The segment of the MPS to be left canonised. All tensors from 242 | `start` to `end`-1 will be left canonised. `end`=-1 implies that 243 | the MPS will be canonised to the right boundary. 244 | chi : int 245 | Maximum number of singular values of each tensor to keep after 246 | performing singular-value decomposition. 247 | threshold : float 248 | Lower bound on the magnitude of singular values to keep. Singular 249 | values less than or equal to this value will be truncated. 250 | normalise : bool 251 | False value indicates resulting state will have same norm as 252 | original. True value indicates that, if the entire MPS is to be 253 | left canonised, it will be divided by a factor such that it is 254 | normalised (have norm=1). Has no effect if only a segment of the 255 | MPS is to be left canonised (resulting state will have the same 256 | norm as input). 257 | qr_decomposition : bool 258 | True specifies that a QR decomposition is performed rather than an 259 | SVD (which may improve performance). No truncation of singular 260 | values is possible with a QR decomposition, thus `chi` and 261 | `threshold` arguments are ignored. 262 | """ 263 | N = len(self) 264 | if end == -1: 265 | end = N 266 | 267 | if qr_decomposition: 268 | for i in range(start, end): 269 | if i == N - 1: 270 | # The final QR has no right index, so R are just 271 | # scalars. R is the norm of the state. 272 | norm = np.linalg.norm(self[i].data) 273 | #If the norm of the state is zero, convert self a zero product state. 274 | if norm==0.0: 275 | for k in range(N): 276 | self[k].data=np.zeros((self[k].index_dimension(self.phys_label), 1,1)) 277 | self[k].labels=[self.phys_label, self.left_label, self.right_label] 278 | return 279 | if normalise == True and start == 0: # Whole chain is canonised 280 | self[i].data = self[i].data / norm 281 | return 282 | else: 283 | qr_label = unique_label() 284 | Q, R = tsr.tensor_qr(self[i], [self.phys_label, 285 | self.left_label], qr_label=qr_label) 286 | 287 | # Replace tensor at site i with Q 288 | Q.replace_label(qr_label + "in", self.right_label) 289 | self[i] = Q 290 | 291 | # Absorb R into next tensor 292 | self[i + 1] = tsr.contract(R, self[i + 1], self.right_label, 293 | self.left_label) 294 | self[i + 1].replace_label(qr_label + "out", self.left_label) 295 | 296 | else: 297 | # At each step will divide by a constant so that the largest singular 298 | # value of S is 1. Will store the product of these constants in `norm` 299 | norm = 1 300 | for i in range(start, end): 301 | if i == N - 1: 302 | # The final SVD has no right index, so S and V are just scalars. 303 | # S is the norm of the state. 304 | #If the norm of the state is zero, convert self a zero product state. 305 | if np.linalg.norm(self[i].data)==0.0: 306 | for k in range(N): 307 | self[k].data=np.zeros((self[k].index_dimension(self.phys_label), 1,1)) 308 | self[k].labels=[self.phys_label, self.left_label, self.right_label] 309 | return 310 | if normalise == True and start == 0: # Whole chain is canonised 311 | self[i].data = self[i].data / np.linalg.norm(self[i].data) 312 | else: 313 | self[i].data = self[i].data * norm 314 | return 315 | else: 316 | svd_label = unique_label() 317 | U, S, V = tsr.tensor_svd(self[i], [self.phys_label, 318 | self.left_label], svd_label=svd_label) 319 | 320 | # Truncate to threshold and to specified chi 321 | singular_values = np.diag(S.data) 322 | largest_singular_value = singular_values[0] 323 | if largest_singular_value==0.0: 324 | #Return an MPS of same size but all entries zero 325 | #And virtual bond dimension 1 326 | #i.e. a product state of zeros 327 | for k in range(N): 328 | self[k].data=np.zeros((self[k].index_dimension(self.phys_label), 1,1)) 329 | self[k].labels=[self.phys_label, self.left_label, self.right_label] 330 | return 331 | 332 | # Normalise S 333 | singular_values = singular_values / largest_singular_value 334 | norm *= largest_singular_value 335 | 336 | singular_values_to_keep = singular_values[singular_values > 337 | threshold] 338 | if chi: 339 | singular_values_to_keep = singular_values_to_keep[:chi] 340 | S.data = np.diag(singular_values_to_keep) 341 | # Truncate corresponding singular index of U and V 342 | U.data = U.data[:, :, 0:len(singular_values_to_keep)] 343 | V.data = V.data[0:len(singular_values_to_keep)] 344 | 345 | U.replace_label(svd_label + "in", self.right_label) 346 | self[i] = U 347 | self[i + 1] = tsr.contract(V, self[i + 1], self.right_label, 348 | self.left_label) 349 | self[i + 1] = tsr.contract(S, self[i + 1], [svd_label + "in"], 350 | [svd_label + "out"]) 351 | self[i + 1].replace_label(svd_label + "out", self.left_label) 352 | 353 | # Reabsorb normalisation factors into next tensor 354 | # Note if i==N-1 (end of chain), this will not be reached 355 | # and normalisation factors will be taken care of in the earlier 356 | # block. 357 | if i == end - 1: 358 | self[i + 1].data *= norm 359 | 360 | def right_canonise(self, start=0, end=-1, chi=None, threshold=1e-14, 361 | normalise=False, qr_decomposition=False): 362 | """Perform right canonisation of MPS. Identical to `left_canonise` 363 | except that process is mirrored (i.e. canonisation is performed from 364 | right to left). `start` and `end` specify the interval to be canonised. 365 | 366 | Notes 367 | ----- 368 | The first tensor to be canonised is `end`-1 and the final tensor to be 369 | canonised is `start`""" 370 | 371 | self.reverse() 372 | N = len(self) 373 | if end == -1: 374 | end = N 375 | self.left_canonise(start=N - end, end=N - start, chi=chi, 376 | threshold=threshold, normalise=normalise, 377 | qr_decomposition=qr_decomposition) 378 | self.reverse() 379 | 380 | def replace_labels(self, old_labels, new_labels): 381 | """run `tensor.replace_label` method on every tensor in `self` then 382 | replace `self.left_label`, `self.right_label` and `self.phys_label` 383 | appropriately.""" 384 | 385 | if not isinstance(old_labels, list): 386 | old_labels = [old_labels] 387 | if not isinstance(new_labels, list): 388 | new_labels = [new_labels] 389 | 390 | for x in self.data: 391 | x.replace_label(old_labels, new_labels) 392 | 393 | if self.left_label in old_labels: 394 | self.left_label = new_labels[old_labels.index(self.left_label)] 395 | if self.right_label in old_labels: 396 | self.right_label = new_labels[old_labels.index(self.right_label)] 397 | if self.phys_label in old_labels: 398 | self.phys_label = new_labels[old_labels.index(self.phys_label)] 399 | 400 | def standard_labels(self, suffix=""): 401 | """ 402 | overwrite self.labels, self.left_label, self.right_label, 403 | self.phys_label with standard labels "left", "right", "phys" 404 | """ 405 | self.replace_labels([self.left_label, self.right_label, 406 | self.phys_label], ["left" + suffix, "right" + suffix, "phys" + suffix]) 407 | 408 | def check_canonical_form(self, threshold=1e-14, print_output=True): 409 | """Determines which tensors in the MPS are left canonised, and which 410 | are right canonised. Returns the index of the first tensor (starting 411 | from left) that is not left canonised, and the first tensor (starting 412 | from right) that is not right canonised. If print_output=True, will 413 | print useful information concerning whether a given MPS is in a 414 | canonical form (left, right, mixed).""" 415 | mps_cc = mps_complex_conjugate(self) 416 | first_site_not_left_canonised = len(self) - 1 417 | for i in range(len(self) - 1): 418 | I = tsr.contract(self[i], mps_cc[i], 419 | [self.phys_label, self.left_label], 420 | [mps_cc.phys_label, mps_cc.left_label]) 421 | # Check if tensor is left canonised. 422 | if np.linalg.norm(I.data - np.identity(I.data.shape[0])) > threshold: 423 | first_site_not_left_canonised = i 424 | break 425 | first_site_not_right_canonised = 0 426 | for i in range(len(self) - 1, 0, -1): 427 | I = tsr.contract(self[i], mps_cc[i], 428 | [self.phys_label, self.right_label], 429 | [mps_cc.phys_label, mps_cc.right_label]) 430 | # Check if tensor is right canonised. 431 | if np.linalg.norm(I.data - np.identity(I.data.shape[0])) > threshold: 432 | first_site_not_right_canonised = i 433 | break 434 | if print_output: 435 | if first_site_not_left_canonised == first_site_not_right_canonised: 436 | if first_site_not_left_canonised == len(self) - 1: 437 | if abs(np.linalg.norm(self[-1].data) - 1) > threshold: 438 | print("MPS in left canonical form (unnormalised)") 439 | else: 440 | print("MPS in left canonical form (normalised)") 441 | elif first_site_not_left_canonised == 0: 442 | if abs(np.linalg.norm(self[0].data) - 1) > threshold: 443 | print("MPS in right canonical form (unnormalised)") 444 | else: 445 | print("MPS in right canonical form (normalised)") 446 | else: 447 | print("MPS in mixed canonical form with orthogonality " 448 | "centre at site " + 449 | str(first_site_not_right_canonised)) 450 | else: 451 | if first_site_not_left_canonised == 0: 452 | print("No tensors left canonised") 453 | else: 454 | print("Tensors left canonised up to site " + 455 | str(first_site_not_left_canonised)) 456 | if first_site_not_right_canonised == len(self) - 1: 457 | print("No tensors right canonised") 458 | else: 459 | print("Tensors right canonised up to site " + 460 | str(first_site_not_right_canonised)) 461 | return (first_site_not_left_canonised, first_site_not_right_canonised) 462 | 463 | def svd_compress(self, chi=None, threshold=1e-15, normalise=False, 464 | reverse=False): 465 | """Compress MPS to a given bond dimension `chi` or to a minimum 466 | singular value `threshold` using SVD compression as described in U. 467 | Schollwock, Ann. Phys. 326 (2011) 96-192. This is achieved by 468 | performing two successive canonisations. If `reverse` is False, 469 | canonisation is first performed from left to right (with QR 470 | decomposition) then the resulting state is canonised from right to left 471 | (using SVD decomposition). The resulting MPS is in right canonical 472 | form. If `reverse` is True this is mirrored, resulting in a state in 473 | left canonical form. """ 474 | if reverse: 475 | self.reverse() 476 | self.left_canonise(normalise=False, qr_decomposition=True) 477 | # Normalise the state temporarily 478 | norm = self.norm(canonical_form="left") 479 | self[-1].data /= norm 480 | self.right_canonise(chi=chi, threshold=threshold, normalise=False) 481 | if normalise == False: 482 | self[0].data *= norm 483 | if reverse: 484 | self.reverse() 485 | 486 | def variational_compress(self, chi, max_iter=10, initial_guess=None, 487 | tolerance=1e-15, normalise=False): 488 | """Compress MPS to a given bond dimension `chi` or to the same bond 489 | dimensions as an optional input MPS `initial_guess` using an iterative 490 | compression procedure described in U. Schollwock, Ann. Phys. 326 (2011) 491 | 96-192. The algorithm will start from an initial guess for the target 492 | MPS, either computed with the `svd_compress` method or, if supplied, 493 | with the `initial_guess` keyword argument. It will sweep over the 494 | chain, successively optimising individual tensors until convergence. 495 | The output MPS will be in right canonical form. Should be more 496 | accurate, albeit slower, than `svd_compress` method. 497 | 498 | Parameters 499 | ---------- 500 | 501 | chi : int 502 | Bond dimension of resulting MPS. 503 | 504 | max_iter : int 505 | Maximum number of full updates to perform, where a full update 506 | consists of a sweep from right to left, then left to right. If 507 | convergence is not reached after `max_iter` full updates, an error 508 | will be returned. 509 | 510 | initial_guess : MatrixProductState 511 | Starting point for variational algorithm. Output MPS will have the 512 | same bond dimension as `initial_guess`. If not provided, an SVD 513 | compression of the input MPS will be computed and used as the 514 | starting point. 515 | 516 | tolerance : float 517 | After a full update is completed, the difference in norm with the 518 | target state for the last two sweeps is computed. The algorithm 519 | will be regarded as having converged and will stop if this 520 | difference is less than `tolerance`. 521 | """ 522 | if initial_guess == None: 523 | mps = self.copy() 524 | # Make sure state is in left canonical form to start 525 | mps.svd_compress(chi=chi, reverse=True) 526 | else: 527 | mps = initial_guess 528 | # Put state in left canonical form 529 | mps.left_canonise(qr_decomposition=True) 530 | 531 | # Give mps1 unique labels 532 | mps.replace_labels([mps.left_label, mps.right_label, mps.phys_label], 533 | [unique_label(), unique_label(), unique_label()]) 534 | 535 | le_label = unique_label() 536 | left_environments = ladder_contract(mps, self, mps.phys_label, 537 | self.phys_label, return_intermediate_contractions=True, 538 | right_output_label=le_label, complex_conjugate_array1=True) 539 | 540 | def variational_sweep(mps1, mps2, left_environments): 541 | """Iteratively update mps1, to minimise frobenius distance to mps2 542 | by sweeping from right to left. Expects mps1 to be in right 543 | canonical form.""" 544 | 545 | # Get the base label of left_environments 546 | le_label = left_environments[0].labels[0][:-1] 547 | # Generate some unique labels to avoid conflicts 548 | re_label = unique_label() 549 | lq_label = unique_label() 550 | 551 | right_environments = [] 552 | norms = [mps1[-1].norm()] 553 | for i in range(mps2.nsites - 1, 0, -1): 554 | 555 | # Optimise the tensor at site i by contracting with left and 556 | # right environments 557 | updated_tensor = tsr.contract(mps2[i], left_environments[i - 1], 558 | mps2.left_label, le_label + "2") 559 | if i != mps2.nsites - 1: 560 | updated_tensor = tsr.contract(updated_tensor, 561 | right_environment, mps2.right_label, re_label + "2") 562 | updated_tensor.replace_label(re_label + "1", 563 | mps1.right_label) 564 | updated_tensor.replace_label([le_label + "1", mps2.phys_label] 565 | , [mps1.left_label, mps1.phys_label]) 566 | 567 | # Right canonise the tensor at site i using LQ decomposition 568 | # Absorb L into tensor at site i-1 569 | L, Q = tsr.tensor_lq(updated_tensor, mps1.left_label, 570 | lq_label=lq_label) 571 | Q.replace_label(lq_label + "out", mps1.left_label) 572 | L.replace_label(lq_label + "in", mps1.right_label) 573 | mps1[i] = Q 574 | mps1[i - 1] = tsr.contract(mps1[i - 1], L, mps1.right_label, 575 | mps1.left_label) 576 | 577 | # Compute norm of mps 578 | # Taking advantage of canonical form 579 | norms.append(mps1[i - 1].norm()) 580 | 581 | # Compute next column of right_environment 582 | if i == mps2.nsites - 1: 583 | right_environment = tsr.contract(tsr.conjugate(mps1[i]), 584 | mps2[i], mps1.phys_label, self.phys_label) 585 | right_environment.remove_all_dummy_indices( 586 | labels=[mps1.right_label, mps2.right_label]) 587 | else: 588 | right_environment.contract(tsr.conjugate(mps1[i]), 589 | re_label + "1", mps1.right_label) 590 | right_environment.contract(mps2[i], [mps1.phys_label, 591 | re_label + "2"], [self.phys_label, self.right_label]) 592 | 593 | right_environment.replace_label([mps1.left_label, 594 | mps2.left_label], [re_label + "1", re_label + "2"]) 595 | right_environments.append(right_environment.copy()) 596 | 597 | # At second last site, compute final tensor 598 | if i == 1: 599 | updated_tensor = tsr.contract(mps2[0], right_environment, 600 | mps2.right_label, re_label + "2") 601 | updated_tensor.replace_label([mps2.phys_label, 602 | re_label + "1"], 603 | [mps1.phys_label, mps1.right_label]) 604 | mps1[0] = updated_tensor 605 | 606 | return right_environments, np.array(norms) 607 | 608 | for i in range(max_iter): 609 | left_environments, norms1 = variational_sweep(mps, self, 610 | left_environments) 611 | mps.reverse() 612 | self.reverse() 613 | le_label = left_environments[0].labels[0][:-1] 614 | left_environments, norms2 = variational_sweep(mps, self, 615 | left_environments) 616 | mps.reverse() 617 | self.reverse() 618 | # Compute differences between norms of successive updates in second 619 | # sweep. As shown in U. Schollwock, Ann. Phys. 326 (2011) 96-192, 620 | # these quantities are equivalent to the differences between the 621 | # frobenius norms between the target state and the variational 622 | # state. 623 | if np.all(np.abs(norms2[1:] - norms2[:-1]) / norms2[1:] < tolerance): 624 | mps.replace_labels([mps.left_label, mps.right_label, 625 | mps.phys_label], [self.left_label, self.right_label, 626 | self.phys_label]) 627 | if normalise == True: 628 | mps[-1].data /= mps.norm(canonical_form="left") 629 | return mps 630 | elif i == max_iter - 1: # Has reached the last iteration 631 | raise RuntimeError("variational_compress did not converge.") 632 | 633 | def physical_site(self, n): 634 | """ Return position of n'th physical (pos=n). Implemented for 635 | comaptibility with MatrixProductStateCanonical.""" 636 | return n 637 | 638 | def physdim(self, site): 639 | """Return physical index dimesion for site""" 640 | return self.data[site].index_dimension(self.phys_label) 641 | 642 | def norm(self, canonical_form=False): 643 | """Return norm of mps. 644 | 645 | Parameters 646 | ---------- 647 | 648 | canonical_form : str 649 | If `canonical_form` is "left", the state will be assumed to be in 650 | left canonical form, if "right" the state will be assumed to be in 651 | right canonical form. In these cases the norm can be read off the 652 | last tensor (much more efficient). 653 | """ 654 | 655 | if canonical_form == "left": 656 | return np.linalg.norm(self[-1].data) 657 | elif canonical_form == "right": 658 | return np.linalg.norm(self[0].data) 659 | else: 660 | return np.sqrt(inner_product_mps(self, self)) 661 | 662 | def apply_gate(self, gate, firstsite, gate_outputs=None, gate_inputs=None, 663 | chi=None, threshold=1e-15, canonise='left'): 664 | """ 665 | Apply Tensor `gate` on sites `firstsite`, `firstsite`+1, ..., 666 | `firstsite`+`nsites`-1, where `nsites` is the length of gate_inputs. 667 | The physical index of the nth site is contracted with the nth label of 668 | `gate_inputs`. After the contraction the MPS is put back into the 669 | original form by SVD, and the nth sites physical index is given 670 | by the nth label of `gate_outputs` (but relabeled to `self.phys_label` 671 | to preserve the original MPS form). 672 | 673 | Parameters 674 | ---------- 675 | gate : Tensor 676 | Tensor representing the multisite gate. 677 | firstsite : int 678 | First site of MPS involved in the gate 679 | gate_outputs : list of str, optional 680 | Output labels corresponding to the input labels given by 681 | `gate_inputs`. Must have the same length as `gate_inputs`. 682 | If `None` the first half of `gate.labels` will be taken as output 683 | labels. 684 | gate_inputs : list of str, optional 685 | Input labels. The first index of the list is contracted with 686 | `firstsite`, the second with `firstsite`+1 etc. 687 | If `None` the second half of `gate.labels` will be taken as input 688 | labels. 689 | threshold : float 690 | Lower bound on the magnitude of singular values to keep. Singular 691 | values less than or equal to this value will be truncated. 692 | chi : int 693 | Maximum number of singular values of each tensor to keep after 694 | performing singular-value decomposition. 695 | canonise : str {'left', 'right'} 696 | Direction in which to canonise the sites after applying gate. 697 | 698 | Notes 699 | ----- 700 | At the end of the gate all physical indices are relabeled to 701 | `self.phys_label`. 702 | 703 | Only use this for gates acting on small number of sites. 704 | """ 705 | # Set gate_outputs and gate_inputs to default values if not given 706 | if gate_outputs is None and gate_inputs is None: 707 | gate_outputs = gate.labels[:int(len(gate.labels) / 2)] 708 | gate_inputs = gate.labels[int(len(gate.labels) / 2):] 709 | elif gate_outputs is None: 710 | gate_outputs = [x for x in gate.labels if x not in gate_inputs] 711 | elif gate_inputs is None: 712 | gate_inputs = [x for x in gate.labels if x not in gate_outputs] 713 | 714 | nsites = len(gate_inputs) 715 | if len(gate_outputs) != nsites: 716 | raise ValueError("len(gate_outputs) != len(gate_inputs)") 717 | 718 | # contract the sites first 719 | t = contract_virtual_indices(self, firstsite, firstsite + nsites, 720 | periodic_boundaries=False) 721 | 722 | # contract all physical indices with gate input indices 723 | t = tsr.contract(t, gate, self.phys_label, gate_inputs) 724 | 725 | # split big tensor into MPS form by exact SVD 726 | if canonise == 'right': 727 | phys_labels = gate_outputs[::-1] 728 | left_label = 'right' 729 | right_label = 'left' 730 | else: 731 | phys_labels = gate_outputs 732 | left_label = 'left' 733 | right_label = 'right' 734 | mps = tensor_to_mps(t, phys_labels=phys_labels, 735 | mps_phys_label=self.phys_label, left_label=left_label, 736 | right_label=right_label, chi=chi, threshold=threshold) 737 | if canonise == 'right': 738 | mps.reverse() 739 | self.data[firstsite:firstsite + nsites] = mps.data 740 | 741 | def expval(self, gate, firstsite, 742 | left_canonised_up_to=0, right_canonised_up_to=-1, 743 | gate_outputs=None, gate_inputs=None, 744 | ): 745 | """ 746 | Compute multi-site expectation value for operator `gate` applied to 747 | `firstsite`, `firstsite+1`, ... `firstsite_+n-1` where `n` is the 748 | number of gate inputs. 749 | 750 | Assumes that MPS has been canonised such that everything to the left 751 | of `left_canonised_up_to` is left-canonised and everything to the right 752 | of `right_canonised_up_to` is right-canonised. 753 | 754 | Parameters 755 | ---------- 756 | gate : Tensor 757 | Tensor representing the multisite gate. 758 | firstsite : int 759 | First site of MPS involved in the gate 760 | gate_outputs : list of str, optional 761 | Output labels corresponding to the input labels given by 762 | `gate_inputs`. Must have the same length as `gate_inputs`. 763 | If `None` the first half of `gate.labels` will be taken as output 764 | labels. 765 | gate_inputs : list of str, optional 766 | Input labels. The first index of the list is contracted with 767 | `firstsite`, the second with `firstsite`+1 etc. 768 | If `None` the second half of `gate.labels` will be taken as input 769 | labels. 770 | left_canonised_up_to : int 771 | Everything to the left of this is assumed to be left-canonised. 772 | right_canonised_up_to : int 773 | Everything to the right of this is assumed to be right-canonised. 774 | 775 | Returns 776 | ------ 777 | t : Tensor 778 | Contracted tensor 779 | 780 | Notes 781 | ----- 782 | The MPS is left-canonised up to `firstsite` and right-canonised up to 783 | `firstsite+n` after the operation. 784 | """ 785 | # Set gate_outputs and gate_inputs to default values if not given 786 | if gate_outputs is None and gate_inputs is None: 787 | gate_outputs = gate.labels[:int(len(gate.labels) / 2)] 788 | gate_inputs = gate.labels[int(len(gate.labels) / 2):] 789 | elif gate_outputs is None: 790 | gate_outputs = [x for x in gate.labels if x not in gate_inputs] 791 | elif gate_inputs is None: 792 | gate_inputs = [x for x in gate.labels if x not in gate_outputs] 793 | 794 | nsites = len(gate_inputs) 795 | if len(gate_outputs) != nsites: 796 | raise ValueError("len(gate_outputs) != len(gate_inputs)") 797 | 798 | N = len(self) 799 | if right_canonised_up_to == -1: 800 | right_canonised_up_to = N 801 | 802 | # Mover left/right orthogonality centers to firstsite/firtsite+n 803 | if left_canonised_up_to < firstsite: 804 | self.left_canonise(left_canonised_up_to, firstsite) 805 | if right_canonised_up_to > firstsite + nsites: 806 | self.right_canonise(firstsite + nsites, right_canonised_up_to) 807 | 808 | # contract the MPS sites first 809 | t = contract_virtual_indices(self, firstsite, firstsite + nsites, 810 | periodic_boundaries=False) 811 | td = t.copy() 812 | td.conjugate() 813 | 814 | # contract all physical indices with gate indices 815 | exp = tsr.contract(t, gate, self.phys_label, gate_inputs) 816 | exp = tsr.contract(td, exp, self.phys_label, gate_outputs) 817 | # contract boundary indices 818 | exp.tr(self.left_label, self.left_label, index1=0, 819 | index2=1) 820 | exp.tr(self.right_label, self.right_label, index1=0, 821 | index2=1) 822 | 823 | return exp 824 | 825 | def ptrace(self, firstsite, lastsite=None, 826 | left_canonised_up_to=0, right_canonised_up_to=-1): 827 | """ 828 | Compute local density matrix for sites `firstsite` to `lastsite` 829 | assuming left and right canonisation up to boundaries. 830 | 831 | Parameters 832 | ---------- 833 | firstsite : int 834 | First physical site of MPS not traced out 835 | lastsite : int 836 | Last physical site of MPS not traced out. By default `lastsite` is 837 | set to `firstsite`. 838 | left_canonised_up_to : int 839 | Everything to the left of this is assumed to be left-canonised. 840 | right_canonised_up_to : int 841 | Everything to the right of this is assumed to be right-canonised. 842 | 843 | Returns 844 | ------ 845 | t : Tensor 846 | Local density matrix 847 | 848 | Notes 849 | ----- 850 | The MPS is left-canonised up to `firstsite` and right-canonised up to 851 | `firstsite+n` after the operation. 852 | """ 853 | if right_canonised_up_to == -1: 854 | right_canonised_up_to = self.nsites 855 | if lastsite is None: 856 | lastsite = firstsite 857 | 858 | # Mover left/right orthogonality centers to firstsite/firtsite+n 859 | if left_canonised_up_to < firstsite: 860 | self.left_canonise(left_canonised_up_to, firstsite) 861 | if right_canonised_up_to > lastsite: 862 | self.right_canonise(lastsite + 1, right_canonised_up_to) 863 | 864 | start = self.physical_site(firstsite) 865 | end = self.physical_site(lastsite) 866 | t = contract_virtual_indices(self, start, end + 1, 867 | periodic_boundaries=False) 868 | td = t.copy() 869 | td.conjugate() 870 | # rename physical labels 871 | for i, l in enumerate(t.labels): 872 | if l == self.phys_label: 873 | t.labels[i] = l + "_out" + str(i) 874 | td.labels[i] = l + "_in" + str(i) 875 | rho = (t[self.left_label, self.right_label] 876 | * td[self.left_label, self.right_label]) 877 | return rho 878 | 879 | 880 | class MatrixProductStateCanonical(OneDimensionalTensorNetwork): 881 | """ 882 | Matrix product state in canonical form with every other tensor assumed to 883 | be a diagonal matrix of singular values. The site numbering is 884 | 885 | 0 1 2 3 ... N-2 N-1 886 | 887 | Lambda Gamma Lambda Gamma ... Gamma Lambda 888 | 889 | where the Gammas are rank three tensors and the Lambdas diagonal matrices 890 | of singular values. The left-mots and right-most Lambda matrices are 891 | trivial one-by-one matrices inserted for convenience. For a canonical form 892 | MPS created from a right-canonical MPS using e.g. 893 | `right_canonical_to_canonical` the right-most Lambda is equal to the norm 894 | of the state, and vice versa if created from a left-canonical MPS. 895 | 896 | The n'th physical site index (i=2*n+1) can conveniently be accessed with 897 | the `physical_site` method. 898 | 899 | Notes 900 | ----- 901 | Convenient for TEBD type algorithms. Many methods assume canonical form 902 | for efficiency. 903 | 904 | See U. Schollwock, Ann. Phys. 326 (2011) 96-192 section 4.6. 905 | """ 906 | 907 | def __init__(self, tensors, left_label="left", right_label="right", 908 | phys_label="phys"): 909 | OneDimensionalTensorNetwork.__init__(self, tensors, 910 | left_label=left_label, right_label=right_label) 911 | self.phys_label = phys_label 912 | 913 | def __repr__(self): 914 | return ("MatrixProductStateCanonical(tensors=%r, left_label=%r," 915 | "right_label=%r, phys_label=%r)" % (self.data, self.left_label, 916 | self.right_label, self.phys_label)) 917 | 918 | def __str__(self): 919 | return ("MatrixProductStateCanonical object: " + 920 | "sites (incl. singular value sites)= " + str(len(self)) + 921 | ", left_label = " + self.left_label + 922 | ", right_label = " + self.right_label + 923 | ", phys_label = " + self.phys_label) 924 | 925 | def copy(self): 926 | """Return an MPS that is not linked in memory to the original.""" 927 | return MatrixProductStateCanonical([x.copy() for x in self], 928 | self.left_label, self.right_label, self.phys_label) 929 | 930 | def replace_labels(self, old_labels, new_labels): 931 | """run `tensor.replace_label` method on every tensor in `self` then 932 | replace `self.left_label`, `self.right_label` and `self.phys_label` 933 | appropriately.""" 934 | 935 | if not isinstance(old_labels, list): 936 | old_labels = [old_labels] 937 | if not isinstance(new_labels, list): 938 | new_labels = [new_labels] 939 | 940 | for x in self.data: 941 | x.replace_label(old_labels, new_labels) 942 | 943 | if self.left_label in old_labels: 944 | self.left_label = new_labels[old_labels.index(self.left_label)] 945 | if self.right_label in old_labels: 946 | self.right_label = new_labels[old_labels.index(self.right_label)] 947 | if self.phys_label in old_labels: 948 | self.phys_label = new_labels[old_labels.index(self.phys_label)] 949 | 950 | def standard_labels(self, suffix=""): 951 | """ 952 | overwrite self.labels, self.left_label, self.right_label, 953 | self.phys_label with standard labels "left", "right", "phys" 954 | """ 955 | self.replace_labels([self.left_label, self.right_label, 956 | self.phys_label], ["left" + suffix, "right" + suffix, "phys" + suffix]) 957 | 958 | def physical_site(self, n): 959 | """ Return position of n'th physical (pos=2*n+1)""" 960 | return 2 * n + 1 961 | 962 | def singular_site(self, n): 963 | """ Return position of n'th singular value site (pos=2*n)""" 964 | return 2 * n 965 | 966 | def physdim(self, site): 967 | """Return physical index dimesion for `physical_site(site)`""" 968 | return self.data[self.physical_site(site)].index_dimension( 969 | self.phys_label) 970 | 971 | def singulardim(self, site): 972 | """Return chi for chi by chi singular matrix at 973 | `singular_site(site)`""" 974 | return self.data[self.singular_site(site)].index_dimension( 975 | self.left_label) 976 | 977 | def bonddims(self): 978 | """Return list of all bond dimensions. Note that for 979 | MatrixProductStateCanonical every other site is a diagonal chi by chi 980 | matrix. Hence, this function returns an output of the form 981 | [chi0, chi0, chi1, chi1, chi2, chi2, ...]""" 982 | return super(MatrixProductStateCanonical, self).bonddims() 983 | 984 | @property 985 | def nsites_physical(self): 986 | return int((self.nsites - 1) / 2) 987 | 988 | def norm(self, canonical_form=True): 989 | """Return norm of mps. 990 | 991 | Parameters 992 | ---------- 993 | canonical_form : bool 994 | If `canonical_form` is `True`, the state will be assumed to be in 995 | canonical form. In this case the norm can be read off from the 996 | edge singular value matrices (much more efficient). 997 | 998 | """ 999 | if canonical_form is True: 1000 | return np.linalg.norm(self[-1].data) * np.linalg.norm(self[0].data) 1001 | else: 1002 | return np.sqrt(inner_product_mps(self, self)) 1003 | 1004 | def check_canonical_form(self, threshold=1e-14, print_output=True): 1005 | """Check if MPS is in canonical form, by checking for every site: 1006 | 1) if A=Lambda Gamma satisfies 1007 | `A[phys_label, left_label]*Ad[phys_label, left_label]` where `Ad` is 1008 | the conjugate tensor, 1009 | 2) if B=Gamma Lambda satisfies 1010 | `B[phys_label, right_label]*Bd[phys_label, right_label]` where `Bd` is 1011 | the conjugate tensor. 1012 | 1013 | Returns a list of sites not satisfying 1), a list not satisfying 2), 1014 | and a list containing any un-normalised left-most or right-most sites. 1015 | If print_output=True, will print useful information concerning whether 1016 | a given MPS is in canonical form.""" 1017 | not_left_canonised = [] 1018 | not_right_canonised = [] 1019 | not_normalised = [] 1020 | for i in range(self.nsites_physical): 1021 | A = (self[self.physical_site(i) - 1][self.right_label,] 1022 | * self[self.physical_site(i)][self.left_label,]) 1023 | Ad = tsr.conjugate(A) 1024 | I = tsr.contract(A, Ad, 1025 | [self.phys_label, self.left_label], 1026 | [self.phys_label, self.left_label]) 1027 | # Check if tensor is left canonised. 1028 | if np.linalg.norm(I.data - np.identity(I.data.shape[0])) > threshold: 1029 | if i == 0 or i == self.nsites_physical - 1: 1030 | If = I.data.flatten() 1031 | if len(If[np.abs(If) > threshold]) > 1: 1032 | not_left_canonised.append(i) 1033 | else: 1034 | not_normalised.append(i) 1035 | else: 1036 | not_left_canonised.append(i) 1037 | for i in range(self.nsites_physical): 1038 | B = (self[self.physical_site(i)][self.right_label,] 1039 | * self[self.physical_site(i) + 1][self.left_label,]) 1040 | Bd = tsr.conjugate(B) 1041 | I = tsr.contract(B, Bd, 1042 | [self.phys_label, self.right_label], 1043 | [self.phys_label, self.right_label]) 1044 | # Check if tensor is right canonised. 1045 | if np.linalg.norm(I.data - np.identity(I.data.shape[0])) > threshold: 1046 | if i == 0 or i == self.nsites_physical - 1: 1047 | If = I.data.flatten() 1048 | if len(If[np.abs(If) > threshold]) > 1: 1049 | not_right_canonised.append(i) 1050 | else: 1051 | not_normalised.append(i) 1052 | else: 1053 | not_right_canonised.append(i) 1054 | if print_output: 1055 | if len(not_left_canonised) == 0 and len(not_right_canonised) == 0: 1056 | if len(not_normalised) == 0: 1057 | print("MPS in canonical form (normalised)") 1058 | else: 1059 | print("MPS in canonical form (unnormalised)") 1060 | else: 1061 | print("Physical sites not left-canonical:") 1062 | print(not_left_canonised) 1063 | print("Physical sites not right-canonical:") 1064 | print(not_right_canonised) 1065 | return not_left_canonised, not_right_canonised, not_normalised 1066 | 1067 | def compress_bond(self, singular_site, chi=None, threshold=1e-15): 1068 | """ Compress bonds connecting to `singular_site(singular_site)` by 1069 | truncating singular values. 1070 | """ 1071 | # contract the MPS sites first 1072 | site = self.singular_site(singular_site) 1073 | if self.singulardim(site) == 1: 1074 | return 1075 | start = site - 2 1076 | end = site + 2 1077 | self[end - 1].prime_label(self.phys_label) 1078 | t = contract_virtual_indices(self, start, end + 1, 1079 | periodic_boundaries=False) 1080 | # Remember singular values 1081 | S1_inv = self[start].copy() 1082 | S1_inv.inv() 1083 | S2_inv = self[end].copy() 1084 | S2_inv.inv() 1085 | # SVD and compress 1086 | U, S, V = tsr.truncated_svd(t, [self.phys_label, self.left_label], 1087 | chi=chi, threshold=threshold, absorb_singular_values=None) 1088 | U.replace_label("svd_in", self.right_label) 1089 | V.replace_label("svd_out", self.left_label) 1090 | S.replace_label(["svd_out", "svd_in"], [self.left_label, 1091 | self.right_label]) 1092 | self[start + 1] = S1_inv[self.right_label,] * U[self.left_label,] 1093 | self[start + 2] = S 1094 | self[end - 1] = V[self.right_label,] * S2_inv[self.left_label,] 1095 | self[end - 1].unprime_label(self.phys_label) 1096 | 1097 | def compress_all(self, chi=None, threshold=1e-15, normalise=False): 1098 | raise NotImplementedError 1099 | 1100 | def apply_gate(self, gate, firstsite, gate_outputs=None, gate_inputs=None, 1101 | chi=None, threshold=1e-15): 1102 | """ 1103 | Apply multi-site gate to `physical_site(firstsite)`, 1104 | `physical_site(firstsite+1)`, ... and perform optimal compression, 1105 | assuming canonical form. 1106 | 1107 | Currently only implemented for 1-site and 2-site gates. 1108 | 1109 | Parameters 1110 | ---------- 1111 | gate : Tensor 1112 | Tensor representing the multisite gate. 1113 | firstsite : int 1114 | First site of MPS involved in the gate 1115 | gate_outputs : list of str, optional 1116 | Output labels corresponding to the input labels given by 1117 | `gate_inputs`. Must have the same length as `gate_inputs`. 1118 | If `None` the first half of `gate.labels` will be taken as output 1119 | labels. 1120 | gate_inputs : list of str, optional 1121 | Input labels. The first index of the list is contracted with 1122 | `firstsite`, the second with `firstsite`+1 etc. 1123 | If `None` the second half of `gate.labels` will be taken as input 1124 | labels. 1125 | threshold : float 1126 | Lower bound on the magnitude of singular values to keep. Singular 1127 | values less than or equal to this value will be truncated. 1128 | chi : int 1129 | Maximum number of singular values of each tensor to keep after 1130 | performing singular-value decomposition. 1131 | 1132 | Notes 1133 | ----- 1134 | At the end of the gate all physical indices are relabeled to 1135 | `self.phys_label`. 1136 | 1137 | Only use this for gates acting on small number of sites. 1138 | """ 1139 | # Set gate_outputs and gate_inputs to default values if not given 1140 | if gate_outputs is None and gate_inputs is None: 1141 | gate_outputs = gate.labels[:int(len(gate.labels) / 2)] 1142 | gate_inputs = gate.labels[int(len(gate.labels) / 2):] 1143 | elif gate_outputs is None: 1144 | gate_outputs = [x for x in gate.labels if x not in gate_inputs] 1145 | elif gate_inputs is None: 1146 | gate_inputs = [x for x in gate.labels if x not in gate_outputs] 1147 | 1148 | nsites = len(gate_inputs) 1149 | if len(gate_outputs) != nsites: 1150 | raise ValueError("len(gate_outputs) != len(gate_inputs)") 1151 | if nsites > 2: 1152 | raise NotImplementedError("gate acting on more than two sites.") 1153 | 1154 | # contract the MPS sites first 1155 | start = self.physical_site(firstsite) - 1 1156 | end = self.physical_site(firstsite + nsites - 1) + 1 1157 | t = contract_virtual_indices(self, start, end + 1, 1158 | periodic_boundaries=False) 1159 | 1160 | # contract all physical indices with gate input indices 1161 | t = tsr.contract(t, gate, self.phys_label, gate_inputs) 1162 | 1163 | # split big tensor into MPS form by exact SVD 1164 | S1_inv = self[start].copy() 1165 | S1_inv.inv() 1166 | S2_inv = self[end].copy() 1167 | S2_inv.inv() 1168 | if nsites == 1: 1169 | t.replace_label([gate_outputs[0]], [self.phys_label]) 1170 | t = S1_inv[self.right_label,] * t[self.left_label,] 1171 | self[start + 1] = t[self.right_label,] * S2_inv[self.left_label,] 1172 | # if chi is not None: 1173 | # self.compress_bond(firstsite) 1174 | # self.compress_bond(firstsite+1) 1175 | elif nsites == 2: 1176 | U, S, V = tsr.truncated_svd(t, [gate_outputs[0], self.left_label], 1177 | chi=chi, threshold=threshold, absorb_singular_values=None) 1178 | U.replace_label(["svd_in", gate_outputs[0]], 1179 | [self.right_label, self.phys_label]) 1180 | V.replace_label(["svd_out", gate_outputs[1]], 1181 | [self.left_label, self.phys_label]) 1182 | S.replace_label(["svd_out", "svd_in"], [self.left_label, 1183 | self.right_label]) 1184 | self[start + 1] = S1_inv[self.right_label,] * U[self.left_label,] 1185 | self[start + 2] = S 1186 | self[start + 3] = V[self.right_label,] * S2_inv[self.left_label,] 1187 | 1188 | def swap_gate(self, i, chi=None, threshold=1e-15): 1189 | """ 1190 | Apply a swap gate swapping all "physical" (i.e., non-"left" and 1191 | non-"right") indices for site `physical_site(i)` and 1192 | `physical_site(i+1)` of a MatrixProductStateCanonical object. 1193 | 1194 | Parameters 1195 | ---------- 1196 | i : int 1197 | threshold : float 1198 | Lower bound on the magnitude of singular values to keep. Singular 1199 | values less than or equal to this value will be truncated. 1200 | chi : int 1201 | Maximum number of singular values of each tensor to keep after 1202 | performing singular-value decomposition. 1203 | 1204 | Notes 1205 | ----- 1206 | The swap is implemented by SVD as described 1207 | in Y.-Y. Shi et al, Phys. Rev. A 74, 022320 (2006). 1208 | """ 1209 | # contract the MPS sites first 1210 | start = self.physical_site(i) - 1 1211 | end = self.physical_site(i + 1) + 1 1212 | self[start + 1].prime_label(self.phys_label) 1213 | t = contract_virtual_indices(self, start, end + 1, 1214 | periodic_boundaries=False) 1215 | 1216 | # remember inverse singular values 1217 | S1_inv = self[start].copy() 1218 | S1_inv.inv() 1219 | S2_inv = self[end].copy() 1220 | S2_inv.inv() 1221 | 1222 | U, S, V = tsr.truncated_svd(t, [self.left_label, self.phys_label], 1223 | chi=chi, threshold=threshold, absorb_singular_values=None) 1224 | V.unprime_label(self.phys_label) 1225 | 1226 | U.replace_label("svd_in", self.right_label) 1227 | V.replace_label('svd_out', self.left_label) 1228 | S.replace_label(["svd_out", "svd_in"], [self.left_label, 1229 | self.right_label]) 1230 | self[start + 1] = S1_inv[self.right_label,] * U[self.left_label,] 1231 | self[start + 2] = S 1232 | self[start + 3] = V[self.right_label,] * S2_inv[self.left_label,] 1233 | 1234 | def expval(self, gate, firstsite, gate_outputs=None, gate_inputs=None): 1235 | """ 1236 | Compute multi-site expectation value for operator `gate` applied to 1237 | `physical_site(firstsite)`, `physical_site(firstsite+1)`, ..., 1238 | assuming canonical form. 1239 | 1240 | Parameters 1241 | ---------- 1242 | gate : Tensor 1243 | Tensor representing the multisite gate. 1244 | firstsite : int 1245 | First site of MPS involved in the gate 1246 | gate_outputs : list of str, optional 1247 | Output labels corresponding to the input labels given by 1248 | `gate_inputs`. Must have the same length as `gate_inputs`. 1249 | If `None` the first half of `gate.labels` will be taken as output 1250 | labels. 1251 | gate_inputs : list of str, optional 1252 | Input labels. The first index of the list is contracted with 1253 | `firstsite`, the second with `firstsite`+1 etc. 1254 | If `None` the second half of `gate.labels` will be taken as input 1255 | labels. 1256 | 1257 | Returns 1258 | ------ 1259 | t : Tensor 1260 | Contracted tensor 1261 | """ 1262 | # Set gate_outputs and gate_inputs to default values if not given 1263 | if gate_outputs is None and gate_inputs is None: 1264 | gate_outputs = gate.labels[:int(len(gate.labels) / 2)] 1265 | gate_inputs = gate.labels[int(len(gate.labels) / 2):] 1266 | elif gate_outputs is None: 1267 | gate_outputs = [x for x in gate.labels if x not in gate_inputs] 1268 | elif gate_inputs is None: 1269 | gate_inputs = [x for x in gate.labels if x not in gate_outputs] 1270 | 1271 | nsites = len(gate_inputs) 1272 | if len(gate_outputs) != nsites: 1273 | raise ValueError("len(gate_outputs) != len(gate_inputs)") 1274 | 1275 | # contract the MPS sites first 1276 | start = self.physical_site(firstsite) - 1 1277 | end = self.physical_site(firstsite + len(gate_inputs) - 1) + 1 1278 | t = contract_virtual_indices(self, start, end + 1, 1279 | periodic_boundaries=False) 1280 | td = t.copy() 1281 | td.conjugate() 1282 | 1283 | # contract all physical indices with gate indices 1284 | exp = t[self.phys_label,] * gate[gate_inputs] 1285 | exp = td[self.phys_label,] * exp[gate_outputs] 1286 | # contract boundary indices 1287 | exp.tr(self.left_label, self.left_label, index1=0, index2=1) 1288 | exp.tr(self.right_label, self.right_label, index1=0, index2=1) 1289 | 1290 | return exp 1291 | 1292 | def ptrace(self, firstsite, lastsite=None): 1293 | """ 1294 | Compute local density matrix for sites 1295 | `physical_site(firstsite)` to `physical_site(lastsite)` 1296 | assuming canonical form. 1297 | 1298 | Parameters 1299 | ---------- 1300 | firstsite : int 1301 | First physical site of MPS not traced out 1302 | lastsite : int 1303 | Last physical site of MPS not traced out. By default `lastsite` is 1304 | set to `firstsite`. 1305 | 1306 | Returns 1307 | ------ 1308 | t : Tensor 1309 | Local density matrix 1310 | """ 1311 | if lastsite is None: 1312 | lastsite = firstsite 1313 | start = self.physical_site(firstsite) - 1 1314 | end = self.physical_site(lastsite) + 1 1315 | t = contract_virtual_indices(self, start, end + 1, 1316 | periodic_boundaries=False) 1317 | td = t.copy() 1318 | td.conjugate() 1319 | # rename physical labels 1320 | for i, l in enumerate(t.labels): 1321 | if l == self.phys_label: 1322 | t.labels[i] = l + "_out" + str(i) 1323 | td.labels[i] = l + "_in" + str(i) 1324 | rho = (t[self.left_label, self.right_label] 1325 | * td[self.left_label, self.right_label]) 1326 | return rho 1327 | 1328 | 1329 | class MatrixProductOperator(OneDimensionalTensorNetwork): 1330 | # TODO currently assumes open boundaries 1331 | """Matrix product operator "is a list of tensors, each having and index 1332 | labelled "phys" and at least one of the indices "left", "right" 1333 | Input is a list of tensors, with three up to three index labels, If the 1334 | labels aren't already specified as "left", "right", "physin", "physout" 1335 | need to specify which labels correspond to these using 1336 | arguments left_label, right_label, physin_label and physout_label. """ 1337 | 1338 | def __init__(self, tensors, left_label="left", right_label="right", 1339 | physout_label="physout", physin_label="physin"): 1340 | OneDimensionalTensorNetwork.__init__(self, tensors, left_label, 1341 | right_label) 1342 | self.physout_label = physout_label 1343 | self.physin_label = physin_label 1344 | 1345 | def __repr__(self): 1346 | return ("MatrixProductOperator(tensors=%r, left_label=%r," 1347 | " right_label=%r, physout_label=%r, phsin_labe=%r)" 1348 | % (self.data, self.left_label, self.right_label, 1349 | self.physout_label, self.physin_label)) 1350 | 1351 | def __str__(self): 1352 | return ("MatrixProductOperator object: " + 1353 | "sites = " + str(len(self)) + 1354 | ", left_label = " + self.left_label + 1355 | ", right_label = " + self.right_label + 1356 | ", physout_label = " + self.physout_label + 1357 | ", physin_label = " + self.physin_label) 1358 | 1359 | ###TODO replace copy method 1360 | 1361 | def physoutdim(self, site): 1362 | """Return output physical index dimesion for site""" 1363 | return self.data[site].index_dimension(self.physout_label) 1364 | 1365 | def physindim(self, site): 1366 | """Return input physical index dimesion for site""" 1367 | return self.data[site].index_dimension(self.physin_label) 1368 | 1369 | 1370 | def contract_multi_index_tensor_with_one_dim_array(tensor, array, label1, 1371 | label2): 1372 | """Will contract a one dimensional tensor array of length N 1373 | with a single tensor with N indices with label1. 1374 | All virtual indices are also contracted. 1375 | Each tensor in array is assumed to have an index with label2. 1376 | Starting from the left, the label2 index of each tensor 1377 | is contracted with the first uncontracted label1 index of tensor 1378 | until every tensor in array is incorporated. 1379 | It is assumed that only the indices to be contracted have the labels label1 1380 | label2.""" 1381 | 1382 | # To avoid possible label conflicts, rename labels temporarily 1383 | temp_label = unique_label() 1384 | tensor.replace_label(label1, temp_label) 1385 | 1386 | C = tsr.contract(tensor, array[0], temp_label, label2, index_slice1=[0]) 1387 | for i in range(1, len(array)): 1388 | # TODO make this work 1389 | C = tsr.contract(C, array[i], [array.right_label, temp_label], 1390 | [array.left_label, label2], index_slice1=[0, 1]) 1391 | 1392 | # Contract boundaries of array 1393 | C.contract_internal(array.right_label, array.left_label) 1394 | # Restore original labelling to tensor 1395 | tensor.replace_label(temp_label, label1) 1396 | return C 1397 | 1398 | 1399 | def contract_virtual_indices(array_1d, start=0, end=None, 1400 | periodic_boundaries=True): 1401 | """ 1402 | Return a Tensor by contracting all virtual indices of a segment of a 1403 | OneDimensionalTensorNetwork. 1404 | 1405 | Params 1406 | ----- 1407 | array_1d : OneDimensionalTensorNetwork 1408 | start : int 1409 | First site of segment to be contracted 1410 | end : int 1411 | Last site of segment to be contracted 1412 | periodic_boundaries : bool 1413 | If `True` leftmost and rightmost virtual indices are contracted. 1414 | """ 1415 | C = array_1d[start].copy() 1416 | for x in array_1d[start + 1:end]: 1417 | C = tsr.contract(C, x, array_1d.right_label, array_1d.left_label) 1418 | if periodic_boundaries: 1419 | # Contract left and right boundary indices (periodic boundaries) 1420 | # Note that this will simply remove boundary indices of dimension one. 1421 | C.contract_internal(array_1d.right_label, array_1d.left_label) 1422 | return C 1423 | 1424 | 1425 | def left_canonical_form_mps(orig_mps, chi=0, threshold=1e-14, 1426 | normalise=False): 1427 | """ 1428 | Computes left canonical form of an MPS 1429 | 1430 | See also 1431 | -------- 1432 | Tensor.left_canonise() 1433 | """ 1434 | mps = orig_mps.copy() 1435 | mps.left_canonise(chi=chi, threshold=threshold, normalise=normalise) 1436 | return mps 1437 | 1438 | 1439 | def right_canonical_form_mps(orig_mps, chi=0, threshold=1e-14, 1440 | normalise=False): 1441 | """Computes left canonical form of an MPS""" 1442 | 1443 | mps = orig_mps.copy() 1444 | mps.right_canonise(chi=chi, threshold=threshold, normalise=normalise) 1445 | return mps 1446 | 1447 | 1448 | def canonical_form_mps(orig_mps, chi=0, threshold=1e-14, 1449 | normalise=False): 1450 | """Computes canonical form of an MPS""" 1451 | 1452 | mps = orig_mps.copy() 1453 | mps.right_canonise(chi=chi, threshold=threshold, normalise=normalise) 1454 | return right_canonical_to_canonical(mps, threshold=threshold) 1455 | 1456 | 1457 | def reverse_mps(orig_mps): 1458 | mps = orig_mps.copy() 1459 | mps.reverse() 1460 | return mps 1461 | 1462 | 1463 | def check_canonical_form_mps(mps, threshold=1e-14, print_output=True): 1464 | return mps.check_canonical_form(threshold=threshold, 1465 | print_output=print_output) 1466 | 1467 | 1468 | def svd_compress_mps(orig_mps, chi, threshold=1e-15, normalise=False): 1469 | """Simply right canonise the left canonical form according to Schollwock""" 1470 | mps = left_canonical_form_mps(orig_mps, threshold=threshold, 1471 | normalise=normalise) 1472 | return right_canonical_form_mps(mps, chi=chi, threshold=threshold, 1473 | normalise=normalise) 1474 | 1475 | 1476 | def variational_compress_mps(mps, chi, max_iter=10, initial_guess=None, 1477 | tolerance=1e-15): 1478 | return mps.variational_compress(chi, max_iter=max_iter, 1479 | initial_guess=initial_guess, tolerance=tolerance) 1480 | 1481 | 1482 | def mps_complex_conjugate(mps): 1483 | """Will take complex conjugate of every entry of every tensor in mps, 1484 | and append label_suffix to every label""" 1485 | new_mps = mps.copy() 1486 | for x in new_mps.data: 1487 | x.conjugate() 1488 | return new_mps 1489 | 1490 | 1491 | def ladder_contract(array1, array2, label1, label2, start=0, end=None, 1492 | complex_conjugate_array1=False, left_output_label="left", 1493 | right_output_label="right", return_intermediate_contractions=False): 1494 | """ 1495 | Contract two one-dimensional tensor networks. Indices labelled `label1` in 1496 | `array1` and indices labelled `label2` in `array2` are contracted pairwise 1497 | and all virtual indices are contracted. The contraction pattern 1498 | resembles a ladder when represented graphically. 1499 | 1500 | Parameters 1501 | ---------- 1502 | 1503 | array1 : OneDimensionalTensorNetwork 1504 | array2 : OneDimensionalTensorNetwork 1505 | The one-dimensional networks to be contracted. 1506 | 1507 | label1 : str 1508 | label2 : str 1509 | The index labelled `label1` is contracted with the index labelled 1510 | `label2` for every site in array. 1511 | 1512 | start : int 1513 | end : int 1514 | The endpoints of the interval to be contracted. The leftmost tensors 1515 | involved in the contraction are `array1[start]` and `array2[start]`, 1516 | while the rightmost tensors are `array2[end]` and `array2[end]`. 1517 | 1518 | complex_conjugate_array1 : bool 1519 | Whether the complex conjugate of `array1` will be used, rather than 1520 | `array1` itself. This is useful if, for instance, the two arrays are 1521 | matrix product states and the inner product is to be taken (Note that 1522 | inner_product_mps could be used in this case). 1523 | 1524 | right_output_label : str 1525 | Base label assigned to right-going indices of output tensor. 1526 | Right-going indices will be assigned labels `right_output_label`+"1" 1527 | and `right_output_label`+"2" corresponding, respectively, to `array1` 1528 | and `array2`. 1529 | 1530 | left_output_label : str 1531 | Base label assigned to left-going indices of output tensor. Left-going 1532 | indices will be assigned labels `left_output_label`+"1" and 1533 | `left_output_label`+"2" corresponding, respectively, to `array1` and 1534 | `array2`. 1535 | 1536 | return_intermediate_contractions : bool 1537 | If true, a list of tensors is returned. If the contraction is performed 1538 | from left to right (see Notes below), the i-th entry contains the 1539 | contraction up to the i-th contracted pair. If contraction is performed 1540 | from right to left, this order is reversed (so the last entry 1541 | corresponds to the contraction of the right-most pair tensors, which 1542 | are first to be contracted). 1543 | 1544 | Returns 1545 | ------- 1546 | tensor : Tensor 1547 | Tensor obtained by contracting the two arrays. The tensor may have left 1548 | indices, right indices, both or neither depending on the interval 1549 | specified. 1550 | 1551 | intermediate_contractions : list 1552 | If `return_intermediate_contractions` is true a list 1553 | `intermediate_contractions` is returned containing a list of tensors 1554 | corresponding to contraction up to a particular column. 1555 | 1556 | Notes 1557 | ----- 1558 | If the interval specified contains the left open boundary, contraction is 1559 | performed from left to right. If not and if interval contains right 1560 | boundary, contraction is performed from right to left. If the interval 1561 | does not contain either boundary, contraction is performed from left to 1562 | right. 1563 | """ 1564 | 1565 | # If no end specified, will contract to end 1566 | if end == None: 1567 | end = min(array1.nsites, array2.nsites) - 1 # index of the last site 1568 | 1569 | if end < start: 1570 | raise ValueError("Badly defined interval (end before start).") 1571 | 1572 | a1 = array1.copy() 1573 | a2 = array2.copy() 1574 | 1575 | if complex_conjugate_array1: 1576 | a1.complex_conjugate() 1577 | 1578 | # Give all contracted indices unique labels so no conflicts with other 1579 | # labels in array1, array2 1580 | a1.unique_virtual_labels() 1581 | a2.unique_virtual_labels() 1582 | rung_label = unique_label() 1583 | a1.replace_labels(label1, rung_label) 1584 | a2.replace_labels(label2, rung_label) 1585 | 1586 | intermediate_contractions = [] 1587 | if start == 0: # Start contraction from left 1588 | for i in range(0, end + 1): 1589 | if i == 0: 1590 | C = tsr.contract(a1[0], a2[0], rung_label, rung_label) 1591 | else: 1592 | C.contract(a1[i], a1.right_label, a1.left_label) 1593 | C.contract(a2[i], [a2.right_label, rung_label], 1594 | [a2.left_label, rung_label]) 1595 | 1596 | if return_intermediate_contractions: 1597 | t = C.copy() 1598 | t.replace_label([a1.right_label, a2.right_label], 1599 | [right_output_label + "1", right_output_label + "2"]) 1600 | # Remove dummy indices except the right indices 1601 | t.remove_all_dummy_indices(labels=[x for x in t.labels if x 1602 | not in [right_output_label + "1", right_output_label + "2"]]) 1603 | intermediate_contractions.append(t) 1604 | 1605 | C.replace_label([a1.right_label, a2.right_label], 1606 | [right_output_label + "1", right_output_label + "2"]) 1607 | C.remove_all_dummy_indices() 1608 | 1609 | elif end == a1.nsites - 1 and end == a2.nsites - 1: # Contract from the right 1610 | for i in range(end, start - 1, -1): 1611 | if i == end: 1612 | C = tsr.contract(a1[end], a2[end], rung_label, rung_label) 1613 | else: 1614 | C.contract(a1[i], a1.left_label, a1.right_label) 1615 | C.contract(a2[i], [a2.left_label, rung_label], 1616 | [a2.right_label, rung_label]) 1617 | 1618 | if return_intermediate_contractions: 1619 | t = C.copy() 1620 | t.replace_label([a1.left_label, a2.left_label], 1621 | [left_output_label + "1", left_output_label + "2"]) 1622 | # Remove dummy indices except the left indices 1623 | t.remove_all_dummy_indices(labels=[x for x in t.labels if x 1624 | not in [left_output_label + "1", left_output_label + "2"]]) 1625 | intermediate_contractions.insert(0, t) 1626 | 1627 | C.replace_label([a1.left_label, a2.left_label], 1628 | [left_output_label + "1", left_output_label + "2"]) 1629 | C.remove_all_dummy_indices() 1630 | 1631 | else: 1632 | # When an interval does not contain a boundary, contract in pairs first 1633 | # then together 1634 | for i in range(start, end + 1): 1635 | t = tsr.contract(a1[i], a2[i], rung_label, rung_label) 1636 | if i == start: 1637 | C = t 1638 | else: 1639 | C.contract(t, [a1.right_label, a2.right_label], 1640 | [a1.left_label, a2.left_label]) 1641 | 1642 | if return_intermediate_contractions: 1643 | t = C.copy() 1644 | t.replace_label([a1.right_label, a2.right_label, a1.left_label, 1645 | a2.left_label], [right_output_label + "1", 1646 | right_output_label + "2", left_output_label + "1", 1647 | left_output_label + "2"]) 1648 | # Remove dummy indices except the left and right indices 1649 | t.remove_all_dummy_indices(labels=[x for x in t.labels if x 1650 | not in [right_output_label + "1", right_output_label + "2", 1651 | left_output_label + "1", left_output_label + "2"]]) 1652 | t.remove_all_dummy_indices() 1653 | intermediate_contractions.append(t) 1654 | 1655 | C.replace_label([a1.right_label, a2.right_label, a1.left_label, 1656 | a2.left_label], [right_output_label + "1", right_output_label + "2", 1657 | left_output_label + "1", left_output_label + "2"]) 1658 | C.remove_all_dummy_indices() 1659 | 1660 | if return_intermediate_contractions: 1661 | return intermediate_contractions 1662 | else: 1663 | return C 1664 | 1665 | 1666 | def inner_product_mps(mps_bra, mps_ket, complex_conjugate_bra=True, 1667 | return_whole_tensor=False): 1668 | """Compute the inner product of two MatrixProductState objects.""" 1669 | # If MPS are in canonical form, convert left-canonical first 1670 | if isinstance(mps_bra, MatrixProductStateCanonical): 1671 | mps_bra_tmp = canonical_to_left_canonical(mps_bra) 1672 | else: 1673 | mps_bra_tmp = mps_bra 1674 | if isinstance(mps_ket, MatrixProductStateCanonical): 1675 | mps_ket_tmp = canonical_to_left_canonical(mps_ket) 1676 | else: 1677 | mps_ket_tmp = mps_ket 1678 | t = ladder_contract(mps_bra_tmp, mps_ket_tmp, mps_bra.phys_label, 1679 | mps_ket.phys_label, complex_conjugate_array1=complex_conjugate_bra) 1680 | if return_whole_tensor: 1681 | return t 1682 | else: 1683 | return t.data 1684 | 1685 | 1686 | def frob_distance_squared(mps1, mps2): 1687 | ip = inner_product_mps 1688 | return ip(mps1, mps1) + ip(mps2, mps2) - 2 * np.real(ip(mps1, mps2)) 1689 | 1690 | 1691 | def contract_mps_mpo(mps, mpo): 1692 | """Will contract the physical index of mps with the physin index of mpo. 1693 | Left and right indices will be combined. The resulting MPS will have the 1694 | same left and right labels as mps and the physical label will be 1695 | mpo.physout_label""" 1696 | if isinstance(mps, MatrixProductStateCanonical): 1697 | raise NotImplementedError(("Function not implemented for" 1698 | + "MatrixProductStateCanonical")) 1699 | N = len(mps) 1700 | new_mps = [] 1701 | for i in range(N): 1702 | new_tensor = tsr.contract(mps[i], mpo[i], mps.phys_label, 1703 | mpo.physin_label) 1704 | new_tensor.consolidate_indices() 1705 | new_mps.append(new_tensor) 1706 | new_mps = MatrixProductState(new_mps, mps.left_label, mps.right_label, 1707 | mpo.physout_label) 1708 | return new_mps 1709 | 1710 | 1711 | def tensor_to_mps(tensor, phys_labels=None, mps_phys_label='phys', 1712 | left_label='left', right_label='right', chi=0, threshold=1e-15): 1713 | """ 1714 | Split a tensor into MPS form by exact SVD 1715 | 1716 | Parameters 1717 | ---------- 1718 | tensor : Tensor 1719 | phys_labels list of str, optional 1720 | Can be used to specify the order of the physical indices for the MPS. 1721 | mps_phys_label : str 1722 | Physical labels of the resulting MPS will be renamed to this value. 1723 | left_label : str 1724 | Label for index of `tensor` that will be regarded as the leftmost index 1725 | of the resulting MPS if it exists (must be unique). 1726 | Also used as `left_label` for the resulting MPS. 1727 | right_label : str 1728 | Label for index of `tensor` that will be regarded as the rightmost 1729 | index of the resulting MPS if it exists (must be unique). 1730 | Also used as `right_label` for the resulting MPS. 1731 | chi : int, optional 1732 | Maximum number of singular values of each tensor to keep after 1733 | performing singular-value decomposition. 1734 | threshold : float 1735 | Lower bound on the magnitude of singular values to keep. Singular 1736 | values less than or equal to this value will be truncated. 1737 | 1738 | Notes 1739 | ----- 1740 | The resulting MPS is left-canonised. 1741 | """ 1742 | if phys_labels is None: 1743 | phys_labels = [x for x in tensor.labels if x not in 1744 | [left_label, right_label]] 1745 | 1746 | nsites = len(phys_labels) 1747 | V = tensor.copy() 1748 | mps = [] 1749 | for k in range(nsites - 1): 1750 | U, V, _ = tsr.truncated_svd(V, [left_label] * (left_label in V.labels) 1751 | + [phys_labels[k]], chi=chi, threshold=threshold, 1752 | absorb_singular_values='right') 1753 | U.replace_label('svd_in', right_label) 1754 | U.replace_label(phys_labels[k], mps_phys_label) 1755 | mps.append(U) 1756 | # t = tsr.contract(S, V, ['svd_in'], ['svd_out']) 1757 | V.replace_label('svd_out', left_label) 1758 | V.replace_label(phys_labels[nsites - 1], mps_phys_label) 1759 | mps.append(V) 1760 | return MatrixProductState(mps, phys_label=mps_phys_label, 1761 | left_label=left_label, right_label=right_label) 1762 | 1763 | 1764 | def tensor_to_mpo(tensor, physout_labels=None, physin_labels=None, 1765 | mpo_physout_label='physout', mpo_physin_label='physin', 1766 | left_label='left', right_label='right', chi=0, threshold=1e-15): 1767 | """ 1768 | Split a tensor into MPO form by exact SVD 1769 | 1770 | Parameters 1771 | ---------- 1772 | tensor : Tensor 1773 | physout_labels : list of str, optional 1774 | The output physical indices for the MPO. First site of MPO has output 1775 | index corresponding to physout_labels[0] etc. 1776 | If `None` the first half of `tensor.labels` will be taken as output 1777 | labels. 1778 | physin_labels : list of str, optional 1779 | The input physical indices for the MPO. First site of MPO has input 1780 | index corresponding to physin_labels[0] etc. 1781 | If `None` the second half of `tensor.labels` will be taken as input 1782 | labels. 1783 | mpo_phys_label : str 1784 | Physical input labels of the resulting MPO will be renamed to this. 1785 | mpo_phys_label : str 1786 | Physical output labels of the resulting MPO will be renamed to this. 1787 | left_label : str 1788 | Label for index of `tensor` that will be regarded as the leftmost index 1789 | of the resulting MPO if it exists (must be unique). 1790 | Also used as `left_label` for the resulting MPO. 1791 | right_label : str 1792 | Label for index of `tensor` that will be regarded as the rightmost 1793 | index of the resulting MPO if it exists (must be unique). 1794 | Also used as `right_label` for the resulting MPO. 1795 | chi : int, optional 1796 | Maximum number of singular values of each tensor to keep after 1797 | performing singular-value decomposition. 1798 | threshold : float 1799 | Lower bound on the magnitude of singular values to keep. Singular 1800 | values less than or equal to this value will be truncated. 1801 | """ 1802 | # Set physout_labels and physin_labels to default values if not given 1803 | phys_labels = [x for x in tensor.labels if x not in 1804 | [left_label, right_label]] 1805 | if physout_labels is None and physin_labels is None: 1806 | physout_labels = phys_labels[:int(len(phys_labels) / 2)] 1807 | physin_labels = phys_labels[int(len(phys_labels) / 2):] 1808 | elif physout_labels is None: 1809 | physout_labels = [x for x in phys_labels if x not in physin_labels] 1810 | elif physin_labels is None: 1811 | physin_labels = [x for x in phys_labels if x not in physout_labels] 1812 | 1813 | nsites = len(physin_labels) 1814 | if len(physout_labels) != nsites: 1815 | raise ValueError("len(physout_labels) != len(physin_labels)") 1816 | 1817 | V = tensor.copy() 1818 | mpo = [] 1819 | for k in range(nsites - 1): 1820 | U, V, _ = tsr.truncated_svd(V, [left_label] * (left_label in V.labels) 1821 | + [physout_labels[k], physin_labels[k]], 1822 | chi=chi, threshold=threshold) 1823 | U.replace_label('svd_in', right_label) 1824 | U.replace_label(physout_labels[k], mpo_physout_label) 1825 | U.replace_label(physin_labels[k], mpo_physin_label) 1826 | mpo.append(U) 1827 | V.replace_label('svd_out', left_label) 1828 | V.replace_label(physout_labels[nsites - 1], mpo_physout_label) 1829 | V.replace_label(physin_labels[nsites - 1], mpo_physin_label) 1830 | mpo.append(V) 1831 | return MatrixProductOperator(mpo, physout_label=mpo_physout_label, 1832 | physin_label=mpo_physin_label, left_label=left_label, 1833 | right_label=right_label) 1834 | 1835 | 1836 | def right_canonical_to_canonical(mps, threshold=1e-14): 1837 | """ 1838 | Turn an MPS in right canonical form into an MPS in canonical form 1839 | """ 1840 | N = mps.nsites 1841 | 1842 | S_prev = tsr.Tensor([[1.0]], labels=[mps.left_label, mps.right_label]) 1843 | S_prev_inv = S_prev.copy() 1844 | B = mps[0] 1845 | tensors = [] 1846 | svd_label = unique_label() 1847 | for i in range(N): 1848 | U, S, V = tsr.tensor_svd(B, [mps.phys_label, mps.left_label], 1849 | svd_label=svd_label) 1850 | # Truncate to threshold 1851 | singular_values = np.diag(S.data) 1852 | singular_values_to_keep = singular_values[singular_values > 1853 | threshold] 1854 | S.data = np.diag(singular_values_to_keep) 1855 | # Truncate corresponding singular index of U and V 1856 | U.data = U.data[:, :, 0:len(singular_values_to_keep)] 1857 | V.data = V.data[0:len(singular_values_to_keep)] 1858 | 1859 | U.replace_label(svd_label + "in", mps.right_label) 1860 | V.replace_label(svd_label + "out", mps.left_label) 1861 | S.replace_label([svd_label + "out", svd_label + "in"], 1862 | [mps.left_label, mps.right_label]) 1863 | 1864 | G = S_prev_inv[mps.right_label,] * U[mps.left_label,] 1865 | tensors.append(S_prev) 1866 | tensors.append(G) 1867 | 1868 | if i == N - 1: 1869 | # The final SVD has no right index, so S and V are just scalars. 1870 | # S is the norm of the state. 1871 | tensors.append(S) 1872 | else: 1873 | V = S[mps.right_label,] * V[mps.left_label,] 1874 | B = V[mps.right_label,] * mps[i + 1][mps.left_label,] 1875 | # Store S and S^{-1} for next iteration 1876 | S_prev = S.copy() 1877 | S_prev_inv = S_prev.copy() 1878 | S_prev_inv.inv() 1879 | # S_prev_inv.data = np.diag(1./singular_values_to_keep) 1880 | 1881 | # Construct MPS in canonical form 1882 | return MatrixProductStateCanonical(tensors, 1883 | left_label=mps.left_label, right_label=mps.right_label, 1884 | phys_label=mps.phys_label) 1885 | 1886 | 1887 | def left_canonical_to_canonical(mps, threshold=1e-14): 1888 | """ 1889 | Turn an MPS in left canonical form into an MPS in canonical form 1890 | """ 1891 | mpsr = reverse_mps(mps) 1892 | mpsc = right_canonical_to_canonical(mpsr, threshold=threshold) 1893 | mpsc.reverse() 1894 | return mpsc 1895 | 1896 | 1897 | def canonical_to_right_canonical(mps): 1898 | """ 1899 | Turn an MPS in canonical form into an MPS in right canonical form 1900 | """ 1901 | N = mps.nsites_physical 1902 | tensors = [] 1903 | for i in range(N - 1): 1904 | tensors.append(mps[mps.physical_site(i)][mps.right_label,] 1905 | * mps[mps.physical_site(i) + 1][mps.left_label,]) 1906 | tensors.append(mps[mps.physical_site(N - 1)]) 1907 | tensors[0].data = (tensors[0].data * np.linalg.norm(mps[-1].data) 1908 | * np.linalg.norm(mps[0].data)) 1909 | return MatrixProductState(tensors, 1910 | left_label=mps.left_label, right_label=mps.right_label, 1911 | phys_label=mps.phys_label) 1912 | 1913 | 1914 | def canonical_to_left_canonical(mps): 1915 | """ 1916 | Turn an MPS in canonical form into an MPS in left canonical form 1917 | """ 1918 | N = mps.nsites_physical 1919 | tensors = [] 1920 | tensors.append(mps[mps.physical_site(0)]) 1921 | for i in range(1, N): 1922 | tensors.append(mps[mps.physical_site(i) - 1][mps.right_label,] 1923 | * mps[mps.physical_site(i)][mps.left_label,]) 1924 | tensors[-1].data = (tensors[-1].data * np.linalg.norm(mps[-1].data) 1925 | * np.linalg.norm(mps[0].data)) 1926 | return MatrixProductState(tensors, 1927 | left_label=mps.left_label, right_label=mps.right_label, 1928 | phys_label=mps.phys_label) 1929 | -------------------------------------------------------------------------------- /tncontract/onedim/onedim_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | """ 5 | onedim_utils 6 | ========== 7 | 8 | Module with various functions for MPS/MPOs. 9 | """ 10 | 11 | __all__ = ['init_mps_random', 'init_mps_allzero', 'init_mps_logical', 12 | 'onebody_sum_mpo', 'expvals_mps', 'ptrace_mps'] 13 | 14 | import numpy as np 15 | 16 | from tncontract import tensor as tnc 17 | from tncontract.onedim import onedim_core as onedim 18 | 19 | 20 | # from tncontract import onedim as onedim 21 | 22 | 23 | def init_mps_random(nsites, physdim, bonddim=1, left_label='left', 24 | right_label='right', phys_label='phys'): 25 | """ 26 | Create an MPS with `nsites` sites and random tensors with physical 27 | dimensions given by `physdim` and bond dimensions given by 28 | `bonddim`. Open boundary conditions are used. The MPS is not normalized. 29 | 30 | Parameters 31 | ---------- 32 | nsites : int 33 | physdim : int or list of ints 34 | bonddim : int or list of ints, optional 35 | The nth element of `bonddim` determines the right and left index of 36 | the tensors at sites n and n+1, respectively. The length of `bonddim` 37 | should be `nsites`-1. If `bonddim` is an int this is this is used for 38 | all bonds. 39 | left_label : str 40 | right_label : str 41 | phys_label : str 42 | """ 43 | if not np.iterable(physdim): 44 | physdim = [physdim] * nsites 45 | if not np.iterable(bonddim): 46 | bonddim = [bonddim] * (nsites - 1) 47 | bonddim = [1] + bonddim + [1] 48 | tensors = [] 49 | for i in range(nsites): 50 | rt = tnc.Tensor(np.random.rand( 51 | physdim[i], bonddim[i], bonddim[i + 1]), 52 | [phys_label, left_label, right_label]) 53 | # Normalize matrix to avoid norm blowing up 54 | U, S, V = tnc.tensor_svd(rt, [phys_label, left_label]) 55 | S.data = S.data / S.data[0, 0] 56 | rt = U["svd_in",] * S["svd_out",] 57 | rt = rt["svd_in",] * V["svd_out",] 58 | tensors.append(rt) 59 | return onedim.MatrixProductState(tensors, left_label=left_label, 60 | right_label=right_label, phys_label=phys_label) 61 | 62 | 63 | def init_mps_allzero(nsites, physdim, left_label='left', 64 | right_label='right', phys_label='phys'): 65 | """ 66 | Create an MPS with `nsites` sites in the "all zero" state |00..0>. 67 | 68 | Parameters 69 | ---------- 70 | nsites : int 71 | physdim : int or list of ints 72 | left_label : str 73 | right_label : str 74 | phys_label : str 75 | """ 76 | if not np.iterable(physdim): 77 | physdim = [physdim] * nsites 78 | 79 | tensors = [] 80 | for j in range(nsites): 81 | t = np.zeros(physdim[j]) 82 | t[0] = 1.0 83 | t = tnc.Tensor(t.reshape(physdim[j], 1, 1), [phys_label, left_label, 84 | right_label]) 85 | tensors.append(t) 86 | 87 | return onedim.MatrixProductState(tensors, left_label=left_label, 88 | right_label=right_label, phys_label=phys_label) 89 | 90 | 91 | def init_mps_logical(nsites, basis_state, physdim, left_label='left', 92 | right_label='right', phys_label='phys'): 93 | """ 94 | Create an MPS with `nsites` sites in the logical basis state |ijk..l>. 95 | 96 | Parameters 97 | ---------- 98 | nsites : int 99 | basis_state : int or list of ints 100 | Site `i` will be in the state |`basis_state[i]`> (or simply 101 | |`basis_state`> if a single int is provided). 102 | physdim : int or list of ints 103 | left_label : str 104 | right_label : str 105 | phys_label : str 106 | """ 107 | if not np.iterable(physdim): 108 | physdim = [physdim] * nsites 109 | 110 | tensors = [] 111 | for j in range(nsites): 112 | t = np.zeros(physdim[j]) 113 | t[basis_state[j]] = 1.0 114 | t = tnc.Tensor(t.reshape(physdim[j], 1, 1), [phys_label, left_label, 115 | right_label]) 116 | tensors.append(t) 117 | 118 | return onedim.MatrixProductState(tensors, left_label=left_label, 119 | right_label=right_label, phys_label=phys_label) 120 | 121 | 122 | def onebody_sum_mpo(terms, output_label=None): 123 | """ 124 | Construct an MPO from a sum of onebody operators, using the recipe from 125 | the Supplemental Material of [1]_ (Eqs. (3) and (4)) 126 | 127 | Parameters 128 | --------- 129 | terms : list 130 | A list containing the terms in the sum. Each term should be 2D 131 | array-like, e.g., a rank-two Tensor or numpy array. 132 | output_label : str, optional 133 | Specify the label corresponding to the output index. Must be the same 134 | for each element of `terms`. If not specified the first index is taken 135 | to be the output index. 136 | 137 | Returns 138 | ------ 139 | MatrixProductOperator 140 | 141 | References 142 | ---------- 143 | .. [1] E. Sanchez-Burillo et al., Phys. Rev. Lett. 113, 263604 (2014) 144 | """ 145 | tensors = [] 146 | for i, term1 in enumerate(terms): 147 | if output_label is not None: 148 | term = term1.copy() 149 | term.move_index(output_label, 0) 150 | else: 151 | term = term1 152 | if i == 0: 153 | B = np.zeros(shape=term.shape + (2,), dtype=complex) 154 | for k in range(term.shape[0]): 155 | for l in range(term.shape[1]): 156 | B[k, l, :] = [term[k, l], k == l] 157 | tensors.append(tnc.Tensor(B, ['physout', 'physin', 'right'])) 158 | elif i == len(terms) - 1: 159 | B = np.zeros(shape=term.shape + (2,), dtype=complex) 160 | for k in range(term.shape[0]): 161 | for l in range(term.shape[1]): 162 | B[k, l, :] = [k == l, term[k, l]] 163 | tensors.append(tnc.Tensor(B, ['physout', 'physin', 'left'])) 164 | else: 165 | B = np.zeros(shape=term.shape + (2, 2), dtype=complex) 166 | for k in range(term.shape[0]): 167 | for l in range(term.shape[1]): 168 | B[k, l, :, :] = [[k == l, 0], [term[k, l], k == l]] 169 | tensors.append(tnc.Tensor(B, ['physout', 'physin', 170 | 'left', 'right'])) 171 | return onedim.MatrixProductOperator(tensors, left_label='left', 172 | right_label='right', physin_label='physin', physout_label='physout') 173 | 174 | 175 | def expvals_mps(mps, oplist=[], sites=None, output_label=None, canonised=None): 176 | # TODO: Why canonised gives strange results? 177 | """ 178 | Return single site expectation values _i for all i 179 | 180 | Parameters 181 | ---------- 182 | mps : MatrixProductState 183 | oplist : list or Tensor 184 | List of rank-two tensors representing the operators at each site. 185 | If a single `Tensor` is given this will be used for all sites. 186 | sites : int or list of ints, optional 187 | Sites for which to compute expectation values. If None all 188 | sites will be returned. 189 | output_label : str, optional 190 | Specify the label corresponding to the output index. Must be the same 191 | for each element of `terms`. If not specified the first index is taken 192 | to be the output index. 193 | canonised : {'left', 'right', None}, optional 194 | Flag to specify theat `mps` is already in left or right canonical form. 195 | 196 | Returns 197 | ------ 198 | array 199 | Complex array of same length as `mps` of expectation values. 200 | 201 | Notes 202 | ----- 203 | After the function call, `mps` will be in left (right) canonical form for 204 | `canonised = 'right'` (`canonised = 'left'`). 205 | """ 206 | if sites is None: 207 | sites = range(len(mps)) 208 | if not np.iterable(sites): 209 | sites = [sites] 210 | 211 | N = len(sites) 212 | expvals = np.zeros(N, dtype=complex) 213 | 214 | if not isinstance(oplist, list): 215 | oplist_new = [oplist] * N 216 | else: 217 | oplist_new = oplist 218 | 219 | if canonised == 'left': 220 | mps.reverse() 221 | oplist_new = oplist_new[::-1] 222 | elif canonised != 'right': 223 | mps.right_canonise() 224 | 225 | center = 0 226 | for i, site in enumerate(sites): 227 | # Mover orthogonality center to site k 228 | mps.left_canonise(center, site) 229 | center = site 230 | 231 | # compute exp value for site k 232 | op = oplist_new[i] 233 | A = mps[site] 234 | if output_label is None: 235 | out_label = op.labels[0] 236 | in_label = op.labels[1] 237 | else: 238 | out_label = output_label 239 | in_label = [x for x in op.labels if x is not out_label][0] 240 | Ad = A.copy() 241 | Ad.conjugate() 242 | exp = tnc.contract(A, op, mps.phys_label, in_label) 243 | exp = tnc.contract(Ad, exp, mps.phys_label, out_label) 244 | exp.contract_internal(mps.left_label, mps.left_label, index1=0, 245 | index2=1) 246 | exp.contract_internal(mps.right_label, mps.right_label, index1=0, 247 | index2=1) 248 | expvals[i] = exp.data 249 | 250 | if canonised == 'left': 251 | mps.reverse() 252 | oplist_new = oplist_new[::-1] 253 | expvals = expvals[::-1] 254 | 255 | return expvals 256 | 257 | 258 | def ptrace_mps(mps, sites=None, canonised=None): 259 | # TODO: Why canonised gives strange results? 260 | """ 261 | Return single site reduced density matrix rho_i for all i in sites. 262 | 263 | Parameters 264 | ---------- 265 | mps : MatrixProductState 266 | sites : int or list of ints, optional 267 | Sites for which to compute the reduced density matrix. If None all 268 | sites will be returned. 269 | canonised : {'left', 'right', None}, optional 270 | Flag to specify theat `mps` is already in left or right canonical form. 271 | 272 | Returns 273 | ------ 274 | list 275 | List of same length as `mps` with rank-two tensors representing the 276 | reduced density matrices. 277 | 278 | Notes 279 | ----- 280 | `mps` will be in left canonical form after the function call. 281 | """ 282 | rho_list = [] 283 | 284 | if canonised == 'left': 285 | mps.reverse() 286 | elif canonised != 'right': 287 | mps.right_canonise() 288 | 289 | if sites is None: 290 | sites = range(len(mps)) 291 | if not np.iterable(sites): 292 | sites = [sites] 293 | 294 | center = 0 295 | for site in sites: 296 | # Mover orthogonality center to site k 297 | mps.left_canonise(center, site) 298 | center = site 299 | 300 | A = mps[center] 301 | Ad = A.copy() 302 | Ad.conjugate() 303 | Ad.prime_label(mps.phys_label) 304 | 305 | rho = tnc.contract(A, Ad, [mps.left_label, mps.right_label], 306 | [mps.left_label, mps.right_label]) 307 | rho_list.append(rho) 308 | 309 | if canonised == 'left': 310 | mps.reverse() 311 | rho_list = rho_list[::-1] 312 | 313 | return rho_list 314 | -------------------------------------------------------------------------------- /tncontract/qutip_conv.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | from builtins import * 4 | 5 | 6 | """ 7 | qutip_conv 8 | ========== 9 | 10 | QuTiP / tncontract conversions. 11 | 12 | Functionality for converting between `qutip.Qobj` and `Tensor`. 13 | 14 | Requires `qutip`. 15 | """ 16 | 17 | import numpy as np 18 | 19 | import qutip as qt 20 | 21 | import tncontract as tn 22 | import tncontract.onedim as onedim 23 | 24 | 25 | def qobj_to_tensor(qobj, labels=None, trim_dummy=True): 26 | """ 27 | Convert a `qutip.Qobj` object to a `Tensor` 28 | 29 | Parameters 30 | ---------- 31 | qobj : Qobj 32 | Qobj to convert. 33 | labels : list, optional 34 | List of labels for the indices. Output labels followed by input labels. 35 | Defaults to `['out1', ..., 'outk', 'in1', ..., 'ink'] 36 | trim_dummy : bool 37 | If true dummy indices of dimension one are trimmed away 38 | 39 | Returns 40 | ------ 41 | Tensor 42 | """ 43 | 44 | data = qobj.data.toarray() 45 | 46 | if not len(np.shape(qobj.dims)) == 2: 47 | # wrong dims (not a ket, bra or operator) 48 | raise ValueError("qobj element not a ket/bra/operator") 49 | 50 | output_dims = qobj.dims[0] 51 | input_dims = qobj.dims[1] 52 | nsys = len(output_dims) 53 | if labels is None: 54 | output_labels = ['out'+str(k) for k in range(nsys)] 55 | input_labels = ['in'+str(k) for k in range(nsys)] 56 | else: 57 | output_labels = labels[:nsys] 58 | input_labels = labels[nsys:] 59 | t = tn.matrix_to_tensor(data, output_dims+input_dims, output_labels+ 60 | input_labels) 61 | if trim_dummy: 62 | t.remove_all_dummy_indices() 63 | return t 64 | 65 | 66 | def tensor_to_qobj(tensor, output_labels, input_labels): 67 | """ 68 | Convert a `Tensor` object to a `qutip.Qobj` 69 | 70 | Parameters 71 | ---------- 72 | tensor : Tensor 73 | Tensor to convert. 74 | output_labels : list 75 | List of labels that will be the output indices for the `Qobj`. 76 | `None` can be used to insert a dummy index of dimension one. 77 | inpul_labels : list 78 | List of labels that will be the input indices for the `Qobj`. 79 | `None` can be used to insert a dummy index of dimension one. 80 | 81 | Returns 82 | ------- 83 | Qobj 84 | 85 | Notes 86 | ----- 87 | The `output_labels` and `input_labels` determines the tensor product 88 | structure of the resulting `Qobj`, inclding the order of the components. 89 | If the indices corresponding to `output_labels` have dimensions 90 | [dim_out1, ..., dim_outk] and the indices corresponding to `input_labels` 91 | have dimensions [dim_in1, ..., dim_inl], the `Qobj.dims` attribute will be 92 | `Qobj.dims = [[dim_out1, ..., dim_outk], [dim_in1, ..., dim_inl]] 93 | 94 | Examples 95 | -------- 96 | Turn a rank-one vector into a ket `Qobj` (note the use of a `None` input 97 | label to get a well defined `Qobj`) 98 | >>> t = Tensor(np.array([1,0]), labels=['idx1']) 99 | >>> q = tensor_to_qobj(t, ['idx1'], [None]) 100 | >>> print(q) 101 | Quantum object: dims = [[2], [1]], shape = [2, 1], type = ket 102 | Qobj data = 103 | [[ 1.] 104 | [ 0.]] 105 | """ 106 | 107 | output_dims = [] 108 | input_dims = [] 109 | t = tensor.copy() 110 | 111 | if not isinstance(output_labels, list): 112 | output_labels=[output_labels] 113 | if not isinstance(input_labels, list): 114 | input_labels=[input_labels] 115 | # order the indices according to output_labels and input_labels 116 | for i, label in enumerate(output_labels+input_labels): 117 | if label is None: 118 | label = 'dummy'+str(i) 119 | t.add_dummy_index(label, i) 120 | t.move_index(label, i) 121 | if i < len(output_labels): 122 | output_dims.append(t.shape[i]) 123 | else: 124 | input_dims.append(t.shape[i]) 125 | 126 | output_labels_new = [l if l is not None else 'dummy'+str(i) 127 | for i,l in enumerate(output_labels)] 128 | 129 | data = tn.tensor_to_matrix(t, output_labels_new) 130 | dims = [output_dims, input_dims] 131 | return qt.Qobj(data, dims=dims) 132 | 133 | 134 | def qobjlist_to_mpo(qobjlist): 135 | """ 136 | Construct an MPO from a list of Qobj operators. 137 | 138 | Many-body operators are put in MPO form by exact SVD, and virtual "left" 139 | and "right" indices with bond dimension one are added between the elements 140 | of the list. 141 | """ 142 | tensors = np.array([]) 143 | for i, qobj in enumerate(qobjlist): 144 | if not len(np.shape(qobj.dims)) == 2: 145 | # wrong dims (not a ket, bra or operator) 146 | raise ValueError("qobj element not a ket/bra/operator") 147 | 148 | t = qobj_to_tensor(qobj, trim_dummy=False) 149 | 150 | # Add left and right indices with bonddim one 151 | t.add_dummy_index('left', -1) 152 | t.add_dummy_index('right', -1) 153 | 154 | # Break up many-body operators by SVDing 155 | tmp_mpo = onedim.tensor_to_mpo(t) 156 | 157 | tensors = np.concatenate((tensors, tmp_mpo.data)) 158 | return onedim.MatrixProductOperator(tensors, left_label='left', 159 | right_label='right', physin_label='physin', physout_label='physout') 160 | 161 | 162 | def qobjlist_to_mps(qobjlist): 163 | """ 164 | Construct an MPS from a list of Qobj kets. 165 | 166 | Many-body states are put in MPS form by exact SVD, and virtual "left" 167 | and "right" indices with bond dimension one are added between the elements 168 | of the list. 169 | """ 170 | mpo = qobjlist_to_mpo(qobjlist) 171 | tensors = mpo.data 172 | for t in tensors: 173 | # Remove dummy input labels 174 | t.remove_all_dummy_indices(labels=[mpo.physin_label]) 175 | # Change physical label to the standard choice 'phys' 176 | t.replace_label(mpo.physout_label, 'phys') 177 | return onedim.MatrixProductState(tensors, left_label='left', 178 | right_label='right', phys_label='phys') 179 | -------------------------------------------------------------------------------- /tncontract/tensor.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | 4 | __all__ = ['Tensor', 'contract', 'distance', 'matrix_to_tensor', 5 | 'tensor_to_matrix', 'random_tensor', 'tensor_product', 'tensor_svd', 6 | 'truncated_svd', 'zeros_tensor'] 7 | 8 | import copy 9 | import warnings 10 | import numpy as np 11 | import scipy as sp 12 | 13 | from tncontract import label as lbl 14 | 15 | 16 | class Tensor(): 17 | """ 18 | A single tensor containing a numpy array and a list of labels. 19 | 20 | A tensor, for our purposes, is a multi-index array of complex numbers. 21 | Tensors can be contracted with other tensors to form new tensors. A basic 22 | contraction requires specification of two indices, either from the same 23 | tensor of from a pair of different tensors. 24 | 25 | The `Tensor` class contains a multi-dimensional ndarray (stored in the 26 | `data` attribute), and list of labels (stored in the `labels` attribute) 27 | where each label in `labels` corresponds to an axis of `data`. Labels are 28 | assumed to be strings. The order of the labels in `labels` should agree 29 | with the order of the axes in `data` such that the first label corresponds 30 | to the first axis and so on, and the length of labels should equal the 31 | number of axes in `data`. Functions and methods that act on Tensor objects 32 | should update `labels` whenever `data` is changed and vice versa, such that 33 | a given label always corresponds to the same axis. For instance, if two 34 | axes are swapped in `data` the corresponding labels should be swapped in 35 | `labels`. The exceptions being when labels are explicitly changed e.g. when 36 | using the `replace_label` method. 37 | 38 | Attributes 39 | ---------- 40 | 41 | data : ndarray 42 | A multi-dimensional array of numbers. 43 | labels : list 44 | A list of strings which label the axes of data. `label[i]` is the label 45 | for the `i`-1th axis of data. 46 | """ 47 | 48 | def __init__(self, data, labels=None, base_label="i"): 49 | labels = [] if labels is None else labels 50 | self.data = np.array(data) 51 | 52 | if len(labels) == 0: 53 | self.assign_labels(base_label=base_label) 54 | else: 55 | self.labels = labels 56 | 57 | def __repr__(self): 58 | return "Tensor(data=%r, labels=%r)" % (self.data, self.labels) 59 | 60 | def __str__(self): 61 | array_str = str(self.data) 62 | lines = array_str.splitlines() 63 | if len(lines) > 20: 64 | lines = lines[:20] + ["...", 65 | "Printed output of large array was truncated.\nString " 66 | "representation of full data array returned by " 67 | "tensor.data.__str__()."] 68 | array_str = "\n".join(lines) 69 | 70 | # Specify how index information is printed 71 | lines = [] 72 | for i, label in enumerate(self.labels): 73 | lines.append(" " + str(i) + ". (dim=" + str(self.shape[i]) + ") " + 74 | str(label) + "\n") 75 | indices_str = "".join(lines) 76 | 77 | return ("Tensor object: \n" + 78 | "Data type: " + str(self.data.dtype) + "\n" 79 | "Number of indices: " + str(len(self.data.shape)) + "\n" 80 | "\nIndex labels:\n" + indices_str + 81 | # "shape = " + str(self.shape) + 82 | # ", labels = " + str(self.labels) + "\n" +( 83 | "\nTensor data = \n" + array_str) 84 | 85 | def __eq__(self, other): 86 | if isinstance(other, Tensor): 87 | return (np.array_equal(self.data, other.data) 88 | and self.labels == other.labels) 89 | else: 90 | return False 91 | 92 | def __neq__(self, other): 93 | return not self.__eq__(other) 94 | 95 | def __mul__(self, other): 96 | """ 97 | Multiplcation with Tensor on left (i.e. `self*other`). 98 | Returns a copy of `self` with `data` attribute multiplied by 99 | `other`. 100 | """ 101 | try: 102 | out = self.copy() 103 | out.data = out.data * other 104 | return out 105 | except TypeError: 106 | raise TypeError("unsupported operand type(s) *: for '" 107 | + self.__class__.__name__ + "' and '" 108 | + other.__class__.__name__ + "'") 109 | 110 | def __rmul__(self, other): 111 | """ 112 | Multiplcation with Tensor on right (i.e. `other*self`). 113 | Returns a copy of `self` with `data` attribute multiplied by 114 | `other`. 115 | """ 116 | try: 117 | out = self.copy() 118 | out.data = other * out.data 119 | return out 120 | except TypeError: 121 | raise TypeError("unsupported operand type(s) *: for '" 122 | + self.__class__.__name__ + "' and '" 123 | + other.__class__.__name__ + "'") 124 | 125 | def __add__(self, other): 126 | """ 127 | Return a new tensor with the same labels as Tensor objects `self` (and `other`) 128 | with a data array equal to the sum of the data arrays of `self` and 129 | `other`. Requires that the labels of `self` and `other` are the same and, 130 | that their corresponding indices have the same dimension. 131 | """ 132 | try: 133 | a=self.copy() 134 | b=other.copy() 135 | a.consolidate_indices() 136 | b.consolidate_indices() 137 | return Tensor(a.data+b.data, labels=a.labels) 138 | except: 139 | raise TypeError("Can only add together tensors with the same"+ 140 | " indices: labels and dimensions of each index must match.") 141 | 142 | def __getitem__(self, *args): 143 | """Used to allow convenient shorthand for defining tensor 144 | contraction.""" 145 | return ToContract(self, *args) 146 | 147 | # Define functions for getting and setting labels 148 | def get_labels(self): 149 | return self._labels 150 | 151 | def set_labels(self, labels): 152 | if len(labels) == len(self.data.shape): 153 | self._labels = list(labels) 154 | else: 155 | raise ValueError("Labels do not match shape of data.") 156 | 157 | labels = property(get_labels, set_labels) 158 | 159 | def assign_labels(self, base_label="i"): 160 | """Assign labels to all of the indices of `Tensor`. The i-th axis will 161 | be assigned the label `base_label`+"i-1".""" 162 | self.labels = [base_label + str(i) for i in range(len(self.data.shape))] 163 | 164 | def replace_label(self, old_labels, new_labels): 165 | """ 166 | Takes two lists old_labels, new_labels as arguments. If a label in 167 | self.labels is in old_labels, it is replaced with the respective label 168 | In new_labels. 169 | """ 170 | 171 | # If either argument is not a list, convert to list with single entry 172 | if not isinstance(old_labels, list): 173 | old_labels = [old_labels] 174 | if not isinstance(new_labels, list): 175 | new_labels = [new_labels] 176 | 177 | for i, label in enumerate(self.labels): 178 | if label in old_labels: 179 | self.labels[i] = new_labels[old_labels.index(label)] 180 | 181 | def prime_label(self, labels=None): 182 | """ 183 | Add a prime (') to all `label` in `labels` 184 | 185 | Parameters 186 | ---------- 187 | labels : str or list of str, optional 188 | Labels to prime. If None all labels of the tensor will be primed. 189 | 190 | See also 191 | ------- 192 | unprime_label 193 | """ 194 | if labels is None: 195 | labels = self.labels 196 | elif not isinstance(labels, list): 197 | labels = [labels] 198 | for i, label in enumerate(self.labels): 199 | for noprime in labels: 200 | if lbl.noprime_label(label) == noprime: 201 | self.labels[i] = lbl.prime_label(self.labels[i]) 202 | 203 | def unprime_label(self, labels=None): 204 | """ 205 | Remove the last prime (') from all `label` in `labels` 206 | 207 | Parameters 208 | ---------- 209 | labels : str or list of str, optional 210 | Labels to unprime. If None all labels of the tensor will be 211 | unprimed. 212 | 213 | Examples 214 | -------- 215 | >>> t = Tensor(np.array([1,0]), labels=["idx"]) 216 | >>> t.prime_label("idx") 217 | >>> print(t) 218 | Tensor object: shape = (2,), labels = ["idx'"] 219 | >>> t.prime_label("idx") 220 | >>> print(t) 221 | Tensor object: shape = (2,), labels = ["idx''"] 222 | >>> t.unprime_label("idx") 223 | >>> print(t) 224 | Tensor object: shape = (2,), labels = ["idx'"] 225 | >>> t.unprime_label("idx") 226 | >>> print(t) 227 | Tensor object: shape = (2,), labels = ["idx"] 228 | """ 229 | if labels is None: 230 | labels = self.labels 231 | elif not isinstance(labels, list): 232 | labels = [labels] 233 | for i, label in enumerate(self.labels): 234 | for noprime in labels: 235 | if lbl.noprime_label(label) == noprime: 236 | self.labels[i] = lbl.unprime_label(self.labels[i]) 237 | 238 | def fuse_indices(self, indices_to_fuse, new_label, 239 | preserve_relative_order=False): 240 | """Fuse multiple indices into a single index. If 241 | `preserve_relative_order` is True, the relative order of the fused 242 | indices will be preserved. Otherwise, the order will follow the order 243 | in the `indices_to_fuse` argument. 244 | 245 | Examples 246 | -------- 247 | In this example we fuse a pair of indices to a single index, 248 | then split them again. We start with a random rank-5 tensor. 249 | >>> t=random_tensor(2,3,4,5,6, labels=["a","b","c","d","a"]) 250 | >>> t_orig=t.copy() 251 | 252 | Fuse indices "b" and "d" to a new index called "new_index". 253 | >>> t.fuse_indices(["b","d"], "new_index") 254 | >>> print(t) 255 | Tensor object: 256 | Data type: float64 257 | Number of indices: 4 258 | Index labels: 259 | 0. (dim=15) new_index 260 | 1. (dim=2) a 261 | 2. (dim=4) c 262 | 3. (dim=6) a 263 | 264 | Split the "new_index" index into two indices "b" and "d" with 265 | dimensions 3 and 5 respectively. 266 | >>> t.split_index("new_index", (3,5), ["b","d"]) 267 | >>> print(t) 268 | Tensor object: 269 | Data type: float64 270 | Number of indices: 5 271 | Index labels: 272 | 0. (dim=3) b 273 | 1. (dim=5) d 274 | 2. (dim=2) a 275 | 3. (dim=4) c 276 | 4. (dim=6) a 277 | 278 | The resulting tensor is identical to the original (up to a reordering 279 | of the indices). 280 | >>> distance(t, t_orig) 281 | 0.0 282 | """ 283 | # Move the indices to fuse to position zero 284 | self.move_indices(indices_to_fuse, 0, 285 | preserve_relative_order=preserve_relative_order) 286 | # Compute the total dimension of the new index 287 | total_dim = 1 288 | for i, x in enumerate(self.labels): 289 | if x in indices_to_fuse: 290 | total_dim *= self.data.shape[i] 291 | last_idx=i #Last fused index 292 | new_labels = [new_label] + self.labels[last_idx+1:] 293 | new_shape = (total_dim,) + self.data.shape[last_idx+1:] 294 | 295 | self.data = np.reshape(self.data, new_shape) 296 | self.labels = new_labels 297 | 298 | def split_index(self, label, new_dims, new_labels): 299 | """ 300 | Split a single index into multiple indices. 301 | 302 | See also 303 | -------- 304 | fuse_indices 305 | """ 306 | if len(new_dims) != len(new_labels): 307 | raise ValueError("Length of new_dims must equal length of " 308 | "new_labels") 309 | 310 | new_dims = tuple(new_dims) 311 | i = self.labels.index(label) 312 | new_shape = self.data.shape[:i] + new_dims + self.data.shape[i + 1:] 313 | new_labels = self.labels[:i] + new_labels + self.labels[i + 1:] 314 | 315 | self.data = np.reshape(self.data, new_shape) 316 | self.labels = new_labels 317 | 318 | def contract_internal(self, label1, label2, index1=0, index2=0): 319 | """By default will contract the first index with label1 with the 320 | first index with label2. index1 and index2 can be specified to contract 321 | indices that are not the first with the specified label.""" 322 | 323 | label1_indices = [i for i, x in enumerate(self.labels) if x == label1] 324 | label2_indices = [i for i, x in enumerate(self.labels) if x == label2] 325 | 326 | index_to_contract1 = label1_indices[index1] 327 | index_to_contract2 = label2_indices[index2] 328 | 329 | self.data = np.trace(self.data, axis1=index_to_contract1, axis2= 330 | index_to_contract2) 331 | 332 | # The following removes the contracted indices from the list of labels 333 | self.labels = [label for j, label in enumerate(self.labels) 334 | if j not in [index_to_contract1, index_to_contract2]] 335 | 336 | # aliases for contract_internal 337 | trace = contract_internal 338 | tr = contract_internal 339 | 340 | def consolidate_indices(self, labels=[]): 341 | """Combines all indices with the same label into a single label. 342 | If `labels` keyword argument is non-empty, only labels in `labels` will 343 | be consolidated. Puts labels in alphabetical order (and reshapes data 344 | accordingly) if `labels` is empty. 345 | """ 346 | labels_unique = sorted(set(self.labels)) 347 | if len(labels) !=0: 348 | #If `labels` is set, only consolidate indices in `labels` 349 | labels_unique=[x for x in labels_unique if x in labels] 350 | for p, label in enumerate(labels_unique): 351 | indices = [i for i, j in enumerate(self.labels) if j == label] 352 | # Put all of these indices together 353 | for k, q in enumerate(indices): 354 | self.data = np.rollaxis(self.data, q, p + k) 355 | # Total dimension of all indices with label 356 | total_dim = self.data.shape[p] 357 | for r in range(1, len(indices)): 358 | total_dim = total_dim * self.data.shape[p + r] 359 | # New shape after consolidating all indices with label into 360 | # one at position p 361 | new_shape = (list(self.data.shape[0:p]) + [total_dim] + 362 | list(self.data.shape[p + len(indices):])) 363 | self.data = np.reshape(self.data, tuple(new_shape)) 364 | 365 | # Update self.labels 366 | # Remove all instances of label from self.labels 367 | new_labels = [x for x in self.labels if x != label] 368 | # Reinsert label at position p 369 | new_labels.insert(p, label) 370 | self.labels = new_labels 371 | 372 | def sort_labels(self): 373 | self.consolidate_indices() 374 | 375 | def copy(self): 376 | """Creates a copy of the tensor that does not point to the original""" 377 | """Never use A=B in python as modifying A will modify B""" 378 | return Tensor(data=self.data.copy(), labels=copy.copy(self.labels)) 379 | 380 | def move_index(self, label, position): 381 | """Change the order of the indices by moving the first index with label 382 | `label` to position `position`, possibly shifting other indices forward 383 | or back in the process. """ 384 | index = self.labels.index(label) 385 | # Move label in list 386 | self.labels.pop(index) 387 | self.labels.insert(position, label) 388 | 389 | # To roll axis of self.data 390 | # Not 100% sure why, but need to add 1 when rolling an axis backward 391 | if position <= index: 392 | self.data = np.rollaxis(self.data, index, position) 393 | else: 394 | self.data = np.rollaxis(self.data, index, position + 1) 395 | 396 | def move_indices(self, labels, position, 397 | preserve_relative_order=False): 398 | """Move indices with labels in `labels` to consecutive positions 399 | starting at `position`. If `preserve_relative_order`==True, the 400 | relative order of the moved indices will be identical to their order in 401 | the original tensor. If not, the relative order will be determined by 402 | the order in the `labels` argument. 403 | 404 | Examples 405 | -------- 406 | First initialise a random tensor. 407 | >>> from tncontract import random_tensor 408 | >>> t=random_tensor(2,3,4,5,6, labels=["a", "b", "c", "b", "d"]) 409 | 410 | Now we move the indices labelled "d", "b" and "c" to position 0 (i.e. 411 | the beginning). When preserve_relative_order is True, the relative 412 | order of these indices is identical to the original tensor. 413 | >>> t.move_indices(["d","b","c"], 0, preserve_relative_order=True) 414 | >>> print(t) 415 | Tensor object: 416 | Data type: float64 417 | Number of indices: 5 418 | Index labels: 419 | 0. (dim=3) b 420 | 1. (dim=4) c 421 | 2. (dim=5) b 422 | 3. (dim=6) d 423 | 4. (dim=2) a 424 | 425 | If, on the other hand, preserve_relative_order is False, the order of 426 | the indices is determined by the order in which they appear in the 427 | `labels` argument of `move_indices`. In this case, "d" comes first 428 | then the "b" indices then "c". 429 | >>> t=random_tensor(2,3,4,5,6, labels=["a", "b", "c", "b", "d"]) 430 | >>> t.move_indices(["d","b","c"], 0, preserve_relative_order=False) 431 | >>> print(t) 432 | Tensor object: 433 | Data type: float64 434 | Number of indices: 5 435 | Index labels: 436 | 0. (dim=6) d 437 | 1. (dim=3) b 438 | 2. (dim=5) b 439 | 3. (dim=4) c 440 | 4. (dim=2) a 441 | 442 | """ 443 | 444 | if not isinstance(labels, list): 445 | labels = [labels] 446 | 447 | if preserve_relative_order: 448 | orig_labels = self.labels.copy() 449 | n_indices_to_move = 0 450 | for label in orig_labels: 451 | if label in labels: 452 | # Move label to end of list 453 | self.move_index(label, len(self.labels) - 1) 454 | n_indices_to_move += 1 455 | else: 456 | # Remove duplicates 457 | unique_labels = [] 458 | for label in labels: 459 | if label not in unique_labels: 460 | unique_labels.append(label) 461 | labels = unique_labels 462 | 463 | n_indices_to_move = 0 464 | for label in labels: 465 | for i in range(self.labels.count(label)): 466 | # Move label to end of list 467 | self.move_index(label, len(self.labels) - 1) 468 | n_indices_to_move += 1 469 | 470 | if position + n_indices_to_move > len(self.labels): 471 | raise ValueError("Specified position too far right.") 472 | 473 | # All indices to move are at the end of the array 474 | # Now put put them in desired place 475 | for j in range(n_indices_to_move): 476 | old_index = len(self.labels) - n_indices_to_move + j 477 | label = self.labels[old_index] 478 | # Move label in list 479 | self.labels.pop(old_index) 480 | self.labels.insert(position + j, label) 481 | # Reshape accordingly 482 | self.data = np.rollaxis(self.data, old_index, position + j) 483 | 484 | def conjugate(self): 485 | self.data = self.data.conjugate() 486 | 487 | def inv(self): 488 | self.data = np.linalg.inv(self.data) 489 | 490 | def add_suffix_to_labels(self, suffix): 491 | """Warning: by changing the labels, e.g. with this method, 492 | the MPS will no longer be in the correct form for various MPS functions 493 | .""" 494 | new_labels = [] 495 | for label in self.labels: 496 | new_labels.append(label + suffix) 497 | self.labels = new_labels 498 | 499 | def suf(self, suffix): 500 | """Return copy of `self`, with string `suffix` appended to all labels. 501 | """ 502 | t=self.copy() 503 | t.labels=[x+suffix for x in t.labels] 504 | return t 505 | 506 | def add_dummy_index(self, label, position=0): 507 | """Add an additional index to the tensor with dimension 1, and label 508 | specified by the index "label". The position argument specifies where 509 | the index will be inserted. """ 510 | # Will insert an axis of length 1 in the first position 511 | self.data = self.data[np.newaxis, :] 512 | self.labels.insert(0, label) 513 | self.move_index(label, position) 514 | 515 | def remove_all_dummy_indices(self, labels=None): 516 | """Removes all dummy indices (i.e. indices with dimension 1) 517 | which have labels specified by the labels argument. None 518 | for the labels argument implies all labels.""" 519 | orig_shape = self.shape 520 | for i, x in enumerate(self.labels): 521 | if labels != None: 522 | if x in labels and orig_shape[i] == 1: 523 | self.move_index(x, 0) 524 | self.data = self.data[0] 525 | self.labels = self.labels[1:] 526 | elif orig_shape[i] == 1: 527 | self.move_index(x, 0) 528 | self.data = self.data[0] 529 | self.labels = self.labels[1:] 530 | 531 | def index_dimension(self, label): 532 | """Will return the dimension of the first index with label=label""" 533 | index = self.labels.index(label) 534 | return self.data.shape[index] 535 | 536 | def to_matrix(self, row_labels): 537 | """ 538 | Convert tensor to a matrix regarding row_labels as row index 539 | (output) and the remaining indices as column index (input). 540 | """ 541 | return tensor_to_matrix(self, row_labels) 542 | 543 | def pad_index(self, label, inc, before=False): 544 | """ 545 | Increase the dimension of first index with `label` by `inc` by padding 546 | with zeros. 547 | 548 | By default zeros are appended after the last edge of the axis in 549 | question, e.g., [1,2,3] -> [1,2,3,0..0]. If `before=True` the zeros 550 | will be padded before the first edge of the index instead, 551 | e.g., [1,2,3] -> [0,..,0,1,2,3]. 552 | 553 | See also 554 | -------- 555 | numpy.pad 556 | """ 557 | if before: 558 | npad = ((inc, 0),) 559 | else: 560 | npad = ((0, inc),) 561 | index = self.labels.index(label) 562 | npad = ((0, 0),) * (index) + npad + ((0, 0),) * (self.rank - index - 1) 563 | self.data = np.pad(self.data, npad, mode='constant', constant_values=0) 564 | 565 | def contract(self, *args, **kwargs): 566 | """ 567 | A method that calls the function `contract`, passing `self` as the 568 | first argument. 569 | 570 | See also 571 | -------- 572 | contract (function) 573 | 574 | """ 575 | t = contract(self, *args, **kwargs) 576 | self.data = t.data 577 | self.labels = t.labels 578 | 579 | @property 580 | def shape(self): 581 | return self.data.shape 582 | 583 | @property 584 | def rank(self): 585 | return len(self.shape) 586 | 587 | def norm(self): 588 | """Return the frobenius norm of the tensor, equivalent to taking the 589 | sum of absolute values squared of every element. """ 590 | return np.linalg.norm(self.data) 591 | 592 | 593 | class ToContract(): 594 | """A simple class that contains a Tensor and a list of indices (labels) of 595 | that tensor which are to be contracted with another tensor. Used in 596 | __mul__, __rmul__ for convenient tensor contraction.""" 597 | 598 | def __init__(self, tensor, labels): 599 | self.tensor = tensor 600 | self.labels = labels 601 | 602 | def __mul__(self, other): 603 | # If label argument is not a tuple, simply use that as the argument to 604 | # contract function. Otherwise convert to a list. 605 | if not isinstance(self.labels, tuple): 606 | labels1 = self.labels 607 | else: 608 | labels1 = list(self.labels) 609 | if not isinstance(other.labels, tuple): 610 | labels2 = other.labels 611 | else: 612 | labels2 = list(other.labels) 613 | return contract(self.tensor, other.tensor, labels1, labels2) 614 | 615 | # Tensor constructors 616 | 617 | 618 | def random_tensor(*args, **kwargs): 619 | """Construct a random tensor of a given shape. Entries are generated using 620 | `numpy.random.rand`.""" 621 | labels = kwargs.pop("labels", []) 622 | base_label = kwargs.pop("base_label", "i") 623 | return Tensor(np.random.rand(*args), labels=labels, base_label=base_label) 624 | 625 | 626 | def zeros_tensor(*args, **kwargs): 627 | """Construct a tensor of a given shape with every entry equal to zero.""" 628 | labels = kwargs.pop("labels", []) 629 | dtype = kwargs.pop("dtype", np.float) 630 | base_label = kwargs.pop("base_label", "i") 631 | return Tensor(np.zeros(*args, dtype=dtype), labels=labels, 632 | base_label=base_label) 633 | 634 | 635 | def contract(tensor1, tensor2, labels1, labels2, index_slice1=None, 636 | index_slice2=None): 637 | """ 638 | Contract the indices of `tensor1` specified in `labels1` with the indices 639 | of `tensor2` specified in `labels2`. 640 | 641 | This is an intuitive wrapper for numpy's `tensordot` function. A pairwise 642 | tensor contraction is specified by a pair of tensors `tensor1` and 643 | `tensor2`, a set of index labels `labels1` from `tensor1`, and a set of 644 | index labels `labels2` from `tensor2`. All indices of `tensor1` with label 645 | in `labels1` are fused (preserving order) into a single label, and likewise 646 | for `tensor2`, then these two fused indices are contracted. 647 | 648 | Parameters 649 | ---------- 650 | tensor1, tensor2 : Tensor 651 | The two tensors to be contracted. 652 | 653 | labels1, labels2 : str or list 654 | The indices of `tensor1` and `tensor2` to be contracted. Can either be 655 | a single label, or a list of labels. 656 | 657 | Examples 658 | -------- 659 | Define a random 2x2 tensor with index labels "spam" and "eggs" and a random 660 | 2x3x2x4 tensor with index labels 'i0', 'i1', etc. 661 | 662 | >>> A = random_tensor(2, 2, labels = ["spam", "eggs"]) 663 | >>> B = random_tensor(2, 3, 2, 4) 664 | >>> print(B) 665 | Tensor object: shape = (2, 3, 2, 4), labels = ['i0', 'i1', 'i2', 'i3'] 666 | 667 | Contract the "spam" index of tensor A with the "i2" index of tensor B. 668 | >>> C = contract(A, B, "spam", "i2") 669 | >>> print(C) 670 | Tensor object: shape = (2, 2, 3, 4), labels = ['eggs', 'i0', 'i1', 'i3'] 671 | 672 | Contract the "spam" index of tensor A with the "i0" index of tensor B and 673 | also contract the "eggs" index of tensor A with the "i2" index of tensor B. 674 | 675 | >>> D = contract(A, B, ["spam", "eggs"], ["i0", "i2"]) 676 | >>> print(D) 677 | Tensor object: shape = (3, 4), labels = ['i1', 'i3'] 678 | 679 | Note that the following shorthand can be used to perform the same operation 680 | described above. 681 | >>> D = A["spam", "eggs"]*B["i0", "i2"] 682 | >>> print(D) 683 | Tensor object: shape = (3, 4), labels = ['i1', 'i3'] 684 | 685 | Returns 686 | ------- 687 | C : Tensor 688 | The result of the tensor contraction. Regarding the `data` and `labels` 689 | attributes of this tensor, `C` will have all of the uncontracted 690 | indices of `tensor1` and `tensor2`, with the indices of `tensor1` 691 | always coming before those of `tensor2`, and their internal order 692 | preserved. 693 | 694 | """ 695 | 696 | # If the input labels is not a list, convert to list with one entry 697 | if not isinstance(labels1, list): 698 | labels1 = [labels1] 699 | if not isinstance(labels2, list): 700 | labels2 = [labels2] 701 | 702 | tensor1_indices = [] 703 | for label in labels1: 704 | # Append all indices to tensor1_indices with label 705 | tensor1_indices.extend([i for i, x in enumerate(tensor1.labels) 706 | if x == label]) 707 | 708 | tensor2_indices = [] 709 | for label in labels2: 710 | # Append all indices to tensor1_indices with label 711 | tensor2_indices.extend([i for i, x in enumerate(tensor2.labels) 712 | if x == label]) 713 | 714 | # Replace the index -1 with the len(tensor1_indeces), 715 | # to refer to the last element in the list 716 | if index_slice1 is not None: 717 | index_slice1 = [x if x != -1 else len(tensor1_indices) - 1 for x 718 | in index_slice1] 719 | if index_slice2 is not None: 720 | index_slice2 = [x if x != -1 else len(tensor2_indices) - 1 for x 721 | in index_slice2] 722 | 723 | # Select some subset or permutation of these indices if specified 724 | # If no list is specified, contract all indices with the specified labels 725 | # If an empty list is specified, no indices will be contracted 726 | if index_slice1 is not None: 727 | tensor1_indices = [j for i, j in enumerate(tensor1_indices) 728 | if i in index_slice1] 729 | if index_slice2 is not None: 730 | tensor2_indices = [j for i, j in enumerate(tensor2_indices) 731 | if i in index_slice2] 732 | 733 | # Contract the two tensors 734 | try: 735 | C = Tensor(np.tensordot(tensor1.data, tensor2.data, 736 | (tensor1_indices, tensor2_indices))) 737 | except ValueError as e: 738 | # Print more useful info in case of ValueError. 739 | # Check if number of indices are equal 740 | if not len(tensor1_indices) == len(tensor2_indices): 741 | raise ValueError('Number of indices in contraction ' 742 | 'does not match.') 743 | # Check if indices have equal dimensions 744 | for i in range(len(tensor1_indices)): 745 | d1 = tensor1.data.shape[tensor1_indices[i]] 746 | d2 = tensor2.data.shape[tensor2_indices[i]] 747 | if d1 != d2: 748 | raise ValueError(labels1[i] + ' with dim=' + str(d1) + 749 | ' does not match ' + labels2[i] + 750 | ' with dim=' + str(d2)) 751 | # Check if indices exist 752 | for i in range(len(labels1)): 753 | if not labels1[i] in tensor1.labels: 754 | raise ValueError(labels1[i] + 755 | ' not in list of labels for tensor1') 756 | if not labels2[i] in tensor2.labels: 757 | raise ValueError(labels2[i] + 758 | ' not in list of labels for tensor2') 759 | # Re-raise exception 760 | raise e 761 | 762 | # The following removes the contracted indices from the list of labels 763 | # and concatenates them 764 | new_tensor1_labels = [i for j, i in enumerate(tensor1.labels) 765 | if j not in tensor1_indices] 766 | new_tensor2_labels = [i for j, i in enumerate(tensor2.labels) 767 | if j not in tensor2_indices] 768 | C.labels = new_tensor1_labels + new_tensor2_labels 769 | 770 | return C 771 | 772 | 773 | def tensor_product(*args): 774 | """Take tensor product of all tensors provided as input, without 775 | contracting any indices.""" 776 | t=args[0] 777 | for x in args[1:]: 778 | t=contract(t, x, [], []) 779 | return t 780 | 781 | 782 | def distance(tensor1, tensor2): 783 | """ 784 | Will compute the Frobenius distance between two tensors, specifically the 785 | distance between the flattened data arrays in the 2 norm. 786 | 787 | Notes 788 | ----- 789 | The `consolidate_indices` method will be run first on copies of the tensors 790 | to put the data in the same shape. `tensor1` and `tensor2` should have the 791 | same labels, and same shape after applying `consolidate_indices`, otherwise 792 | an error will be raised. 793 | """ 794 | t1 = tensor1.copy() 795 | t2 = tensor2.copy() 796 | t1.consolidate_indices() 797 | t2.consolidate_indices() 798 | 799 | if t1.labels == t2.labels: 800 | return np.linalg.norm(t1.data - t2.data) 801 | else: 802 | raise ValueError("Input tensors must have the same labels.") 803 | 804 | 805 | def tensor_to_matrix(tensor, row_labels): 806 | """ 807 | Convert a tensor to a matrix regarding row_labels as row index (output) 808 | and the remaining indices as column index (input). 809 | """ 810 | t = tensor.copy() 811 | # Move labels in row_labels first and reshape accordingly 812 | total_row_dimension = 1 813 | for i, label in enumerate(row_labels): 814 | t.move_index(label, i) 815 | total_row_dimension *= t.data.shape[i] 816 | 817 | total_column_dimension = int(np.product(t.data.shape) / total_row_dimension) 818 | return np.reshape(t.data, (total_row_dimension, total_column_dimension)) 819 | 820 | 821 | def matrix_to_tensor(matrix, shape, labels=None): 822 | """ 823 | Convert a matrix to a tensor by reshaping to `shape` and giving labels 824 | specifid by `labels` 825 | """ 826 | labels = [] if labels is None else labels 827 | return Tensor(np.reshape(np.array(matrix), shape), labels) 828 | 829 | 830 | def tensor_svd(tensor, row_labels, svd_label="svd_", 831 | absorb_singular_values=None): 832 | """ 833 | Compute the singular value decomposition of `tensor` after reshaping it 834 | into a matrix. 835 | 836 | Indices with labels in `row_labels` are fused to form a single index 837 | corresponding to the rows of the matrix (typically the left index of a 838 | matrix). The remaining indices are fused to form the column indices. An SVD 839 | is performed on this matrix, yielding three matrices u, s, v, where u and 840 | v are unitary and s is diagonal with positive entries. These three 841 | matrices are then reshaped into tensors U, S, and V. Contracting U, S and V 842 | together along the indices labelled by `svd_label` will yeild the original 843 | input `tensor`. 844 | 845 | Parameters 846 | ---------- 847 | tensor : Tensor 848 | The tensor on which the SVD will be performed. 849 | row_labels : list 850 | List of labels specifying the indices of `tensor` which will form the 851 | rows of the matrix on which the SVD will be performed. 852 | svd_label : str 853 | Base label for the indices that are contracted with `S`, the tensor of 854 | singular values. 855 | absorb_singular_values : str, optional 856 | If "left", "right" or "both", singular values will be absorbed into 857 | U, V, or the square root into both, respectively, and only U and V 858 | are returned. 859 | 860 | Returns 861 | ------- 862 | U : Tensor 863 | Tensor obtained by reshaping the matrix u obtained by SVD as described 864 | above. Has indices labelled by `row_labels` corresponding to the 865 | indices labelled `row_labels` of `tensor` and has one index labelled 866 | `svd_label`+"in" which connects to S. 867 | V : Tensor 868 | Tensor obtained by reshaping the matrix v obtained by SVD as described 869 | above. Indices correspond to the indices of `tensor` that aren't in 870 | `row_labels`. Has one index labelled `svd_label`+"out" which connects 871 | to S. 872 | S : Tensor 873 | Tensor with data consisting of a diagonal matrix of singular values. 874 | Has two indices labelled `svd_label`+"out" and `svd_label`+"in" which 875 | are contracted with with the `svd_label`+"in" label of U and the 876 | `svd_label`+"out" of V respectively. 877 | 878 | Examples 879 | -------- 880 | >>> a=random_tensor(2,3,4, labels = ["i0", "i1", "i2"]) 881 | >>> U,S,V = tensor_svd(a, ["i0", "i2"]) 882 | >>> print(U) 883 | Tensor object: shape = (2, 4, 3), labels = ['i0', 'i2', 'svd_in'] 884 | >>> print(V) 885 | Tensor object: shape = (3, 3), labels = ['svd_out', 'i1'] 886 | >>> print(S) 887 | Tensor object: shape = (3, 3), labels = ['svd_out', 'svd_in'] 888 | 889 | Recombining the three tensors obtained from SVD, yeilds a tensor very close 890 | to the original. 891 | 892 | >>> temp=tn.contract(S, V, "svd_in", "svd_out") 893 | >>> b=tn.contract(U, temp, "svd_in", "svd_out") 894 | >>> tn.distance(a,b) 895 | 1.922161284937472e-15 896 | """ 897 | 898 | t = tensor.copy() 899 | 900 | # Move labels in row_labels to the beginning of list, and reshape data 901 | # accordingly 902 | total_input_dimension = 1 903 | for i, label in enumerate(row_labels): 904 | t.move_index(label, i) 905 | total_input_dimension *= t.data.shape[i] 906 | 907 | column_labels = [x for x in t.labels if x not in row_labels] 908 | 909 | old_shape = t.data.shape 910 | total_output_dimension = int(np.product(t.data.shape) / total_input_dimension) 911 | data_matrix = np.reshape(t.data, (total_input_dimension, 912 | total_output_dimension)) 913 | 914 | try: 915 | u, s, v = np.linalg.svd(data_matrix, full_matrices=False) 916 | except (np.linalg.LinAlgError, ValueError): 917 | # Try with different lapack driver 918 | warnings.warn(('numpy.linalg.svd failed, trying scipy.linalg.svd with' + 919 | ' lapack_driver="gesvd"')) 920 | try: 921 | u, s, v = sp.linalg.svd(data_matrix, full_matrices=False, 922 | lapack_driver='gesvd') 923 | except ValueError: 924 | # Check for inf's and nan's: 925 | print("tensor_svd failed. Matrix contains inf's: " 926 | + str(np.isinf(data_matrix).any()) 927 | + ". Matrix contains nan's: " 928 | + str(np.isnan(data_matrix).any())) 929 | raise # re-raise the exception 930 | 931 | # New shape original index labels as well as svd index 932 | U_shape = list(old_shape[0:len(row_labels)]) 933 | U_shape.append(u.shape[1]) 934 | U = Tensor(data=np.reshape(u, U_shape), labels=row_labels + [svd_label + "in"]) 935 | V_shape = list(old_shape)[len(row_labels):] 936 | V_shape.insert(0, v.shape[0]) 937 | V = Tensor(data=np.reshape(v, V_shape), 938 | labels=[svd_label + "out"] + column_labels) 939 | 940 | S = Tensor(data=np.diag(s), labels=[svd_label + "out", svd_label + "in"]) 941 | 942 | # Absorb singular values S into either V or U 943 | # or take the square root of S and absorb into both 944 | if absorb_singular_values == "left": 945 | U_new = contract(U, S, ["svd_in"], ["svd_out"]) 946 | V_new = V 947 | return U_new, V_new 948 | elif absorb_singular_values == "right": 949 | V_new = contract(S, V, ["svd_in"], ["svd_out"]) 950 | U_new = U 951 | return U_new, V_new 952 | elif absorb_singular_values == "both": 953 | sqrtS = S.copy() 954 | sqrtS.data = np.sqrt(sqrtS.data) 955 | U_new = contract(U, sqrtS, ["svd_in"], ["svd_out"]) 956 | V_new = contract(sqrtS, V, ["svd_in"], ["svd_out"]) 957 | return U_new, V_new 958 | else: 959 | return U, S, V 960 | 961 | 962 | def tensor_qr(tensor, row_labels, qr_label="qr_"): 963 | """ 964 | Compute the QR decomposition of `tensor` after reshaping it into a matrix. 965 | Indices with labels in `row_labels` are fused to form a single index 966 | corresponding to the rows of the matrix (typically the left index of a 967 | matrix). The remaining indices are fused to form the column index. A QR 968 | decomposition is performed on this matrix, yielding two matrices q,r, where 969 | q and is a rectangular matrix with orthonormal columns and r is upper 970 | triangular. These two matrices are then reshaped into tensors Q and R. 971 | Contracting Q and R along the indices labelled `qr_label` will yeild the 972 | original input tensor `tensor`. 973 | 974 | Parameters 975 | ---------- 976 | tensor : Tensor 977 | The tensor on which the QR decomposition will be performed. 978 | row_labels : list 979 | List of labels specifying the indices of `tensor` which will form the 980 | rows of the matrix on which the QR will be performed. 981 | qr_label : str 982 | Base label for the indices that are contracted between `Q` and `R`. 983 | 984 | Returns 985 | ------- 986 | Q : Tensor 987 | Tensor obtained by reshaping the matrix q obtained from QR 988 | decomposition. Has indices labelled by `row_labels` corresponding to 989 | the indices labelled `row_labels` of `tensor` and has one index 990 | labelled `qr_label`+"in" which connects to `R`. 991 | R : Tensor 992 | Tensor obtained by reshaping the matrix r obtained by QR decomposition. 993 | Indices correspond to the indices of `tensor` that aren't in 994 | `row_labels`. Has one index labelled `qr_label`+"out" which connects 995 | to `Q`. 996 | 997 | Examples 998 | -------- 999 | 1000 | >>> from tncontract.tensor import * 1001 | >>> t=random_tensor(2,3,4) 1002 | >>> print(t) 1003 | Tensor object: shape = (2, 3, 4), labels = ['i0', 'i1', 'i2'] 1004 | >>> Q,R = tensor_qr(t, ["i0", "i2"]) 1005 | >>> print(Q) 1006 | Tensor object: shape = (2, 4, 3), labels = ['i0', 'i2', 'qr_in'] 1007 | >>> print(R) 1008 | Tensor object: shape = (3, 3), labels = ['qr_out', 'i1'] 1009 | 1010 | Recombining the two tensors obtained from `tensor_qr`, yeilds a tensor very 1011 | close to the original 1012 | 1013 | >>> x = contract(Q, R, "qr_in", "qr_out") 1014 | >>> print(x) 1015 | Tensor object: shape = (2, 4, 3), labels = ['i0', 'i2', 'i1'] 1016 | >>> distance(x,t) 1017 | 9.7619164946377426e-16 1018 | """ 1019 | t = tensor.copy() 1020 | 1021 | if not isinstance(row_labels, list): 1022 | # If row_labels is not a list, convert to list with a single entry 1023 | # "row_labels" 1024 | row_labels = [row_labels] 1025 | 1026 | # Move labels in row_labels to the beginning of list, and reshape data 1027 | # accordingly 1028 | t.move_indices(row_labels, 0) 1029 | 1030 | # Compute the combined dimension of the row indices 1031 | row_dimension = 1 1032 | for i, label in enumerate(t.labels): 1033 | if label not in row_labels: 1034 | break 1035 | row_dimension *= t.data.shape[i] 1036 | 1037 | column_labels = [x for x in t.labels if x not in row_labels] 1038 | 1039 | old_shape = t.data.shape 1040 | total_output_dimension = int(np.product(t.data.shape) / row_dimension) 1041 | data_matrix = np.reshape(t.data, (row_dimension, 1042 | total_output_dimension)) 1043 | 1044 | q, r = np.linalg.qr(data_matrix, mode="reduced") 1045 | 1046 | # New shape original index labels as well as svd index 1047 | Q_shape = list(old_shape[0:len(row_labels)]) 1048 | Q_shape.append(q.shape[1]) 1049 | Q = Tensor(data=np.reshape(q, Q_shape), labels=row_labels + [qr_label + "in"]) 1050 | R_shape = list(old_shape)[len(row_labels):] 1051 | R_shape.insert(0, r.shape[0]) 1052 | R = Tensor(data=np.reshape(r, R_shape), labels=[qr_label + "out"] + 1053 | column_labels) 1054 | 1055 | return Q, R 1056 | 1057 | 1058 | def tensor_lq(tensor, row_labels, lq_label="lq_"): 1059 | """ 1060 | Compute the LQ decomposition of `tensor` after reshaping it into a matrix. 1061 | Indices with labels in `row_labels` are fused to form a single index 1062 | corresponding to the rows of the matrix (typically the left index of a 1063 | matrix). The remaining indices are fused to form the column index. An LR 1064 | decomposition is performed on this matrix, yielding two matrices l,q, where 1065 | q and is a rectangular matrix with orthonormal rows and l is upper 1066 | triangular. These two matrices are then reshaped into tensors L and Q. 1067 | Contracting L and Q along the indices labelled `lq_label` will yeild the 1068 | original input `tensor`. Note that the LQ decomposition is actually 1069 | identical to the QR decomposition after a relabelling of indices. 1070 | 1071 | Parameters 1072 | ---------- 1073 | tensor : Tensor 1074 | The tensor on which the LQ decomposition will be performed. 1075 | row_labels : list 1076 | List of labels specifying the indices of `tensor` which will form the 1077 | rows of the matrix on which the LQ decomposition will be performed. 1078 | lq_label : str 1079 | Base label for the indices that are contracted between `L` and `Q`. 1080 | 1081 | Returns 1082 | ------- 1083 | Q : Tensor 1084 | Tensor obtained by reshaping the matrix q obtained by LQ decomposition. 1085 | Indices correspond to the indices of `tensor` that aren't in 1086 | `row_labels`. Has one index labelled `lq_label`+"out" which connects 1087 | to `L`. 1088 | L : Tensor 1089 | Tensor obtained by reshaping the matrix l obtained from LQ 1090 | decomposition. Has indices labelled by `row_labels` corresponding to 1091 | the indices labelled `row_labels` of `tensor` and has one index 1092 | labelled `lq_label`+"in" which connects to `Q`. 1093 | 1094 | See Also 1095 | -------- 1096 | tensor_qr 1097 | 1098 | """ 1099 | 1100 | col_labels = [x for x in tensor.labels if x not in row_labels] 1101 | 1102 | temp_label = lbl.unique_label() 1103 | # Note the LQ is essentially equivalent to a QR decomposition, only labels 1104 | # are renamed 1105 | Q, L = tensor_qr(tensor, col_labels, qr_label=temp_label) 1106 | Q.replace_label(temp_label + "in", lq_label + "out") 1107 | L.replace_label(temp_label + "out", lq_label + "in") 1108 | 1109 | return L, Q 1110 | 1111 | 1112 | def truncated_svd(tensor, row_labels, chi=0, threshold=1e-15, 1113 | absorb_singular_values="right", absolute = True): 1114 | """ 1115 | Will perform svd of a tensor, as in tensor_svd, and provide approximate 1116 | decomposition by truncating all but the largest k singular values then 1117 | absorbing S into U, V or both. Truncation is performedby specifying the 1118 | parameter chi (number of singular values to keep). 1119 | 1120 | Parameters 1121 | ---------- 1122 | chi : int, optional 1123 | Maximum number of singular values of each tensor to keep after 1124 | performing singular-value decomposition. 1125 | threshold : float 1126 | Threshold for the magnitude of singular values to keep. 1127 | If absolute then singular values which are less than threshold will be truncated. 1128 | If relative then singular values which are less than max(singular_values)*threshold will be truncated 1129 | """ 1130 | 1131 | U, S, V = tensor_svd(tensor, row_labels) 1132 | 1133 | singular_values = np.diag(S.data) 1134 | 1135 | # Truncate to maximum number of singular values 1136 | 1137 | if chi: 1138 | singular_values_to_keep = singular_values[:chi] 1139 | truncated_evals_1 = singular_values[chi:] 1140 | else: 1141 | singular_values_to_keep = singular_values 1142 | truncated_evals_1 = np.array([]) 1143 | 1144 | # Thresholding 1145 | 1146 | if absolute: 1147 | truncated_evals_2 = singular_values_to_keep[singular_values_to_keep <= threshold] 1148 | singular_values_to_keep = singular_values_to_keep[singular_values_to_keep > threshold] 1149 | else: 1150 | rel_thresh = singular_values[0]*threshold 1151 | truncated_evals_2 = singular_values_to_keep[singular_values_to_keep <= rel_thresh] 1152 | singular_values_to_keep = singular_values_to_keep[singular_values_to_keep > rel_thresh] 1153 | 1154 | truncated_evals = np.concatenate((truncated_evals_2, truncated_evals_1), axis=0) 1155 | 1156 | # Reconstitute and truncate corresponding singular index of U and V 1157 | 1158 | S.data = np.diag(singular_values_to_keep) 1159 | 1160 | U.move_index("svd_in", 0) 1161 | U.data = U.data[0:len(singular_values_to_keep)] 1162 | U.move_index("svd_in", (np.size(U.labels) - 1)) 1163 | V.data = V.data[0:len(singular_values_to_keep)] 1164 | 1165 | 1166 | if absorb_singular_values is None: 1167 | return U, S, V 1168 | # Absorb singular values S into either V or U 1169 | # or take the square root of S and absorb into both (default) 1170 | if absorb_singular_values == "left": 1171 | U_new = contract(U, S, ["svd_in"], ["svd_out"]) 1172 | V_new = V 1173 | elif absorb_singular_values == "right": 1174 | V_new = contract(S, V, ["svd_in"], ["svd_out"]) 1175 | U_new = U 1176 | else: 1177 | sqrtS = S.copy() 1178 | sqrtS.data = np.sqrt(sqrtS.data) 1179 | U_new = contract(U, sqrtS, ["svd_in"], ["svd_out"]) 1180 | V_new = contract(sqrtS, V, ["svd_in"], ["svd_out"]) 1181 | 1182 | return U_new, V_new, truncated_evals 1183 | 1184 | 1185 | def conjugate(tensor): 1186 | """Return complex conjugate of `tensor`""" 1187 | t = tensor.copy() 1188 | t.conjugate() 1189 | return t 1190 | 1191 | -------------------------------------------------------------------------------- /tncontract/testing.py: -------------------------------------------------------------------------------- 1 | """ 2 | testing 3 | ========== 4 | 5 | Function to run unit tests in tests/ directory using nose. 6 | 7 | Functions named test_* in files named tests/test_* will be run automatically. 8 | To run from command line use "nosetests -v tests" in tncontract/tncontract dir. 9 | """ 10 | 11 | def run(): 12 | import nose 13 | # runs tests in qutip.tests module only 14 | nose.run(defaultTest="tncontract.tests", argv=['nosetests', '-v']) 15 | 16 | 17 | if __name__ == "__main__": 18 | run() 19 | -------------------------------------------------------------------------------- /tncontract/tests/random_10site_mps.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewdarmawan/tncontract/a65b5663fe8ec24f2170cf6d2e27fe6a1882834d/tncontract/tests/random_10site_mps.dat -------------------------------------------------------------------------------- /tncontract/tests/random_10site_mps_py2.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewdarmawan/tncontract/a65b5663fe8ec24f2170cf6d2e27fe6a1882834d/tncontract/tests/random_10site_mps_py2.dat -------------------------------------------------------------------------------- /tncontract/tests/test_canonical_form_mps_conversion.py: -------------------------------------------------------------------------------- 1 | from __future__ import (absolute_import, division, 2 | print_function, unicode_literals) 3 | from builtins import * 4 | 5 | 6 | """ 7 | test_canonical_form_mps_conversion 8 | ========== 9 | 10 | Unit tests for converting between different types of canonical form MPS 11 | """ 12 | 13 | import os 14 | import sys 15 | import numpy.testing as testing 16 | import pickle 17 | import tncontract as tnc 18 | 19 | pwd = os.path.dirname(__file__) 20 | 21 | if sys.version_info > (3, 0): 22 | f = open(pwd + "/random_10site_mps.dat", "rb") 23 | psi = pickle.load(f, encoding="latin1") 24 | f.close() 25 | else: 26 | f = open(pwd + "/random_10site_mps_py2.dat", "rb") 27 | psi = pickle.load(f) 28 | f.close() 29 | 30 | 31 | def test_right_and_left_canonical_to_canonical(): 32 | # SVD compress leaves MPS in right-canonical form 33 | # Test for unnormalized MPS 34 | psi.svd_compress(threshold=1e-12, normalise=False) 35 | psicr = tnc.onedim.right_canonical_to_canonical(psi, threshold=1e-12) 36 | testing.assert_almost_equal(psi.norm(), psicr.norm(), decimal=10) 37 | l, r, n = psicr.check_canonical_form(threshold=1e-10, print_output=False) 38 | testing.assert_equal(len(l), 0) 39 | testing.assert_equal(len(r), 0) 40 | testing.assert_equal(len(n), 1) 41 | psi.left_canonise() 42 | psicl = tnc.onedim.left_canonical_to_canonical(psi, threshold=1e-12) 43 | testing.assert_almost_equal(psi.norm(), psicr.norm(), decimal=10) 44 | l, r, n = psicr.check_canonical_form(threshold=1e-10, print_output=False) 45 | testing.assert_equal(len(l), 0) 46 | testing.assert_equal(len(r), 0) 47 | testing.assert_equal(len(n), 1) 48 | testing.assert_almost_equal(tnc.onedim.inner_product_mps(psicr, psicl), 49 | psi.norm() ** 2, decimal=10) 50 | # Test for normalized MPS 51 | psi.svd_compress(threshold=1e-12, normalise=True) 52 | psicr = tnc.onedim.right_canonical_to_canonical(psi, threshold=1e-12) 53 | l, r, n = psicr.check_canonical_form(threshold=1e-10, print_output=False) 54 | testing.assert_equal(len(l), 0) 55 | testing.assert_equal(len(r), 0) 56 | testing.assert_equal(len(n), 0) 57 | psi.left_canonise() 58 | psicl = tnc.onedim.left_canonical_to_canonical(psi, threshold=1e-12) 59 | l, r, n = psicr.check_canonical_form(threshold=1e-10, print_output=False) 60 | testing.assert_equal(len(l), 0) 61 | testing.assert_equal(len(r), 0) 62 | testing.assert_equal(len(n), 0) 63 | testing.assert_almost_equal(tnc.onedim.inner_product_mps(psicr, psicl), 64 | 1.0, decimal=10) 65 | 66 | 67 | def test_right_canonical_to_canonical_and_back(): 68 | # SVD compress leaves MPS in right-canonical form 69 | psi.svd_compress(threshold=1e-12, normalise=True) 70 | psic = tnc.onedim.right_canonical_to_canonical(psi, threshold=1e-12) 71 | psir = tnc.onedim.canonical_to_right_canonical(psic) 72 | psil = tnc.onedim.canonical_to_left_canonical(psic) 73 | testing.assert_almost_equal(tnc.onedim.inner_product_mps(psi, psil), 1.0, 74 | decimal=10) 75 | testing.assert_almost_equal(tnc.onedim.inner_product_mps(psi, psir), 1.0, 76 | decimal=10) 77 | l, r = psir.check_canonical_form(threshold=1e-10, print_output=False) 78 | testing.assert_equal(r, 0) 79 | l, r = psil.check_canonical_form(threshold=1e-10, print_output=False) 80 | testing.assert_equal(l, len(psi) - 1) 81 | -------------------------------------------------------------------------------- /tncontract/tncon.py: -------------------------------------------------------------------------------- 1 | import tncontract as tn 2 | import numpy as np 3 | 4 | def con(*args): 5 | """ 6 | Contract a network of tensors. Similar purpose to NCON, described in 7 | arxiv.org/abs/1402.0939, but designed to work with the Tensor objects of 8 | tncontract. 9 | 10 | Examples 11 | -------- 12 | 13 | >>> import tncontract as tn 14 | 15 | For the examples below, we define three tensors 16 | 17 | >>> A = tn.Tensor(np.random.rand(3,2,4), labels=["a", "b", "c"]) 18 | >>> B = tn.Tensor(np.random.rand(3,4), labels=["d", "e"]) 19 | >>> C = tn.Tensor(np.random.rand(5,5,2), labels=["f", "g", "h"]) 20 | 21 | Contract a pair indices between two tensors 22 | ------------------------------------------- 23 | The following contracts pairs of indices "a","d" and "c","e" of tensors 24 | `A` and `B`. It is identical to A["a", "c"]*B["d", "e"] 25 | 26 | >>> tn.con(A, B, ("a", "d" ), ("c", "e")) 27 | Tensor object: shape = (2), labels = ["b"] 28 | 29 | Contract a pair of indices beloning to one tensor (internal edges) 30 | ------------------------------------------------------------------ 31 | The following contracts the "f" and "g" indices of tensor `C` 32 | 33 | >>> t.con(C, ("f", "g")) 34 | Tensor object: shape = (2), labels = ["h"] 35 | 36 | Return the tensor product of a pair of tensors 37 | ---------------------------------------------- 38 | After all indices have been contracted, `con` will return the tensor 39 | product of the disconnected components of the tensor contraction. The 40 | following example returns the tensor product of `A` and `B`. 41 | 42 | >>> tn.con(A, B) 43 | Tensor object: shape = (3, 2, 4, 3, 4), labels = ["a", "b", "c", "d", "e"] 44 | 45 | Contract a network of several tensors 46 | ------------------------------------- 47 | It is possible to contract a network of several tensors. Internal edges are 48 | contracted first then edges connecting separate tensors, and then the 49 | tensor product is taken of the disconnected components resulting from the 50 | contraction. Edges between separate tensors are contracted in the order 51 | they appear in the argument list. The result of the example below is a 52 | scalar (since all indices will be contracted). 53 | 54 | >>> tn.con(A, B, C, ("a", "d" ), ("c", "e"), ("f", "g"), ("h", "b")) 55 | 56 | Notes 57 | ----- 58 | Lists of tensors and index pairs for contraction may be used as arguments. 59 | The following example contracts 100 rank 2 tensors in a ring with periodic 60 | boundary conditions. 61 | >>> N=100 62 | >>> A = tn.Tensor(np.random.rand(2,2), labels=["left","right"]) 63 | >>> tensor_list = [A.suf(str(i)) for i in range(N)] 64 | >>> idx_pairs = [("right"+str(j), "left"+str(j+1)) for j in range(N-1)] 65 | >>> tn.con(tensor_list, idx_pairs, ("right"+str(N-1), "left0")) 66 | """ 67 | 68 | tensor_list = [] 69 | contract_list = [] 70 | for x in args: 71 | #Can take lists of tensors/contraction pairs as arguments 72 | if isinstance(x, list): 73 | if isinstance(x[0], tn.Tensor): 74 | tensor_list.extend(x) 75 | else: 76 | contract_list.extend(x) 77 | 78 | elif isinstance(x, tn.Tensor): 79 | tensor_list.append(x) 80 | else: 81 | contract_list.append(x) 82 | 83 | tensor_list = [t.copy() for t in tensor_list] #Unlink from memory 84 | all_tensor_indices = [t.labels for t in tensor_list] 85 | 86 | #Check that all no index is specified in more than one contraction 87 | contracted_indices = [item for pair in contract_list for item in pair] 88 | if len(set(contracted_indices)) != len(contracted_indices): 89 | raise ValueError("Index found in more than one contraction pair.") 90 | 91 | index_lookup = {} 92 | for i,labels in enumerate(all_tensor_indices): 93 | for lab in labels: 94 | if lab in index_lookup.keys(): 95 | raise ValueError("Index label "+lab+" found in two tensors."+ 96 | " Tensors must have unique index labelling.") 97 | index_lookup[lab] = i 98 | 99 | internal_contract = [] #Indicies contracted within the same tensor 100 | pairwise_contract = [] #Indicies contracted between different tensors 101 | tensor_pairs = [] 102 | tensors_involved = set() 103 | for c in contract_list: 104 | if index_lookup[c[0]] == index_lookup[c[1]]: 105 | internal_contract.append(c) 106 | else: 107 | #Takes into account case where multiple indices from a pair of 108 | #tensors are contracted (will contract in one call to np.dot) 109 | #TODO: Better to flatten first? 110 | if (tuple(np.sort((index_lookup[c[0]],index_lookup[c[1]]))) 111 | in tensor_pairs): 112 | idx = tensor_pairs.index((index_lookup[c[0]], 113 | index_lookup[c[1]])) 114 | if not isinstance(pairwise_contract[idx][0], list): 115 | pairwise_contract[idx][0] = [pairwise_contract[idx][0]] 116 | pairwise_contract[idx][1] = [pairwise_contract[idx][1]] 117 | pairwise_contract[idx][0].append(c[0]) 118 | pairwise_contract[idx][1].append(c[1]) 119 | else: 120 | pairwise_contract.append(list(c)) 121 | tensor_pairs.append(tuple(np.sort((index_lookup[c[0]],index_lookup[c[1]])))) 122 | tensors_involved.add(index_lookup[c[0]]) 123 | tensors_involved.add(index_lookup[c[1]]) 124 | 125 | #Contract all internal indices 126 | for c in internal_contract: 127 | tensor_list[index_lookup[c[0]]].trace(c[0], c[1]) 128 | 129 | #Contract pairs of tensors 130 | connected_component = [i for i in range(len(tensor_list))] 131 | for c in pairwise_contract: 132 | 133 | if isinstance(c[0], list): 134 | #Case where multiple indices of two tensors contracted 135 | d=index_lookup[c[0][0]] 136 | e=index_lookup[c[1][0]] 137 | else: 138 | d=index_lookup[c[0]] 139 | e=index_lookup[c[1]] 140 | 141 | if d==e: 142 | tensor_list[d].trace(c[0],c[1]) 143 | else: 144 | if d 1: 84 | return True 85 | return False 86 | 87 | def can_contract(self): 88 | """Check whether the virtual indices of the tensor network can be 89 | contracted, based on bond dimensions.""" 90 | rows, cols = self.data.shape 91 | left_unmatched=[] 92 | up_unmatched=[] 93 | for i in range(1,rows): 94 | for j in range(1,cols): 95 | #Check all horizontal and vertical bonds 96 | if (self[i,j].index_dimension(self.up_label)!= 97 | self[i-1,j].index_dimension(self.down_label)): 98 | left_unmatched.append((i,j)) 99 | if (self[i,j].index_dimension(self.left_label)!= 100 | self[i,j-1].index_dimension(self.right_label)): 101 | up_unmatched.append((i,j)) 102 | if len(left_unmatched) == 0 and len(up_unmatched) == 0: 103 | return True 104 | else: 105 | print("Unmatched bonds found between the following sites:") 106 | for k in left_unmatched: 107 | print("("+str(k[0]-1)+", "+ str(k[1])+")"+" and "+str(k)) 108 | for k in up_unmatched: 109 | print("("+str(k[0])+", "+ str(k[1]-1)+")"+" and "+str(k)) 110 | return False 111 | 112 | def exact_contract(self, until_column=-1): 113 | """Will perform exact contraction of all virtual indices of the square 114 | lattice, starting from the top-left, contracting the whole first 115 | column, then contracting one column at a time.""" 116 | rows, cols = self.data.shape 117 | mpo = column_to_mpo(self, 0) 118 | C = od.contract_virtual_indices(mpo) 119 | for i in range(1, cols): 120 | if i == until_column + 1: 121 | # Return the contraction early 122 | return C 123 | mpo = column_to_mpo(self, i) 124 | C = od.contract_multi_index_tensor_with_one_dim_array(C, mpo, 125 | self.right_label, self.left_label) 126 | C.remove_all_dummy_indices([self.left_label, self.up_label, 127 | self.down_label]) 128 | return C 129 | 130 | def mps_contract(self, chi, compression_type="svd", until_column=-1, 131 | max_iter=10, tolerance=1e-14, return_all_columns = False): 132 | """Approximately contract a square lattice tensor network using MPS 133 | evolution and compression. Will contract from left to right. 134 | If `return_all_columns` is true, will return a list of MPS 135 | corresponding to the contraction up to each column. 136 | """ 137 | 138 | nrows, ncols = self.shape 139 | 140 | if return_all_columns: 141 | column_list=[] 142 | 143 | # Divide matrix product state by its norm after each compression 144 | # but keep these factors in the variable `norm` 145 | norm = np.longdouble(1) 146 | for col in range(ncols - 1): 147 | if col == 0: 148 | mps_to_compress = column_to_mpo(self, 0) 149 | else: 150 | column_mpo = column_to_mpo(self, col) 151 | mps_to_compress = od.contract_mps_mpo(compressed_mps, 152 | column_mpo) 153 | 154 | if compression_type == "svd": 155 | compressed_mps = od.svd_compress_mps(mps_to_compress, chi, 156 | normalise=False, threshold=tolerance) 157 | # Normalise MPS (although keep normalisation factor in `norm`) 158 | mps_norm = compressed_mps.norm(canonical_form="right") 159 | #Return 0 if the norm of the MPS is zero 160 | if mps_norm==0.0: 161 | return 0.0 162 | compressed_mps[0].data = compressed_mps[0].data / mps_norm 163 | norm *= mps_norm 164 | elif compression_type == "variational": 165 | compressed_mps = mps_to_compress.variational_compress( 166 | chi, max_iter=max_iter, tolerance=tolerance) 167 | # Normalise MPS (although keep normalisation factor in `norm`) 168 | mps_norm = compressed_mps.norm(canonical_form="left") 169 | #Return 0 if the norm of the MPS is zero 170 | if mps_norm==0.0: 171 | return 0.0 172 | compressed_mps[-1].data = compressed_mps[-1].data / mps_norm 173 | norm *= mps_norm 174 | 175 | if return_all_columns: 176 | mps_copy=compressed_mps.copy() 177 | mps_copy[0].data *= norm 178 | column_list.append(mps_copy) 179 | 180 | if col == until_column: 181 | if return_all_columns: 182 | return column_list 183 | elif compression_type == "svd": 184 | #Convert to longdouble to store small or large values 185 | #Note this will be stored on the first tensor 186 | compressed_mps[0].data = np.longdouble( 187 | compressed_mps[0].data) 188 | compressed_mps[0].data *= norm 189 | return compressed_mps 190 | elif compression_type == "variational": 191 | compressed_mps[-1].data *= norm 192 | return compressed_mps 193 | 194 | # For final column, compute contraction exactly 195 | final_column_mps = column_to_mpo(self, ncols - 1) 196 | full_contraction = od.inner_product_mps(compressed_mps, 197 | final_column_mps, return_whole_tensor=True, 198 | complex_conjugate_bra=False) * norm 199 | if return_all_columns: 200 | column_list.append(full_contraction) 201 | return column_list 202 | else: 203 | return full_contraction 204 | 205 | def col_to_1D_array(self, col): 206 | """ 207 | Will extract column col from square_tn (which is assumed to be a 208 | SquareLatticeTensorNetwork object), and convert the column into a 209 | MatrixProductState object (if first or last column without periodic 210 | boundary conditions) or a MatrixProductOperator object. 211 | """ 212 | new_data = self[:, col].copy() 213 | return od.OneDimensionalTensorNetwork(new_data, 214 | left_label=self.up_label, 215 | right_label=self.down_label) 216 | 217 | def fliplr(self): 218 | """ 219 | Returns left-right mirror image of TN. Note: will not modify labels of 220 | constituent tensors, but will switch the `left_label` and `right_label` 221 | attributes of `SquareLatticeTensorNetwork`. 222 | """ 223 | mirror_tn=self.copy() 224 | mirror_data=np.fliplr(mirror_tn) 225 | mirror_tn.data=mirror_data 226 | mirror_tn.right_label=self.left_label 227 | mirror_tn.left_label=self.right_label 228 | 229 | return mirror_tn 230 | 231 | 232 | 233 | 234 | class SquareLatticePEPS(SquareLatticeTensorNetwork): 235 | def __init__(self, tensors, up_label="up", right_label="right", 236 | down_label="down", left_label="left", phys_label="phys", 237 | copy_data=True): 238 | SquareLatticeTensorNetwork.__init__(self, tensors, up_label, 239 | right_label, down_label, left_label, 240 | copy_data=copy_data) 241 | self.phys_label = phys_label 242 | 243 | def copy(self): 244 | """Return a copy of SquareLatticePEPS that is not linked in 245 | memory to the original.""" 246 | return SquareLatticePEPS(self.data, 247 | up_label=self.up_label, right_label=self.right_label, 248 | down_label=self.down_label, left_label=self.left_label, 249 | phys_label=self.phys_label, copy_data=True) 250 | 251 | def outer_product(self, physin_label="physin", physout_label="physout"): 252 | """ 253 | Take the outer product of this PEPS with itself, returning a PEPO. 254 | The outer product of each tensor in the PEPS is taken and 255 | virtual indices are consolidated. Returns an instance of SquareLatticePEPO.""" 256 | tensor_array=[] 257 | for row in range(self.shape[0]): 258 | new_row=[] 259 | for col in range(self.shape[1]): 260 | #This takes the outer product of two tensors 261 | #Without contracting any indices 262 | outer = tn.contract(self[row,col], self[row,col], [], []) 263 | #Replace the first physical label with physin label 264 | outer.labels[outer.labels.index(self.phys_label)]=physin_label 265 | #Replace the second physical label with physin label 266 | outer.labels[outer.labels.index(self.phys_label)]=physout_label 267 | 268 | #Consolidate indices 269 | outer.consolidate_indices(labels=[self.left_label, 270 | self.right_label, self.up_label, self.down_label]) 271 | 272 | new_row.append(outer) 273 | tensor_array.append(new_row) 274 | 275 | return SquareLatticePEPO(tensor_array, up_label=self.up_label, 276 | down_label=self.down_label, right_label=self.right_label, 277 | left_label=self.left_label, physin_label=physin_label, 278 | physout_label=physout_label) 279 | 280 | #Alias for outer_product 281 | density_operator = outer_product 282 | 283 | def inner_product_peps(peps_ket, peps_bra, exact_contract="True", 284 | complex_conjugate_bra=True, compression_type="svd", chi=2, 285 | max_iter=10, tolerance=1e-14, contract_virtual=True): 286 | new_tensors=[] #Tensors formed by contracting the physical indices of peps_ket and bra 287 | for i in range(peps_ket.shape[0]): 288 | new_row=[] 289 | for j in range(peps_ket.shape[1]): 290 | t=(tn.tensor.conjugate(peps_bra[i,j])["phys"]* 291 | peps_ket[i,j]["phys"]) 292 | t.consolidate_indices() 293 | new_row.append(t) 294 | new_tensors.append(new_row) 295 | 296 | ip=SquareLatticeTensorNetwork(new_tensors) 297 | if not contract_virtual: 298 | return ip 299 | 300 | if exact_contract: 301 | return ip.exact_contract() 302 | else: 303 | return ip.mps_contract(chi, compression_type=compression_type, 304 | max_iter=max_iter, tolerance=tolerance) 305 | 306 | def outer_product_peps(peps1, peps2, physin_label="physin", 307 | physout_label="physout"): 308 | """Return the outer product of two PEPS networks i.e. if `peps1` and 309 | `peps2` correspond to two PEPS |a> and |b> then outer_product_peps(peps1, 310 | peps2) returns the density operator corresponding to |a>. Assumes that input PEPS are the same size. The output physin label 313 | replaces the phys label of `peps2` and the output label physout replaces 314 | the phys label of `peps1`.""" 315 | #TODO input PEPS must have the same left right up down labels. Check this 316 | #TODO careful for conflicting phys labels of peps1 and peps2 317 | if peps1.shape != peps2.shape: 318 | raise ValueError("Peps input do not have same dimension.") 319 | tensor_array=[] 320 | for row in range(peps1.shape[0]): 321 | new_row=[] 322 | for col in range(peps1.shape[1]): 323 | #This takes the outer product of two tensors 324 | #Without contracting any indices 325 | outer = tn.contract(peps1[row,col], 326 | tn.tensor.conjugate(peps2[row,col]), [], []) 327 | #Replace the physical label of peps1 with physout label 328 | outer.labels[outer.labels.index(peps1.phys_label)]=physout_label 329 | #Replace the physical label of peps2 with physin label 330 | outer.labels[outer.labels.index(peps2.phys_label)]=physin_label 331 | 332 | #Consolidate indices 333 | outer.consolidate_indices(labels=[peps1.left_label, 334 | peps1.right_label, peps1.up_label, peps1.down_label]) 335 | 336 | new_row.append(outer) 337 | tensor_array.append(new_row) 338 | 339 | return SquareLatticePEPO(tensor_array, up_label=peps1.up_label, 340 | down_label=peps1.down_label, right_label=peps1.right_label, 341 | left_label=peps1.left_label, physin_label=physin_label, 342 | physout_label=physout_label) 343 | 344 | class SquareLatticePEPO(SquareLatticeTensorNetwork): 345 | def __init__(self, tensors, up_label="up", right_label="right", 346 | down_label="down", left_label="left", physin_label="physin", 347 | physout_label="physout", copy_data=True): 348 | SquareLatticeTensorNetwork.__init__(self, tensors, up_label, 349 | right_label, down_label, left_label, copy_data=copy_data) 350 | self.physin_label = physin_label 351 | self.physout_label = physout_label 352 | 353 | def copy(self): 354 | """Return a copy of SquareLatticePEPO that is not linked in 355 | memory to the original.""" 356 | return SquareLatticePEPO(self.data, 357 | up_label=self.up_label, right_label=self.right_label, 358 | down_label=self.down_label, left_label=self.left_label, 359 | physin_label=self.physin_label, 360 | physout_label=self.physout_label, copy_data=True) 361 | 362 | def trace(self): 363 | """Contract the physin and physout indices of every tensor. Returns 364 | an instance of SquareLatticeTensorNetwork.""" 365 | tensor_array=[] 366 | for i in range(self.shape[0]): 367 | row=[] 368 | for j in range(self.shape[1]): 369 | tmp=self[i,j].copy() 370 | tmp.trace(self.physin_label, self.physout_label) 371 | row.append(tmp) 372 | tensor_array.append(row) 373 | return SquareLatticeTensorNetwork(tensor_array, up_label=self.up_label, 374 | down_label=self.down_label, right_label=self.right_label, 375 | left_label=self.left_label) 376 | 377 | def apply_pepo_to_peps(peps, pepo): 378 | nrows, ncols = peps.shape 379 | 380 | new_tensors=[] 381 | for i in range(nrows): 382 | new_row=[] 383 | for j in range(ncols): 384 | new_tensor=peps[i,j][peps.phys_label]*pepo[i,j][pepo.physin_label] 385 | new_tensor.replace_label(pepo.physout_label, peps.phys_label) 386 | new_tensor.consolidate_indices() 387 | new_row.append(new_tensor) 388 | new_tensors.append(new_row) 389 | return SquareLatticePEPS(new_tensors, 390 | up_label=peps.up_label, right_label=peps.right_label, 391 | down_label=peps.down_label, left_label=peps.left_label, 392 | phys_label=peps.phys_label) 393 | 394 | def column_to_mpo(square_tn, col): 395 | """ 396 | Will extract column col from square_tn (which is assumed to be a 397 | SquareLatticeTensorNetwork object), and convert the column into a 398 | MatrixProductState object (if first or last column without periodic 399 | boundary conditions) or a MatrixProductOperator object. 400 | """ 401 | new_data = square_tn[:, col].copy() 402 | if col == 0 or col == square_tn.shape[1] - 1: 403 | if col == 0: 404 | new_mps = od.MatrixProductState(new_data, square_tn.up_label, 405 | square_tn.down_label, square_tn.right_label) 406 | for x in new_mps.data: 407 | x.remove_all_dummy_indices(square_tn.left_label) 408 | else: # Last column 409 | new_mps = od.MatrixProductState(new_data, square_tn.up_label, 410 | square_tn.down_label, square_tn.left_label) 411 | for x in new_mps.data: 412 | x.remove_all_dummy_indices(square_tn.right_label) 413 | return new_mps 414 | else: 415 | return od.MatrixProductOperator(new_data, square_tn.up_label, 416 | square_tn.down_label, square_tn.right_label, 417 | square_tn.left_label) 418 | -------------------------------------------------------------------------------- /tncontract/version.py: -------------------------------------------------------------------------------- 1 | # Package version. Loaded by setup.py and __init__.py. 2 | __version__ = '0.1.0' 3 | --------------------------------------------------------------------------------