diff --git a/.gitignore b/.gitignore index b298dfc..01d7e59 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ _build .ipynb_checkpoints docs/src/examples/**/*.m resources/ -*.prj \ No newline at end of file +*.prj +scripts/ \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 2164c6b..035079a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -14,8 +14,8 @@ sphinx: configuration: docs/src/conf.py # Optionally build your docs in additional formats such as PDF and ePub -formats: - - htmlzip +formats: [] + #- htmlzip #- pdf submodules: diff --git a/docs/requirements.txt b/docs/requirements.txt index 20914e8..7927650 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,9 +1,8 @@ -sphinx==4.5.0 -sphinx-rtd-theme==0.5.1 -sphinxcontrib-matlabdomain==0.12.0 -sphinx-prompt +sphinx==5.0.2 +sphinx-rtd-theme==0.5.2 +sphinxcontrib-matlabdomain==0.18.0 +sphinx-prompt==1.5.0 nbsphinx==0.8.9 sphinx-gallery==0.10.1 -myst-parser==0.17.2 linkify-it-py==2.0.0 - +jinja2==3.0.3 diff --git a/docs/src/conf.py b/docs/src/conf.py index 72377ee..3d03a10 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -11,7 +11,7 @@ docs_src = os.path.abspath(os.path.dirname(__file__)) # docs source folder docs_root = os.path.abspath(os.path.join(docs_src, '..')) # docs root folder repo_root = os.path.abspath(os.path.join(docs_src, '..', '..')) # repo root folder -GITHUBBASE = 'https://github.com/lkdvos/TensorTrack' # repo link +GITHUBBASE = 'https://github.com/QuantumGhent/TensorTrack' # repo link # -- Project information @@ -49,7 +49,6 @@ 'sphinx-prompt', 'sphinxcontrib.matlab', 'nbsphinx', - 'myst_parser', 'sphinx_gallery.load_style' ] @@ -57,7 +56,7 @@ templates_path = ['_templates'] # The suffix(es) of source filenames. -source_suffix = ['.rst', '.md'] +source_suffix = ['.rst'] # The master toctree document. master_doc = 'index' @@ -67,7 +66,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'default' @@ -233,24 +232,6 @@ def linkcode_resolve(domain, info): ] -# -- myst_parser ----------------------------------------------- -# extensions for markdown parser -# myst_enable_extensions = [ -# "colon_fence", -# "deflist", -# "dollarmath", -# "fieldlist", -# "html_admonition", -# "html_image", -# "linkify", -# "replacements", -# "smartquotes", -# "strikethrough", -# "substitution", -# "tasklist", -# ] - - # -- sphinx.ext.mathjax configuration ----------------------------------------------- mathjax3_config = { "tex": { diff --git a/docs/src/img/Fmove.svg b/docs/src/img/Fmove.svg index a346b90..b995591 100644 --- a/docs/src/img/Fmove.svg +++ b/docs/src/img/Fmove.svg @@ -1,5 +1,5 @@ - + @@ -9,293 +9,252 @@ - + - - - - - - - + - + - + - - - - - - - - - - - - - - - - - + + - - + + - + - + - + - + - + + + + + + + - + - + - + - + - - - - - - - - - - - - - - - - + - + - + - - - - + - - - - - + + - - + + - - + + - + - - - - - - - - - - - - - - + + - + - - - - - - - + - - - - - - - - + + - - + + - - + + - - + + + + + + + + + + + - + + + + + + + - + - + + - + + - + + - - - - - - - - - - - - - - + + + - + - + - + - + - - - - - - - - - - - - - - - - - - + + - + - - + + + + - - - - - + - - + - + + + - + - - - - - + - + - + - - - + + + - + - + - + + + + - - - - + + + + + + + + + + + + + + + + + - + + - + + + + + + + + + + + + + + + + + - - - - + + + - + + diff --git a/docs/src/img/Rmove.svg b/docs/src/img/Rmove.svg index fd76809..bc1d285 100644 --- a/docs/src/img/Rmove.svg +++ b/docs/src/img/Rmove.svg @@ -1,5 +1,5 @@ - + @@ -8,168 +8,168 @@ - - - - + - + - + + + + - + - - - - + - + - + - + - + - - - - - - - - - - + - - - - - + + - + - + + + + - + - - - - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + - + + - + + + - + + - - - - - - - - - - - + + + + + + + + + + + + - + + + - + - - - - - - - - - - + + + + + + + + + + + + - + + - + - + - + - + - + - - + + - + - + - + - + + - + + - + diff --git a/docs/src/img/fusiontensor.svg b/docs/src/img/fusiontensor.svg index 3fdeadd..89fa76c 100644 --- a/docs/src/img/fusiontensor.svg +++ b/docs/src/img/fusiontensor.svg @@ -1,5 +1,5 @@ - + @@ -9,44 +9,51 @@ + + + - - - - - - + + + + + + - + - + + - + + + - - - - - - - - - - - - + + + + + + + + + + + + + - + diff --git a/docs/src/img/ipe/Fmove.pdf b/docs/src/img/ipe/Fmove.pdf index 8752498..cf14649 100644 Binary files a/docs/src/img/ipe/Fmove.pdf and b/docs/src/img/ipe/Fmove.pdf differ diff --git a/docs/src/img/ipe/conv.sh b/docs/src/img/ipe/conv.sh new file mode 100755 index 0000000..d2eeacc --- /dev/null +++ b/docs/src/img/ipe/conv.sh @@ -0,0 +1,5 @@ +#!/bin/bash +for i in *.pdf +do + pdf2svg ${i%.*}.pdf ../${i%.*}.svg all +done diff --git a/docs/src/index.rst b/docs/src/index.rst index 41126a0..63e72a7 100644 --- a/docs/src/index.rst +++ b/docs/src/index.rst @@ -16,9 +16,9 @@ Additionally, for tensors which are invariant under general global symmetries, v :caption: Manual :maxdepth: 2 - man/intro man/tensor man/symmetries + man/algorithms .. toctree:: @@ -33,5 +33,10 @@ Additionally, for tensors which are invariant under general global symmetries, v :maxdepth: 2 lib/tensors + lib/sparse + lib/mps + lib/algorithms + lib/environments + lib/models lib/utility lib/caches diff --git a/docs/src/lib/algorithms.rst b/docs/src/lib/algorithms.rst new file mode 100644 index 0000000..204c96c --- /dev/null +++ b/docs/src/lib/algorithms.rst @@ -0,0 +1,59 @@ +Algorithms +========== + +.. toctree:: + :maxdepth: 2 + +.. module:: src + +This section contains the API documentation for the :mod:`.algorithms` module. + +Finite MPS algorithms +--------------------- + +.. autoclass:: src.algorithms.Dmrg + :no-members: + :members: fixedpoint + + +Infinite MPS algorithms +----------------------- + +.. autoclass:: src.algorithms.Vumps + :no-members: + :members: fixedpoint + +.. autoclass:: src.algorithms.IDmrg + :no-members: + :members: fixedpoint + +.. autoclass:: src.algorithms.IDmrg2 + :no-members: + :members: fixedpoint + +.. autoclass:: src.algorithms.QPAnsatz + :no-members: + :members: excitations + +.. autoclass:: src.algorithms.Vomps + :no-members: + :members: approximate + +.. autoclass:: src.algorithms.Expand + :no-members: + :members: changebonds + + +Infinite PEPS algorithms +------------------------ + +.. autoclass:: src.algorithms.Ctmrg + :no-members: + :members: fixedpoint + + + +Eigsolvers +---------- + +.. automodule:: src.algorithms.eigsolvers diff --git a/docs/src/lib/caches.rst b/docs/src/lib/caches.rst index 323cbc7..23de751 100644 --- a/docs/src/lib/caches.rst +++ b/docs/src/lib/caches.rst @@ -10,8 +10,8 @@ This section contains the API documentation for the :mod:`.caches` module. .. automodule:: src.caches + GetMD5 ------ .. automodule:: src.caches.GetMD5 - diff --git a/docs/src/lib/environments.rst b/docs/src/lib/environments.rst new file mode 100644 index 0000000..696ad17 --- /dev/null +++ b/docs/src/lib/environments.rst @@ -0,0 +1,13 @@ +Environments +============ + +.. toctree:: + :maxdepth: 2 + +.. module:: src + +This section contains the API documentation for the :mod:`.environments` module. + +.. autoclass:: src.environments.FiniteEnvironment +.. autoclass:: src.environments.CtmrgEnvironment + :no-members: diff --git a/docs/src/lib/models.rst b/docs/src/lib/models.rst new file mode 100644 index 0000000..6854de1 --- /dev/null +++ b/docs/src/lib/models.rst @@ -0,0 +1,39 @@ +Models +====== + +.. toctree:: + :maxdepth: 2 + +.. module:: src + +This section contains the API documentation for the :mod:`.models` module. + +.. Operators +.. --------- + +.. Spin operators +.. `````````````` + +.. .. autofunction:: src.models.spinoperators.sigma_min +.. .. autofunction:: src.models.spinoperators.sigma_plus +.. .. autofunction:: src.models.spinoperators.sigma_z +.. .. autofunction:: src.models.spinoperators.sigma_exhange + + +.. Fermion operators +.. ````````````````` + +.. .. autofunction:: src.models.fermionoperators.c_min +.. .. autofunction:: src.models.fermionoperators.c_plus +.. .. autofunction:: src.models.fermionoperators.c_number + + +.. Models +.. ------ + +.. autofunction:: src.models.quantum1dIsing +.. autofunction:: src.models.quantum1dHeisenberg +.. autofunction:: src.models.quantum1dHubbard + +.. autofunction:: src.models.statmech2dIsing + diff --git a/docs/src/lib/mps.rst b/docs/src/lib/mps.rst new file mode 100644 index 0000000..569c9d6 --- /dev/null +++ b/docs/src/lib/mps.rst @@ -0,0 +1,33 @@ +MPS +=== + +.. toctree:: + :maxdepth: 2 + +.. module:: src + +This section contains the API documentation for the :mod:`.mps` module. + +States +------ + +.. autoclass:: src.mps.MpsTensor +.. autoclass:: src.mps.FiniteMps + :no-members: +.. autoclass:: src.mps.UniformMps +.. autoclass:: src.mps.InfQP + + +Operators +--------- + +.. autoclass:: src.mps.MpoTensor +.. autoclass:: src.mps.FiniteMpo +.. autoclass:: src.mps.InfMpo +.. autoclass:: src.mps.InfJMpo +.. autoclass:: src.mps.PepsTensor +.. autoclass:: src.mps.PepsSandwich + :no-members: +.. autoclass:: src.mps.UniformPeps + :no-members: + :members: UniformPeps diff --git a/docs/src/lib/sparse.rst b/docs/src/lib/sparse.rst new file mode 100644 index 0000000..7593aff --- /dev/null +++ b/docs/src/lib/sparse.rst @@ -0,0 +1,12 @@ +Sparse +====== + +.. toctree:: + :maxdepth: 2 + +.. module:: src + +This section contains the API documentation for the :mod:`.sparse` module. + +.. autoclass:: src.sparse.SparseTensor + :no-members: diff --git a/docs/src/lib/tensors.rst b/docs/src/lib/tensors.rst index 085d0ef..3df31a7 100644 --- a/docs/src/lib/tensors.rst +++ b/docs/src/lib/tensors.rst @@ -12,7 +12,35 @@ This section contains the API documentation for the :mod:`.tensors` module. Symmetry sectors ---------------- -.. automodule:: src.tensors.charges +Type hierarchy +`````````````` + +.. autoclass:: src.tensors.charges.AbstractCharge +.. autoclass:: src.tensors.charges.FusionStyle +.. autoclass:: src.tensors.charges.BraidingStyle +.. autoclass:: src.tensors.charges.ProductCharge + + +Concrete charge types +````````````````````` + +.. autoclass:: src.tensors.charges.Z1 +.. autoclass:: src.tensors.charges.Z2 +.. autoclass:: src.tensors.charges.ZN +.. autoclass:: src.tensors.charges.fZ2 +.. autoclass:: src.tensors.charges.U1 +.. autoclass:: src.tensors.charges.fU1 +.. autoclass:: src.tensors.charges.SU2 +.. autoclass:: src.tensors.charges.fSU2 +.. autoclass:: src.tensors.charges.SUN +.. autoclass:: src.tensors.charges.O2 +.. autoclass:: src.tensors.charges.A4 + + +Helper routines +``````````````` + +.. autoclass:: src.tensors.charges.GtPattern Fusion trees @@ -24,18 +52,49 @@ Fusion trees Spaces ------ -.. automodule:: src.tensors.spaces +Type hierarchy +`````````````` + +.. autoclass:: src.tensors.spaces.AbstractSpace +.. autoclass:: src.tensors.spaces.SumSpace + + +Concrete space types +```````````````````` + +.. autoclass:: src.tensors.spaces.CartesianSpace +.. autoclass:: src.tensors.spaces.ComplexSpace +.. autoclass:: src.tensors.spaces.GradedSpace + + +Convenience constructor wrappers +```````````````````````````````` + +.. autofunction:: src.tensors.spaces.Z2Space +.. autofunction:: src.tensors.spaces.fZ2Space +.. autofunction:: src.tensors.spaces.U1Space +.. autofunction:: src.tensors.spaces.SU2Space + +Helper classes +`````````````` + +.. autoclass:: src.tensors.spaces.Arrow Kernels ------- -.. automodule:: src.tensors.kernels +.. autoclass:: src.tensors.kernels.AbstractBlock +.. autoclass:: src.tensors.kernels.TrivialBlock + :no-members: +.. autoclass:: src.tensors.kernels.MatrixBlock + :no-members: +.. autoclass:: src.tensors.kernels.AbelianBlock + :no-members: Tensors ------- +.. autoclass:: src.tensors.AbstractTensor .. autoclass:: src.tensors.Tensor - -.. autoclass:: src.tensors.SpTensor \ No newline at end of file diff --git a/docs/src/lib/utility.rst b/docs/src/lib/utility.rst index 8c77b18..31eef3d 100644 --- a/docs/src/lib/utility.rst +++ b/docs/src/lib/utility.rst @@ -8,7 +8,38 @@ Utility This section contains the API documentation for the :mod:`.utility` module. -.. automodule:: src.utility +Options and verbosity +--------------------- + +.. autoclass:: src.utility.Options +.. autoclass:: src.utility.Verbosity + +.. IO and converters +----------------- + +.. .. autofunction:: src.utility.safesave +.. .. autofunction:: src.utility.clear_path +.. .. autofunction:: src.utility.mat2jl +.. .. autofunction:: src.utility.jl2mat + + +Toolbox +------- + +.. autofunction:: src.utility.between +.. autofunction:: src.utility.colors +.. autofunction:: src.utility.dim2str +.. autofunction:: src.utility.diracdelta +.. autofunction:: src.utility.memsize +.. .. autofunction:: src.utility.mod1 +.. .. autofunction:: src.utility.netcon +.. autofunction:: src.utility.randc +.. autofunction:: src.utility.randnc +.. autofunction:: src.utility.simulsort +.. autofunction:: src.utility.simulsortrows +.. autofunction:: src.utility.simulunique +.. autofunction:: src.utility.swapvars +.. autofunction:: src.utility.time2str Indices @@ -28,11 +59,10 @@ Permutations .. automodule:: src.utility.permutations +Sparse Arrays +------------- -Uninit ------- - -.. automodule:: src.utility.uninit +.. automodule:: src.utility.sparse Validations diff --git a/docs/src/man/algorithms.rst b/docs/src/man/algorithms.rst new file mode 100644 index 0000000..faca9a3 --- /dev/null +++ b/docs/src/man/algorithms.rst @@ -0,0 +1,4 @@ +Algorithms +========== + +Coming soon. diff --git a/docs/src/man/symmetries.rst b/docs/src/man/symmetries.rst index 07f3df8..de1c5a6 100644 --- a/docs/src/man/symmetries.rst +++ b/docs/src/man/symmetries.rst @@ -1,2 +1,4 @@ Symmetries ========== + +Coming soon. diff --git a/docs/src/man/tensor.rst b/docs/src/man/tensor.rst index de533d0..a27e587 100644 --- a/docs/src/man/tensor.rst +++ b/docs/src/man/tensor.rst @@ -1,13 +1,15 @@ -Tensor manipulations -==================== +Tensors and tensor manipulations +================================ Definitions ----------- -We start with the definition of a tensor. According to the `Link Wikipedia page ` on tensors, there are 3 equivalent ways of describing a tensor object: +We start with the definition of a tensor. According to the `Wikipedia page `_ on tensors, there are 3 equivalent ways of describing a tensor object: * As a multidimensional array. + * As a multilinear map. + * Using tensor products. From a MATLAB perspective, the former approach is the most natural, as such tensors are easily created with several builtin methods. We thus say a tensor of size ``sz`` can be thought of as the array that is created by calling any of the standard functions ``t = zeros(sz)``, ``t = ones(sz)``, ``t = rand(sz)``, ... @@ -15,11 +17,11 @@ We will use a graphical representation of such a tensor as an object with severa Nevertheless, for our purpose it will often be useful to be able to think of such an abstract object as a map, or thus as a matrix. For this, we additionally specify the rank of the tensor ``[n1 n2]``, which results in a partition of the indices into ``n1`` left and ``n2`` right indices, which we will call **codomain** and **domain** respectively. -If we then reshape the original array into a matrix. ``m = reshape(t, prod(sz(1:n1)), prod(sz((1:n2)+n1)))``. +If we then reshape the original array into a matrix. :code:`m = reshape(t, prod(sz(1:n1)), prod(sz((1:n2)+n1)))`. With this definition however, we still run into issues when we want to perform something like matrix multiplication. As MATLAB uses column-major ordering of array elements, ``reshape`` effectively groups indices together from left to right, where the left index is changing the fastest. If we then multiply two tensors, we connect the domain (right indices) of the first tensor with the codomain (left indices) of the second, and note that the counter-clockwise ordering of our indices now causes the domain indices to be grouped bottom-to-top, while the codomain indices are grouped top-to-bottom. -Thus, in order for our matrix multiplication to be consistent, we reverse the order of the domain indices, and define the tensor map as ``m = reshape(permute(t, [1:n1 n1+flip(1:n2)]), prod(sz(1:n1)), prod(sz(n1+(1:n2))``. +Thus, in order for our matrix multiplication to be consistent, we reverse the order of the domain indices, and define the tensor map as :code:`m = reshape(permute(t, [1:n1 n1+flip(1:n2)]), prod(sz(1:n1)), prod(sz(n1+(1:n2))`. The mathematical origin of this unfortunate permutation is found when considering the dual of a tensor product of spaces, which is isomorphic to the tensor product of the dual spaces, but reversed. In other words, we start out with a tensor as follows: @@ -38,22 +40,278 @@ In summary, we consider a tensor to be an object which can be represented in eit These definitions are equivalent, and it is always possible to go from one to the other. The main reason for introducing both is that for some algorithms, one is more convenient than the other, and thus this conversion will prove useful. -Creating and accessing tensors ------------------------------- +Creating tensors +---------------- There are several options provided for creating tensors, which are designed to either mimic the existing MATLAB constructors, or to be conveniently generalisable when dealing with symmetries. Almost each of these methods is a wrapper of some sorts around the general static constructor ``Tensor.new``: .. automethod:: src.tensors.Tensor.new :noindex: +In summary, for tensors without any internal symmetry, the easiest way of constructing tensors is by using the form :code:`Tensor.new(fun, [dims...])`, where optionally it is possible to specify arrows for each of the dimensions with :code:`Tensor.new(fun, [dims...], [arrows...])`. +Additionally, it is possible to make the partition of domain and codomain by supplying a rank for the tensor :code:`Tensor.new(fun, [dims...], [arrows...], 'Rank', [r1 r2])`. +While the latter two options do not alter any of the results in the non symmetric case, adding arrows is often useful from a debugging point of view, as it will then become less likely to accidentaly connect tensors that are not compatible. Additionally, the rank determines how the tensor acts as a linear map, which becomes important for decompositions, eigenvalues, etc. +Finally, the most general form is found by explicitly constructing the vector spaces upon which the tensor will act. +For tensors without symmetries, :class:`ComplexSpace` and :class:`CartesianSpace` represent spaces with or without arrows respectively, while :class:`GradedSpace` is used for tensors which have internal symmetries. +These allow for the final form :code:`Tensor.new(fun, codomain, domain)`. +On top of this, several convenience methods exist for commonly used initializers, such as: + +* :code:`Tensor.zeros` +* :code:`Tensor.ones` +* :code:`Tensor.eye` +* :code:`Tensor.rand` +* :code:`Tensor.randn` +* :code:`Tensor.randc` +* :code:`Tensor.randnc` + +Often, it can prove useful to create new tensors based on existing ones, either to ensure that they can be added, subtracted, or contracted. +In these cases, :code:`Tensor.similar` will be the method of choice, which acts as a wrapper to avoid manually having to select spaces from the tensors. + +.. automethod:: src.tensors.Tensor.similar + :noindex: + + +Accessing tensor data +--------------------- + +In general, :class:`Tensor` objects do not allow indexing or slicing operations, as this is not compatible with their internal structure. +Nevertheless, in accordance with the two ways in which a tensor can be represented, it is possible to access the tensor data in two ways. + +First off, when we consider a tensor as a linear operator which maps the domain to the codomain, we can ask for the matrix representation of this map. +For tensors with internal symmetries this will in general be a list of matrices, that represent the block-diagonal part of the tensor, along with the corresponding charge through :code:`matrixblocks(t)`. + +Secondly, when we want to think of a tensor more in terms of an N-dimensional array, we can access these as :code:`tensorblocks(t)`. +For tensors with internal symmetries, this will generate a list of all channels that are not explicitly zero by virtue of the symmetry, along with a representation of these channels, which is called a :class:`FusionTree`. + +Additional details for the symmetric tensors can be found in the :ref:`Symmetries` section. + +As an example, it could prove educational to understand the sizes of the lists and the sizes of the blocks generated by the example code below: + +.. code-block:: matlab + + >> A = Tensor.zeros([2 2 2], 'Rank', [2 1]); + >> Ablocks = matrixblocks(A) + + Ablocks = + + 1×1 cell array + + {4×2 double} + + >> Atensors = tensorblocks(A) + + Atensors = + + 1×1 cell array + + {2×2×2 double} + + >> z2space = GradedSpace.new(Z2(0, 1), [1 1], false); + >> B = Tensor.zeros([z2space z2space], z2space); + >> [Bblocks, Bcharges] = matrixblocks(B) + + Bblocks = + + 1×2 cell array + + {2×1 double} {2×1 double} + + + Bcharges = + + 1×2 Z2: + + logical data: + 0 1 + + >> [Btensors, Btrees] = tensorblocks(B) + + Btensors = + + 4×1 cell array + + {[0]} + {[0]} + {[0]} + {[0]} + + + Btrees = + + (2, 1) Z2 FusionTree array: + + isdual: + 0 0 0 + charges: + + + | + | + + - - | + | + + - + | - | - + + - | - | - + +In the very same way, in order to write data into a tensor, the same two formats can be used. + +First off, :code:`t = fill_matrix(t, blocks)` will take a list of blocks and fill these into the tensor. +This requires the list to be full, and sorted according to the charge, or in other words it has to be of the same shape and order as the output of :code:`matrixblocks`. +If it is only necessary to change some of the blocks, :code:`t = fill_matrix(t, blocks, charges)` additionally passes on an array of charges which specifies which block will be filled. + +Similarly, :code:`t = fill_tensor(t, tensors)` will take a list of N-dimensional arrays and fill these into the tensor, in the same order and shape of the output of :code:`tensorblocks`. +If it is required to only change some of the tensors, an array of :class:`FusionTree` s can be passed in as :code:`t = fill_tensor(t, tensors, trees)` to specify which tensors should be changed. + +.. code-block:: matlab + + >> Ablocks{1} = ones(size(Ablocks{1})); + >> A = fill_matrix(A, Ablocks); + >> Atensors{1} = rand(size(Atensors{1})); + >> A = fill_matrix(A, Atensors); + + >> Bblocks = cellfun(@(x) ones(size(x)), Bblocks, 'UniformOutput', false); + >> B = fill_matrix(B, Bblocks); + >> Bblocks{1} = Bblocks{1} + 1; + >> B = fill_matrix(B, Bblocks(1), Bcharges(1)); + +Additionally, it is also possible to use initializers instead of a list of data. +These initializers should have signature :code:`fun(dims, identifier)`. +For non symmetric tensors, ``identifier`` will be omitted, but for symmetric tensors the matrix case uses charges as ``identifier``, while the tensor case uses fusion trees as ``identifier``. +Again, it is possible to select only some of the blocks through the third argument. + +.. code-block:: matlab + + >> f = @(dims, identifier) ones(dims); + >> A = fill_matrix(A, f); + >> A = fill_tensor(A, f); + + >> g = @(dims, charge) qdim(charge) * ones(dims); + >> B = fill_matrix(B, g); + >> h = @(dims, tree) qdim(f.coupled) * ones(dims); + >> B = fill_tensor(B, h); + +Finally, we mention that for most tensors, it is possible to generate an N-dimensional array representation, at the cost of losing all information about the symmetries. +This can sometimes be useful as a tool for debugging, and can be accessed through :code:`a = double(t)`. + + Index manipulations ------------------- +Once a tensor has been created, it is possible to manipulate the order and partition of the indices through the use of :code:`permute(t, p, r)`. +This methods works similarly to :code:`permute` for arrays, as it requires a permutation vector ``p`` for determining the new order of the tensor legs. +Additionally and optionally, one may specify a rank `r` to determine the partition of the resulting tensor. +In order to only change the partition without permuting indices, :code:`repartition(t, r)` also is implemented. + +.. code-block:: matlab + + >> A = Tensor.rand([1 2 3]) + + A = + + Rank (3, 0) Tensor: + + 1. CartesianSpace of dimension 1 + + 2. CartesianSpace of dimension 2 + + 3. CartesianSpace of dimension 3 + + >> A2 = repartition(A, [1 2]) + + A2 = + + Rank (1, 2) Tensor: + + 1. CartesianSpace of dimension 1 + + 2. CartesianSpace of dimension 2 + + 3. CartesianSpace of dimension 3 + + >> A3 = permute(A, [3 2 1]) + + A3 = + + Rank (3, 0) Tensor: + + 1. CartesianSpace of dimension 3 + + 2. CartesianSpace of dimension 2 + + 3. CartesianSpace of dimension 1 + + >> A3 = permute(A, [3 2 1], [2 1]) + + A3 = + + Rank (2, 1) Tensor: + + 1. CartesianSpace of dimension 3 + + 2. CartesianSpace of dimension 2 + + 3. CartesianSpace of dimension 1 + + +.. note:: + + While the partition of tensor indices might seem of little importance for tensors without internal structure, it can still have non-trivial consequences. + This is demonstrated by comparing the ``matrixblocks`` and the ``tensorblocks`` before and after repartitioning. Contractions ------------ +It is also possible to combine multiple tensors by contracting one or more indices. +This is only possible if the contracted spaces are compatible, i.e. one is the dual space of the other. +In general, all contractions will be performed pairwise, such that contracting ``A`` and ``B`` consists of permuting all uncontracted indices of ``A`` to its codomain, all contracted indices of ``A`` to its domain, and the reverse for ``B``. +Then, contraction is just a composition of linear maps, hence the need for the contracted spaces to be compatible. + +The syntax for specifying tensor contractions is based on the ``NCon`` (network contraction) algorithm described here (:arxiv:`1402.0939`). +The core principle is that contracted indices are indicated by incrementing positive integers, which are then pairwise contracted in ascending order. +Uncontracted indices are specified with negative integers, which are sorted in descending order (ascending absolute value). +It is also possible to specify the rank of the resulting tensor with a name-value argument ``'Rank'``, and use in-place conjugation with the name-value argument ``'Conj'``. + +.. autofunction:: src.utility.linalg.contract + :noindex: + Factorizations -------------- + +The reverse process, of splitting a single tensor into different, usually smaller, tensors with specific properties is done by means of factorizations. +In short, these are all analogies of their matrix counterpart, by performing the factorization on the tensor interpreted as a linear map. + +Many of these factorizations use the notion of orthogonality (unitarity when working over complex numbers). +An orthogonal tensor map ``t`` is characterised by the fact that ``t' * t = eye = t * t'``, which can be further relaxed to left- or right- orthogonal by respectively dropping the right- and left- hand side of the previous equation. + +.. automethod:: src.tensors.Tensor.leftorth + :noindex: + +.. automethod:: src.tensors.Tensor.rightorth + :noindex: + +.. automethod:: src.tensors.Tensor.tsvd + :noindex: + +.. automethod:: src.tensors.Tensor.leftnull + :noindex: + +.. automethod:: src.tensors.Tensor.rightnull + :noindex: + +.. automethod:: src.tensors.Tensor.eig + :noindex: + + +Matrix functions +---------------- + +Finally, several functions that are defined for matrices have a generalisation to tensors, again by interpreting them as linear maps. + +.. automethod:: src.tensors.Tensor.expm + :noindex: + +.. automethod:: src.tensors.Tensor.sqrtm + :noindex: + +.. automethod:: src.tensors.Tensor.inv + :noindex: + +.. automethod:: src.tensors.Tensor.mpower + :noindex: diff --git a/src/algorithms/Ctmrg.m b/src/algorithms/Ctmrg.m new file mode 100644 index 0000000..2c15eeb --- /dev/null +++ b/src/algorithms/Ctmrg.m @@ -0,0 +1,369 @@ +classdef Ctmrg + % `Corner Transfer Matrix Renormalisation Group algorithm `_ for PEPS. + % + % Properties + % ---------- + % tol : :class:`double` + % tolerance for convergence criterion, defaults to :code:`1e-10`. + % + % miniter : :class:`int` + % minimum number of iteration, defaults to :code:`5`. + % + % maxiter : :class:`int` + % maximum number of iteration, defaults to :code:`100`. + % + % projectortype : :class:`char` + % projector scheme used in the algorithm, currently only supports a single default + % value. + % + % trunc : :class:`struct` + % specification of truncation options, see :meth:`.Tensor.tsvd` for details. + % + % verbosity : :class:`.Verbosity` + % verbosity level of the algorithm, defaults to :code:`Verbosity.iter`. + % + % doplot : :class:`logical` + % plot progress, defaults to :code:`false`. + + %% Options + properties + + tol = 1e-10 + miniter = 5 + maxiter = 100 + + projectortype = 'cornersboris' + trunc + + verbosity = Verbosity.iter + doPlot = false + + doSave = false + saveIterations = 1 + saveMethod = 'full' + name = 'CTMRG' + + end + + methods + function alg = Ctmrg(kwargs) + arguments + kwargs.?Ctmrg + end + + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + alg.(field{1}) = kwargs.(field{1}); + end + end + + end + + function [envs, new_norm, err] = fixedpoint(alg, peps_top, peps_bot, envs) + % Find the fixed point CTMRG environment of an infinite PEPS overlap. + % + % Usage + % ----- + % :code:`[envs, new_norm, err] = fixedpoint(alg, peps_top, peps_bot, envs)` + % + % Arguments + % --------- + % alg : :class:`.Ctmrg` + % CTMRG algorithm. + % + % peps_top : :class:`.UniformPeps` + % top-layer uniform PEPS, usually interpreted as the 'ket' in the overlap. + % + % peps_bot : :class:`.UniformPeps` + % bottom-layer uniform PEPS, usually interpreted as the 'bra' in the overlap, + % defaults to the top layer state. + % + % envs : :class:`.CtmrgEnv` + % initial guess for the fixed point CTMRG environment. + % + % Returns + % ------- + % envs : :class:`.CtmrgEnv` + % fixed point CTMRG environment. + % + % new_norm : :class:`double` + % corresponding norm. + % + % err : :class:`double` + % final error measure at convergence. + + arguments + alg + peps_top + peps_bot = peps_top + envs = CtmrgEnvironment(peps_top, peps_bot) + end + + if isempty(alg.trunc) + alg.trunc = struct('TruncSpace', space(envs.corners{1,:,:},1)); + end + + err = Inf; + iter = 1; + + t_total = tic; + disp_init(alg); + + old_norm = contract_ctrmg(alg, peps_top, peps_bot, envs); + + eta1 = 1.0; + while (err > alg.tol && iter <= alg.maxiter) || iter <= alg.miniter + + t_iter = tic; + + eta = 0.0; + for i = 1:4 + if isfield(alg.trunc,'doPlot') && alg.trunc.doPlot + subplot(2,2,i) + end + [envs, eta0] = left_move(alg, peps_top, peps_bot, envs); + eta = max(eta, eta0); + envs = rot90(envs); + peps_top = rot90(peps_top); + peps_bot = rot90(peps_bot); + end + + new_norm = contract_ctrmg(alg, peps_top, peps_bot, envs); + + err = abs(old_norm - new_norm); + deta = abs((eta1 - eta) / eta1); + + if alg.doPlot + plot(alg, iter, envs, err); + end + + disp_iter(alg, iter, err, abs(new_norm), eta, deta, toc(t_iter)); + if alg.doSave && mod(iter, alg.saveIterations) == 0 + save_iteration(alg, envs, err, new_norm, iter, eta, toc(t_total)); + end + + if iter > alg.miniter && err < alg.tol + disp_conv(alg, err, abs(new_norm), eta, deta, toc(t_total)); + return + end + + old_norm = new_norm; + eta1 = eta; + iter = iter + 1; + end + + disp_maxiter(alg, err, abs(new_norm), eta, deta, toc(t_total)); + end + + function [envs, eta] = left_move(alg, peps_top, peps_bot, envs) + + eta = 0.0; + h = height(peps_top); + w = width(peps_top); + + for j = 1:w + + [above_projs, below_projs, etaj] = get_projectors(alg, peps_top, peps_bot, envs, j); + eta = max(eta, etaj); + + %use the projectors to grow the corners/edges + for i = 1:h + + envs.corners{1,prev(i,h),j} = contract(envs.corners{1,prev(i,h),prev(j,w)}, [2 1], ... + envs.edges{1,prev(i,h),j}, [1 3 4 -2], ... + above_projs{prev(i,h)}, [-1 4 3 2], ... + 'Rank', [1,1]); + + envs.edges{4,i,j} = contract( envs.edges{4,i,prev(j,w)}, [7 2 4 1], ... + peps_top.A{i,j}.var, [6 2 8 -2 3], ... + conj(peps_bot.A{i,j}.var), [6 4 9 -3 5], ... + below_projs{prev(i,h)}, [1 3 5 -4], ... + above_projs{i}, [-1 9 8 7], ... + 'Rank', [3,1]); + + envs.corners{4,next(i,h),j} = contract(envs.corners{4,next(i,h),prev(j,w)}, [1 2], ... + envs.edges{3,next(i,h),j}, [-1 3 4 1], ... + below_projs{i}, [2 3 4 -2], ... + 'Rank', [1,1]); + + end + + envs.corners(1,:,j) = cellfun(@(x) x ./ norm(x), envs.corners(1,:,j), 'UniformOutput', false); + envs.edges(4,:,j) = cellfun(@(x) x ./ norm(x), envs.edges(4,:,j), 'UniformOutput', false); + envs.corners(4,:,j) = cellfun(@(x) x ./ norm(x), envs.corners(4,:,j), 'UniformOutput', false); + + end + end + + function [above_projs, below_projs, etaj] = get_projectors(alg, peps_top, peps_bot, envs, j) + + etaj = 0; + h = height(peps_top); + w = width(peps_top); + above_projs = cell(h,1); + below_projs = cell(h,1); + + switch alg.projectortype + + case 'cornersboris' + + for i = 1:h + + %SW corner + Q1 = contract( envs.edges{3, mod1(i+2,h), j}, [-1 5 3 1], ... + envs.corners{4, mod1(i+2,h), prev(j,w)}, [1 2], ... + envs.edges{4, next(i,h), prev(j,w)}, [2 6 4 -6], ... + peps_top.A{next(i,h), j}.var, [7 6 5 -2 -5], ... + conj(peps_bot.A{next(i,h), j}.var), [7 4 3 -3 -4], ... + 'Rank', [3,3]); + %NW corner + Q2 = contract( envs.edges{4, i, prev(j,w)}, [-1 3 5 2], ... + envs.corners{1, prev(i,h), prev(j,w)}, [2 1], ... + envs.edges{1, prev(i,h), j}, [1 4 6 -6], ... + peps_top.A{i, j}.var, [7 3 -2 -5 4], ... + conj(peps_bot.A{i, j}.var), [7 5 -3 -4 6], ... + 'Rank', [3,3]); + + trun = [fieldnames(alg.trunc),struct2cell(alg.trunc)]'; + [U, S, V, etaij] = tsvd(contract(Q1,[-1,-2,-3,3,2,1],Q2,[1,2,3,-4,-5,-6],'Rank',[3,3]), trun{:}); + + etaj = max(etaj, etaij); + isqS = inv(sqrtm(S)); + Q = isqS*U'*Q1; + P = Q2*V'*isqS; + + %norm(contract(Q,[-1,1,2,3],P,[3,2,1,-2],'Rank',[1,1])-Tensor.eye(space(Q,1),space(Q,1))) + %norm(P*Q-Tensor.eye(space(Q,2:4)',space(Q,2:4)')) + + above_projs{i} = Q; + below_projs{i} = P; %should be ok for any arrow + + end + + end + + + end + + function total = contract_ctrmg(alg, peps_top, peps_bot, envs) + + total = 1.0; + h = height(peps_top); + w = width(peps_top); + + for i = 1:h + for j = 1:w + total = total * contract( envs.corners{1,prev(i,h),prev(j,w)}, [9 1], ... + envs.edges{1,prev(i,h),j}, [1 4 7 2], ... + envs.corners{2,prev(i,h),next(j,w)}, [2 3], ... + envs.edges{2,i,next(j,w)}, [3 5 8 17], ... + envs.corners{3,next(i,h),next(j,w)}, [17 16], ... + envs.edges{3,next(i,h),j}, [16 14 15 13], ... + envs.corners{4,next(i,h),prev(j,w)}, [13 12], ... + envs.edges{4,i,prev(j,w)}, [12 10 11 9], ... + peps_top.A{i,j}.var, [6 10 14 5 4], ... + conj(peps_bot.A{i,j}.var), [6 11 15 8 7]); + + total = total * contract( envs.corners{1,prev(i,h),prev(j,w)}, [4 1], ... + envs.corners{2,prev(i,h),j}, [1 2], ... + envs.corners{3,i,j}, [2 3], ... + envs.corners{4,i,prev(j,w)}, [3 4]); + + total = total / contract( envs.corners{1,prev(i,h),prev(j,w)}, [2 1], ... + envs.corners{2,prev(i,h),j}, [1 3], ... + envs.edges{2,i,j}, [3 4 5 6], ... + envs.corners{3,next(i,h),j}, [6 7], ... + envs.corners{4,next(i,h),prev(j,w)}, [7 8], ... + envs.edges{4,i,prev(j,w)}, [8 4 5 2]); + + total = total / contract( envs.corners{1,prev(i,h),prev(j,w)}, [1 3], ... + envs.corners{4,i,prev(j,w)}, [2 1], ... + envs.edges{1,prev(i,h),j}, [3 4 5 6], ... + envs.corners{2,prev(i,h),next(j,w)}, [6 7], ... + envs.corners{3,i,next(j,w)}, [7 8], ... + envs.edges{3,i,j}, [8 4 5 2]); + end + end + + end + + end + + %% Display + methods(Access = private) + + function plot(alg, iter, envs, eta) + if ~alg.doPlot, return; end + persistent axhistory axspectrum + + D = height(envs); + W = width(envs); + + if iter == 1 + figure('Name', 'Ctmrg'); + axhistory = subplot(D + 1, W, 1:W); + axspectrum = gobjects(D, W); + for d = 1:D, for w = 1:W + axspectrum(d, w) = subplot(D + 1, W, w + d * W); + end, end + linkaxes(axspectrum, 'y'); + end + + + if isempty(axhistory.Children) + semilogy(axhistory, iter, eta, '.', 'Color', colors(1), ... + 'DisplayName', 'Errors', 'MarkerSize', 10); + hold on + ylim(axhistory, [alg.tol / 5, max([1 eta])]); + yline(axhistory, alg.tol, '-', 'Convergence', ... + 'LineWidth', 2); + hold off + else + axhistory.Children(end).XData(end+1) = iter; + axhistory.Children(end).YData(end+1) = eta; + end + plot_entanglementspectrum(UniformMps({MpsTensor(envs.edges{1,:,:})}), 1:D, 1:W, axspectrum); + drawnow + end + + function disp_init(alg) + if alg.verbosity < Verbosity.conv, return; end + fprintf('---- CTMRG ----\n'); + end + + function disp_iter(alg, iter, err, norm, eta, deta, t) + if alg.verbosity < Verbosity.iter, return; end + fprintf('iteration: %4d\t\terror: %.2e\t\tnorm: %.10e\t\teta: %.2e\t\tdeta: %.2e\t(%s)\n', ... + iter, err, norm, eta, deta, time2str(t)); + end + + function disp_conv(alg, err, norm, eta, deta, t) + if alg.verbosity < Verbosity.conv, return; end + fprintf('CTMRG converged \t\terror: %.2e\t\tnorm: %.10e\t\teta: %.2e\t\tdeta: %.2e\t(%s)\n', ... + err, norm, eta, deta, time2str(t)); + fprintf('---------------\n'); + end + + function disp_maxiter(alg, err, norm, eta, deta, t) + if alg.verbosity < Verbosity.warn, return; end + fprintf('CTMRG max iterations \terror: %.2e\t\tnorm: %.10e\t\teta: %.2e\t\tdeta: %.2e\t(%s)\n', ... + err, norm, eta, deta, time2str(t)); + fprintf('---------------\n'); + end + + function save_iteration(alg, envs, err, norm, iter, eta, t) + fileName = alg.name; + fileData = struct; + fileData.envs = envs; + fileData.err = err; + fileData.norm = norm; + fileData.iteration = iter; + fileData.eta = eta; + fileData.time = t; + save(fileName, '-struct', 'fileData', '-v7.3'); + end + end + +end + diff --git a/src/algorithms/Dmrg.m b/src/algorithms/Dmrg.m new file mode 100644 index 0000000..0bf88b5 --- /dev/null +++ b/src/algorithms/Dmrg.m @@ -0,0 +1,321 @@ +classdef Dmrg + % `Density Matrix Renormalisation Group algorithm `_ for marix product states. + % + % Properties + % ---------- + % tol : :class:`double` + % tolerance for convergence criterion, defaults to :code:`1e-10`. + % + % miniter : :class:`int` + % minimum number of iteration, defaults to 5. + % + % maxiter : :class:`int` + % maximum number of iteration, defaults to 100. + % + % verbosity : :class:`.Verbosity` + % verbosity level of the algorithm, defaults to :code:`Verbosity.iter`. + % + % doplot : :class:`logical` + % plot progress, defaults to :code:`false`. + % + % which : :class:`char` + % eigenvalue selector (passed as the :code:`sigma` argument to :func:`.eigsolve`), + % defaults to :code:`'largestabs'`. + % + % dynamical_tols : :class:`logical` + % indicate whether or not to use a dynamical tolerance scaling for the algorithm's + % subroutines based on the current error measure, defaults to :code:`false` + % + % tol_min : :class:`double` + % smallest allowed convergence tolerance for soubroutines, defaults to :code:`1e-12`. + % + % tol_max : :class:`double` + % highest allowed convergence tolerance for soubroutines, defaults to :code:`1e-10`. + % + % eigs_tolfactor : :class:`double` + % relative scaling factor for determining the convergence tolerance of the eigensolver + % subroutine based on the current error measure, defaults to :code:`1e-6` + % + % sweepstyle : :class:`char` + % sweep style indicating how to sweep through the MPS at each iteration, options are: + % + % - :code:`'f2f'`: (default) front-to-front, sweeping from site 1 to the end and back. + % - :code:`'b2b'`: back-to-back, sweeping from site N to the start and back. + % - :code:`'m2m'`: mid-to-mid, sweeping from the middle site to both ends and back. + % + % alg_eigs : :class:`.KrylovSchur` or :class:`.Arnoldi` + % algorithm used for the local eigsolver updates, defaults to + % :code:`KrylovSchur('MaxIter', 100, 'KrylovDim', 20)`. + + %% Options + properties + tol = 1e-10 + miniter = 5 + maxiter = 100 + verbosity = Verbosity.iter + doplot = false + which = 'largestabs' + + dynamical_tols = false + tol_min = 1e-12 + tol_max = 1e-10 + eigs_tolfactor = 1e-6 + + sweepstyle {mustBeMember(sweepstyle, {'f2f', 'b2b', 'm2m'})} = 'f2f' + + doSave = false + saveIterations = 1 + saveMethod = 'full' + name = 'DMRG' + + alg_eigs = KrylovSchur('MaxIter', 100, 'KrylovDim', 20) + end + + properties (Access = private) + progressfig + end + + methods + function alg = Dmrg(kwargs) + arguments + kwargs.?Dmrg + end + + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + alg.(field{1}) = kwargs.(field{1}); + end + end + + if ~isfield('alg_eigs', kwargs) + alg.alg_eigs.tol = sqrt(alg.tol_min * alg.tol_max); + alg.alg_eigs.verbosity = alg.verbosity - 2; + end + end + + function [mps, envs, eta] = fixedpoint(alg, mpo, mps, envs) + % Find the fixed point MPS of a finite MPO, given an initial guess. + % + % Usage + % ----- + % :code:`[mps, envs, eta] = fixedpoint(alg, mpo, mps, envs)` + % + % Arguments + % --------- + % alg : :class:`.Dmrg` + % DMRG algorithm. + % + % mpo : :class:`.FiniteMpo` + % matrix product operator. + % + % mps : :class:`.FiniteMps` + % initial guess for MPS fixed point. + % + % envs : :class:`.FiniteEnvironment` + % initial guess for the environments. + % + % Returns + % ------- + % mps : :class:`.FiniteMps` + % MPS fixed point. + % + % envs : :class:`.FiniteEnvironment` + % corresponding environments. + % + % eta : :class:`double` + % final error measure at convergence. + + arguments + alg + mpo + mps + envs = initialize_envs(mpo) + end + + if length(mpo) ~= length(mps) + error('dmrg:argerror', ... + 'length of mpo (%d) should be equal to that of the mps (%d)', ... + length(mpo), length(mps)); + end + + t_total = tic; + disp_init(alg); + + for iter = 1:alg.maxiter + t_iter = tic; + + order = sweeporder(alg, length(mps)); + eta = zeros(1, length(mps)); + lambda = zeros(1, length(mps)); + + for pos = sweeporder(alg, length(mps)) + [mps, envs, lambda(pos), eta(pos)] = ... + localupdate(alg, mps, mpo, envs, order(pos)); + end + + eta = norm(eta, Inf); + lambda = sum(lambda) / length(lambda); + + if iter > alg.miniter && eta < alg.tol + disp_conv(alg, iter, lambda, eta, toc(t_total)); + return + end + + alg = updatetols(alg, iter, eta); + + plot(alg, iter, mps, eta); + disp_iter(alg, iter, lambda, eta, toc(t_iter)); + end + + disp_maxiter(alg, iter, lambda, eta, toc(t_total)); + end + + function [mps, envs, lambda, eta] = localupdate(alg, mps, mpo, envs, pos) + % update orthogonality center + mps = movegaugecenter(mps, pos); + envs = movegaugecenter(envs, mpo, mps, mps, pos); + + % compute update + H_AC = AC_hamiltonian(mpo, mps, envs, pos); + AC = mps.A{pos}; + [AC, lambda] = eigsolve(alg.alg_eigs, @(x) H_AC.apply(x), AC, 1, alg.which); + + % determine error + phase = dot(AC, mps.A{pos}); + eta = distance(AC ./ sign(phase), mps.A{pos}); + + % take step + mps.A{pos} = AC; + envs = invalidate(envs, pos); + end + end + + %% Option handling + methods + function alg = updatetols(alg, iter, eta) + if alg.dynamical_tols + alg.alg_eigs.tol = between(alg.tol_min, eta * alg.eigs_tolfactor / iter, ... + alg.tol_max); + if alg.verbosity > Verbosity.iter + fprintf('Updated eigsolver tolerances: (%e)\n', alg.alg_eigs.tol); + end + end + end + + function order = sweeporder(alg, L) + switch alg.sweepstyle + case 'f2f' + order = [1:L L-1:-1:2]; + case 'b2b' + order = [L:-1:1 2:L]; + case 'm2m' + order = circshift([2:L, L-1:-1:1], -floor(L / 2)); + otherwise + error('unknown sweepstyle %s', alg.sweepstyle); + end + end + end + + + %% Display + methods (Access = private) + function plot(alg, iter, mps, eta) + if ~alg.doplot, return; end + persistent axhistory axspectrum + + D = depth(mps); + W = period(mps); + + if isempty(alg.progressfig) || ~isvalid(alg.progressfig) || iter == 1 + alg.progressfig = figure('Name', 'Vumps'); + axhistory = subplot(D + 1, W, 1:W); + axspectrum = gobjects(D, W); + for d = 1:D, for w = 1:W + axspectrum(d, w) = subplot(D + 1, W, w + d * W); + end, end + linkaxes(axspectrum, 'y'); + end + + + if isempty(axhistory.Children) + semilogy(axhistory, iter, eta, '.', 'Color', colors(1), ... + 'DisplayName', 'Errors', 'MarkerSize', 10); + hold on + ylim(axhistory, [alg.tol / 5, max([1 eta])]); + yline(axhistory, alg.tol, '-', 'Convergence', ... + 'LineWidth', 2); + hold off + else + axhistory.Children(end).XData(end+1) = iter; + axhistory.Children(end).YData(end+1) = eta; + end + + plot_entanglementspectrum(mps, 1:period(mps), axspectrum); + drawnow + end + + function disp_init(alg) + if alg.verbosity < Verbosity.conv, return; end + fprintf('---- %s ----\n', alg.name); + end + + function disp_iter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.iter, return; end + + s = settings; + if abs(imag(lambda)) < eps(lambda)^(3/4) * abs(lambda) + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('%s %2d:\tE = %-0.4f\terror = %0.1e\t(%s)\n', ... + alg.name, iter, real(lambda), eta, time2str(t)); + otherwise + fprintf('%s %4d:\tE = %-0.15f\terror = %0.4e\t(%s)\n', ... + alg.name, iter, real(lambda), eta, time2str(t, 's')); + + end + else + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('%s %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + alg.name, iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('%s %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + alg.name, iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + end + end + + function disp_conv(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.conv, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('%s converged %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + alg.name, iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('%s converged %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + alg.name, iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + fprintf('---------------\n'); + end + + function disp_maxiter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.warn, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('%s max iterations %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + alg.name, iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('%s max iterations %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + alg.name, iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + fprintf('---------------\n'); + end + end +end + diff --git a/src/algorithms/Expand.m b/src/algorithms/Expand.m new file mode 100644 index 0000000..70056fb --- /dev/null +++ b/src/algorithms/Expand.m @@ -0,0 +1,452 @@ +classdef Expand + % Bond expansion algorithm for uniform matrix product states. + % + % Properties + % ---------- + % bondsmethod : :class:`char` + % bond expansion method, options are: + % + % - :code:`'off'`: no bond expansion. + % - :code:`'factor'`: (default) multiply the bond dimsion in each sector by a fixed + % factor + % - :code:`'explicit'`: manually provide bond dimension expansion. + % - :code:`'extrapolate'`: extrapolate the bond dimension in each sector according to + % a pre-defined exponential distribution. + % - :code:`'twosite'`: expand bond dimension according to a truncated two-site update. + % + % chargesmethod : :class:`char` + % charge expansion method, options are: + % + % - :code:`'off'`: (default) no charge expansion. + % - :code:`'fusionproduct'`: expand virtual charges according to the fusion product of + % each previous virtual space with the corresponding physical space. + % - :code:`'twosite'`: expand virtual charges according to a truncated two-site + % update. + % + % schmidtcut : :class:`double` + % cut in singular values used in two-site update, defaults to :code:`1e-5`. + % + % notrunc : :class:`logical` + % disable truncation such that the bond dimension is only grown, defaults to + % :code:`false`. + % + % noisefactor : :class:`double` + % noise factor applied to expanded MPS entries in order to improve stability, defaults + % to :code:`1e-3`. + % + % which : :class:`char` + % eigenvalue selector used in two-site update routine (passed as the :code:`sigma` + % argument to :func:`.eigsolve`), defaults to :code:`'largestabs'`. + % + % minbond : :class:`int` + % minimal bond dimension in for each charge, defaults to :code:`1` + % + % maxbond : :class:`int` + % maximal bond dimension for each charge, defaults to :code:`1e9`. + % + % tolbond : :class:`double` + % tolerance on expanded bond dimension compared to their current values, defaults to + % :code:`0.2`. + % + % bondfactor : :class:`double` + % expansion factor used for the :code:`'factor'` bond expansion method, defaults to + % :code:`1.2`. + % + % cutfactor : :class:`double` + % cut factor used in bond dimension extrapolation for the :code:`'extrapolate'` bond + % expansion method, defaults to :code:`1`. + % + % explicitbonds : :class:`int` + % vector of integers indicating the bond dimsension to add/subtract for each charge, + % defaults to :code:`[]`. + % + % mincharges : :class:`int` + % minimal number of charges in eevery virtual space, defaults to :code:`2`. + % + % finalize : :class:`function_handle` + % optional finalization. + + %% Options + properties + bondsmethod {mustBeMember(bondsmethod, ... + {'off', 'factor', 'explicit', 'extrapolate', 'twosite', 'twosite_simple'})} = 'factor' + + chargesmethod {mustBeMember(chargesmethod, ... + {'off', 'fusionproduct', 'twosite', 'twosite_simple'})} = 'off' + + % general expansion options + schmidtcut = 1e-5 + notrunc = false + + noisefactor = 1e-3 + which = 'largestabs' % needed for twosite update; needs to be passed from fixedpoint algorithm! + + % bond expansion options + minbond = 1 + maxbond = 1e9 + tolbond = 0.2 + bondfactor = 1.2 + cutfactor = 1 + explicitbonds = [] + + % charge expansion options + mincharges = 2 + + % optional finalization + finalize = [] + end + + + %% + methods + function v = Expand(kwargs) + arguments + kwargs.?Expand + end + + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + v.(field{1}) = kwargs.(field{1}); + end + end + end + + function [mps2, flag] = changebonds(alg, mpo, mps1) + % Change charges and bond dimensions of MPS virtual spaces. + % + % Usage + % ----- + % :code:`[mps2, flag] = changebonds(alg, mpo, mps1)` + % + % Arguments + % --------- + % alg : :class:`.Expand` + % bond expansion algorithm. + % + % mpo : :class:`.InfMpo` + % matrix product operator. + % + % mps1 : :class:`.UniformMps` + % MPS to be expanded. + % + % Returns + % ------- + % mps2 : :class:`.UniformMps` + % expanded MPS. + % + % flag : :class:`struct` + % explain. + + % canonicalize before starting + for d = 1:depth(mps1) + mps1(d) = canonicalize(mps1(d)); + end + + [new_spaces, flag] = determine_new_spaces(alg, mpo, mps1); + new_spaces = fix_injectivity(alg, mps1, new_spaces); + mps2 = expand_mps(alg, mps1, new_spaces); + + % finalize + if ~isempty(alg.finalize) + mps2 = approximate(alg.finalize, mpo, mps1, mps2); + end + end + + function [new_spaces, flag] = determine_new_spaces(alg, mpo, mps) + % Determine which charges and bond dimensions to add/remvove in mps virtual + % spaces. + + for d = depth(mps):-1:1 + for w = period(mps):-1:1 + if startsWith(alg.bondsmethod, 'twosite') || startsWith(alg.chargesmethod, 'twosite') + % twosite expansion takes care of both bonds and charges at the same time + [addbond, addcharge] = expand_twosite(alg, mpo, mps, d, w); + else + % heuristic charge and bond expansion based on current spectrum + [S, C] = schmidt_values(mps(d), w); + addbond = expand_bonds(alg, S); + addcharge = expand_charges(alg, mps, d, w, S, C); + end + if alg.notrunc + addbond = max(0, addbond); + end + flag = addcharge.flag ~= 0 || nnz(addbond) > 0; + + old_space = rightvspace(mps(d).AR{w}); + + new_space = struct; + new_space.charges = charges(old_space); + new_space.degeneracies = degeneracies(old_space) + addbond; + + if addcharge.flag && isdual(old_space) + addcharge.charges = conj(addcharge.charges); + end + + if addcharge.flag == +1 % add charges + new_space.charges = [new_space.charges, addcharge.charges]; + new_space.degeneracies = [new_space.degeneracies, addcharge.bonds]; + elseif addcharge.flag == -1 % remove charges + keep = ~ismember(new_space.charges, addcharge.charges); + new_space.charges = new_space.charges(keep); + new_space.degeneracies = new_space.degeneracies(keep); + end + + new_spaces(d, w) = old_space.new(new_space, isdual(old_space)); + end + end + end + + function spaces = fix_injectivity(alg, mps, spaces) + % attempt to ensure injectivity + for d = 1:depth(mps) + flag = false(1, period(mps)); + while ~all(flag) + for w = period(mps):-1:1 + ww = next(w, period(mps)); + www = prev(w, period(mps)); + rhs1 = prod([spaces(d, www) pspace(mps, w)'], ... + isdual(spaces(d, w))); + rhs2 = prod([spaces(d, ww) pspace(mps, ww)], ... + isdual(spaces(d, w))); + tmp = spaces(d, w); + spaces(d, w) = infimum(spaces(d, w), rhs1); + spaces(d, w) = infimum(spaces(d, w), rhs2); + assert(dims(spaces(d, w)) > 0, 'no fusion channels left'); + flag(w) = tmp == spaces(d, w); + end + end + end + end + + function [addbond, addcharge] = expand_twosite(alg, mpo, mps, d, w) + % Does a twosite update and adds/removes charges that are above/under the cut + % in the new spectrum. + % For larger unit cells, cut is chosen to the right of site w, so + % couple sites w and w+1. + + [svals, charges] = schmidt_values(mps(d), w); + dd = prev(d, depth(mps)); + ww = next(w, period(mps)); + + % perform twosite update, take SVD and normalize + [GL, GR] = environments(Vumps, mpo, mps); % should be able to input this... + H_AC2 = AC2_hamiltonian(mpo, mps, GL, GR, w); + AC2 = computeAC2(mps, dd, w); + if strcmp(alg.bondsmethod, 'twosite') + % twosite fixed point + [AC2, ~] = eigsolve(H_AC2{1}, AC2, 1, alg.which); + else + % single application + AC2 = apply(H_AC2{1}, AC2); + end + [~, C2, ~] = tsvd(AC2.var, ... + 1:mps(dd).AC{w}.plegs+1, ... + (1:mps(dd).AR{ww}.plegs+1) + 1 + mps(dd).AC{w}.plegs, ... + 'TruncBelow', alg.schmidtcut, ... + 'TruncDim', alg.maxbond); + [svals2, charges2] = matrixblocks(C2); + + added_charges = setdiff(charges2, charges); + removed_charges = setdiff(charges, charges2); + if ~isempty(removed_charges) && ~alg.notrunc + % need to remove sectors + addcharge.flag = -1; + addcharge.charges = removed_charges; + elseif ~isempty(added_charges) + % need to add sectors + addcharge.flag = +1; + addcharge.charges = added_charges; + addcharge.bonds = ones(size(added_charges)); + for ii = 1:size(added_charges) + addcharge.bonds(ii) = length(svals2{added_charges(ii) == charges2}); + end + else + % do nothing + addcharge.flag = 0; + end + + % determine new bonds + addbond = zeros(size(svals)); + for ii = 1:length(svals) + if ismember(charges(ii), charges2) + chi_new = length(svals2{charges(ii) == charges2}); + chi_new = between(alg.minbond, chi_new, alg.maxbond); + addbond(ii) = chi_new - length(svals{ii}); + else % charge was removed + addbond(ii) = 0; + end + end + + % enforce alg.notrunc option + if alg.notrunc + if addcharge.flag == -1 + addcharge = struct('flag', 0, 'bonds', [], 'charges', []); + end + addbond = max(addbond, 0); + end + + end + + function addcharge = expand_charges(alg, mps, d, w, svals, charges) + % Determine whether to add or substract sectors. + % addcharge.flag = {+1 [0] -1} for addition/subtraction of sectors. + % addcharge.charges = [new_charges] + % addcharge.bonds = [new_bonds] + + addcharge = struct('flag', false, 'bonds', [], 'charges', []); + + % determine cut if alg.maxbond is reached: + cut = alg.schmidtcut; + for ii = 1:length(svals) + if length(svals{ii}) > alg.maxbond + cut = max(cut, svals{ii}(alg.maxbond)); + end + end + + highest_schmidt = cellfun(@(x) x(1), svals); + + switch alg.chargesmethod + + case {'off', []} + + addcharge.flag = 0; + + case 'fusionproduct' + % Standard case, adds all remaining charges in fusion product of virtual + % and physical space if none of current charges has a highest schmidt + % value above the specified cut. + % Removes all but one charge whose highest schmidt value falls below + % the specified cut. + + if all(highest_schmidt > cut) + % need to add sectors + addcharge.flag = +1; + ww = prev(w, period(mps)); + new_sector = leftvspace(mps(d), ww) * prod(pspace(mps, ww)); + new_charges = new_sector.dimensions.charges; + addcharge.charges = setdiff(new_charges, charges); + addcharge.bonds = ones(size(addcharge.charges)); + elseif length(svals) > alg.mincharges && sum(highest_schmidt < cut) >= 2 + % need to remove sectors + addcharge.flag = -1; + below = find(highest_schmidt < cut); + [~, keep] = max(highest_schmidt(below)); + below(keep) = []; + addcharge.charges = charges(below); + else + % do nothing + addcharge.flag = 0; + end + + end + + % enforce alg.notrunc option + if addcharge.flag == -1 && alg.notrunc + addcharge = struct('flag', 0, 'bonds', [], 'charges', []); + end + + end + + function addbond = expand_bonds(alg, svals) + % Determine whether to add or substract bond dimension. Returns an 1 x n array + % :code:`addbond` of integers, where :code:`addbond(i)` is the bond dimension to + % be added to/subtracted from sector :code:`i`. + + %new bonds are explicitly provided + if strcmp(alg.bondsmethod, 'explicit') + addbond = alg.explicitbonds; + return + end + + % recursive + if iscell(svals) + addbond = zeros(size(svals)); + for ii = 1:length(svals) + addbond(ii) = expand_bonds(alg, svals{ii}); + end + return + end + + cut = alg.schmidtcut; + if length(svals) > alg.maxbond + cut = max(cut, svals(alg.maxbond)); + end + chi = length(svals); + chi_tol = ceil(alg.tolbond * chi); + + switch alg.bondsmethod + case 'factor' + % new bond is factor * old bond + + if svals(end) > cut + % need to add bond + chi_new = ceil(alg.bondfactor * chi); + elseif chi > alg.minbond && svals(max(1, chi-chi_tol)) < cut && ~alg.notrunc + % need to subtract bond + chi_new = min(nnz(svals > cut) + floor(chi_tol / 2), chi); + else + % do nothing + chi_new = chi; + end + + case 'extrapolate' + % do an extrapolation in order to determine new bond + + if svals(end) > cut + % need to add bond + chi_new = extrapolate_schmidt(svals, cut) + floor(chi_tol / 2); + elseif chi > alg.minbond && svals(max(1, chi-chi_tol)) < cut && ~alg.notrunc + % need to subtract bond + chi_new = min(nnz(svals > cut) + 1 + ceil(chi_tol / 2), chi); + else + % do nothing + chi_new = chi; + end + + otherwise + % no method specified + chi_new = chi; + + end + + % enforce min/max bonds. + chi_new = between(alg.minbond, chi_new, alg.maxbond); + addbond = chi_new - chi; + + % enforce alg.notrunc option + if addbond < 0 && alg.notrunc + addbond = 0; + end + + % auxiliary function + function chi = extrapolate_schmidt(svals, cut) + if length(svals) < 5 + chi = max(alg.minbond, ceil(alg.bondfactor * length(svals))); + return + end + y = reshape(log10(svals), [length(svals), 1]); + x = reshape(1:length(y), [length(svals), 1]); + range = floor(length(y) * 1/3):length(y); + ab = [x(range), 1 + 0 * x(range)] \ y(range); + chi = ceil((log10(cut / alg.cutfactor) -ab(2)) / ab(1)); + end + end + + function mps = expand_mps(alg, mps, new_spaces) + % Update mps virtual spaces according to given charge and bond expansion. + + for d = 1:depth(mps) + % expand tensors and convert to UniformMps + AR = mps(d).AR; + for w = 1:period(mps) + AR{w} = expand(AR{w}, new_spaces(d, prev(w, period(mps))), ... + new_spaces(d, w), alg.noisefactor); + end + + mps(d) = UniformMps(AR); + end + end + + end +end diff --git a/src/algorithms/IDmrg.m b/src/algorithms/IDmrg.m new file mode 100644 index 0000000..12e5d65 --- /dev/null +++ b/src/algorithms/IDmrg.m @@ -0,0 +1,260 @@ +classdef IDmrg + % `Infinite Density Matrix Renormalization Group algorithm `_. + % + % Properties + % ---------- + % tol : :class:`double` + % tolerance for convergence criterion, defaults to :code:`1e-10`. + % + % miniter : :class:`int` + % minimum number of iteration, defaults to 5. + % + % maxiter : :class:`int` + % maximum number of iteration, defaults to 100. + % + % verbosity : :class:`.Verbosity` + % verbosity level of the algorithm, defaults to :code:`Verbosity.iter`. + % + % which : :class:`char` + % eigenvalue selector (passed as the :code:`sigma` argument to :func:`.eigsolve`), + % defaults to :code:`'largestabs'`. + % + % dynamical_tols : :class:`logical` + % indicate whether or not to use a dynamical tolerance scaling for the algorithm's + % subroutines based on the current error measure, defaults to :code:`false`. + % + % tol_min : :class:`double` + % smallest allowed convergence tolerance for soubroutines, defaults to :code:`1e-12`. + % + % tol_max : :class:`double` + % highest allowed convergence tolerance for soubroutines, defaults to :code:`1e-6`. + % + % eigs_tolfactor : :class:`double` + % relative scaling factor for determining the convergence tolerance of the local + % update solver subroutine based on the current error measure, defaults to + % :code:`1e-4`. + % + % alg_eigs : :class:`.KrylovSchur` or :class:`.Arnoldi` + % algorithm used for the eigsolver subroutines, defaults to + % :code:`Arnoldi('MaxIter', 100, 'KrylovDim', 20)`. + + properties + tol = 1e-10 + miniter = 5 + maxiter = 100 + verbosity = Verbosity.iter + which = 'largestabs' + + dynamical_tols = true + tol_min = 1e-12 + tol_max = 1e-6 + eigs_tolfactor = 1e-4 + + alg_eigs = Arnoldi('MaxIter', 100, 'KrylovDim', 20) + end + + properties (Access = private) + % TODO: alg_canonical, alg_environments? cf Vumps + end + + methods + function alg = IDmrg(kwargs) + arguments + kwargs.?IDmrg + end + + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + alg.(field{1}) = kwargs.(field{1}); + end + end + + if ~isfield('alg_eigs', kwargs) + alg.alg_eigs.tol = sqrt(alg.tol_min * alg.tol_max); + alg.alg_eigs.verbosity = alg.verbosity - 2; + end + end + + function [mps, lambda, GL, GR] = fixedpoint(alg, mpo, mps) + % Find the fixed point MPS of an infinite MPO, given an initial guess. + % + % Usage + % ----- + % :code:`[mps, lambda, GL, GR] = fixedpoint(alg, mpo, mps)` + % + % Arguments + % --------- + % alg : :class:`.IDmrg` + % IDMRG algorithm. + % + % mpo : :class:`.InfMpo` + % matrix product operator. + % + % mps : :class:`.UniformMps` + % initial guess for MPS fixed point. + % + % Returns + % ------- + % mps : :class:`.UniformMps` + % MPS fixed point. + % + % lambda : :class:`double` + % eigenvalue. + % + % GL : :class:`cell` of :class:`.MpsTensor` + % left environment tensors. + % + % GR : :class:`cell` of :class:`.MpsTensor` + % right environment tensors. + + if period(mpo) ~= period(mps) + error('idmrg:argerror', ... + 'periodicity of mpo (%d) should be equal to that of the mps (%d)', ... + period(mpo), period(mps)); + end + + t_total = tic; + disp_init(alg); + + [GL, GR] = environments(mpo, mps, mps); + + for iter = 1:alg.maxiter + t_iter = tic; + + C_ = mps.C{end}; + kwargs = {}; + lambdas = zeros(1, period(mps)); + for pos = 1:period(mps) + H = AC_hamiltonian(mpo, mps, GL, GR, pos); + [mps.AC{pos}, lambdas(pos)] = ... + eigsolve(alg.alg_eigs, @(x) H{1}.apply(x), mps.AC{pos}, 1, ... + alg.which); + [mps.AL{pos}, mps.C{pos}] = leftorth(mps.AC{pos}); + + T = transfermatrix(mpo, mps, mps, pos, 'Type', 'LL'); + GL{next(pos, period(mps))} = apply(T, GL{pos}) / lambdas(pos); + end + + for pos = period(mps):-1:1 + H = AC_hamiltonian(mpo, mps, GL, GR, pos); + [mps.AC{pos}, lambdas(pos)] = ... + eigsolve(alg.alg_eigs, @(x) H{1}.apply(x), mps.AC{pos}, 1, ... + alg.which); + [mps.C{prev(pos, period(mps))}, mps.AR{pos}] = rightorth(mps.AC{pos}); + + T = transfermatrix(mpo, mps, mps, pos, 'Type', 'RR').'; + GR{pos} = apply(T, GR{next(pos, period(mps))}) / lambdas(pos); + end + + eta = distance(C_, mps.C{end}); + lambda = prod(lambdas); + + if iter > alg.miniter && eta < alg.tol + mps = canonicalize(mps); + disp_conv(alg, iter, lambda, eta, toc(t_total)); + return + end + + alg = updatetols(alg, iter, eta); + disp_iter(alg, iter, lambda, eta, toc(t_iter)); + end + + mps = canonicalize(mps); + disp_maxiter(alg, iter, lambda, eta, toc(t_total)); + end + end + + + methods + function [GL, GR, lambda] = environments(alg, mpo, mps, GL, GR) + arguments + alg + mpo + mps + GL = cell(1, period(mps)) + GR = cell(1, period(mps)) + end + + [GL, GR, lambda] = environments(mpo, mps, mps, GL, GR); + end + + function alg = updatetols(alg, iter, eta) + if alg.dynamical_tols + alg.alg_eigs.tol = between(alg.tol_min, eta * alg.eigs_tolfactor, ... + alg.tol_max / iter); + + if alg.verbosity > Verbosity.iter + fprintf('Updated subalgorithm tolerances: (%e)\n', ... + alg.alg_eigs.tol); + end + end + end + end + + %% Display + methods (Access = private) + function disp_init(alg) + if alg.verbosity < Verbosity.conv, return; end + fprintf('---- IDmrg ----\n'); + end + + function disp_iter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.iter, return; end + + s = settings; + if abs(imag(lambda)) < eps(lambda)^(3/4) * abs(lambda) + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('IDmrg %2d:\tE = %-0.4f\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), eta, time2str(t)); + otherwise + fprintf('IDmrg %4d:\tE = %-0.15f\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), eta, time2str(t, 's')); + + end + else + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('IDmrg %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('IDmrg %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + end + end + + function disp_conv(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.conv, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('IDmrg converged %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('IDmrg converged %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + fprintf('---------------\n'); + end + + function disp_maxiter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.warn, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('IDmrg max iterations %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('IDmrg max iterations %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + fprintf('---------------\n'); + end + end +end + diff --git a/src/algorithms/IDmrg2.m b/src/algorithms/IDmrg2.m new file mode 100644 index 0000000..b6b51cf --- /dev/null +++ b/src/algorithms/IDmrg2.m @@ -0,0 +1,409 @@ +classdef IDmrg2 + % `Infinite Density Matrix Renormalization Group algorithm with 2-site updates `_. + % + % Properties + % ---------- + % tol : :class:`double` + % tolerance for convergence criterion, defaults to :code:`1e-10`. + % + % miniter : :class:`int` + % minimum number of iteration, defaults to 5. + % + % maxiter : :class:`int` + % maximum number of iteration, defaults to 100. + % + % verbosity : :class:`.Verbosity` + % verbosity level of the algorithm, defaults to :code:`Verbosity.iter`. + % + % doplot : :class:`logical` + % plot progress, defaults to :code:`false`. + % + % which : :class:`char` + % eigenvalue selector (passed as the :code:`sigma` argument to :func:`.eigsolve`), + % defaults to :code:`'largestabs'`. + % + % dynamical_tols : :class:`logical` + % indicate whether or not to use a dynamical tolerance scaling for the algorithm's + % subroutines based on the current error measure, defaults to :code:`false`. + % + % tol_min : :class:`double` + % smallest allowed convergence tolerance for soubroutines, defaults to :code:`1e-12`. + % + % tol_max : :class:`double` + % highest allowed convergence tolerance for soubroutines, defaults to :code:`1e-6`. + % + % eigs_tolfactor : :class:`double` + % relative scaling factor for determining the convergence tolerance of the local + % update solver subroutine based on the current error measure, defaults to + % :code:`1e-7`. + % + % trunc : :class:`struct` + % truncation method for local 2-site update, see :meth:`.Tensor.tsvd` for details on + % truncation options. + % + % alg_eigs : :class:`.KrylovSchur` or :class:`.Arnoldi` + % algorithm used for the eigsolver subroutines, defaults to + % :code:`Arnoldi('MaxIter', 100, 'KrylovDim', 20)`. + + properties + tol = 1e-10 + miniter = 5 + maxiter = 100 + verbosity = Verbosity.iter + doplot = false + + which = 'largestabs' + + dynamical_tols = true + tol_min = 1e-12 + tol_max = 1e-6 + eigs_tolfactor = 1e-7 + + trunc = {'TruncDim', 10} + + doSave = false + saveIterations = false + saveMethod = 'full' + name = 'IDmrg2' + + alg_eigs = Arnoldi('MaxIter', 100, 'KrylovDim', 20) + end + + properties (Access = private) + % TODO: alg_canonical, alg_environments? cf Vumps + progressfig + end + + methods + function alg = IDmrg2(kwargs) + arguments + kwargs.?IDmrg2 + end + + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + alg.(field{1}) = kwargs.(field{1}); + end + end + + if ~isfield('alg_eigs', kwargs) + alg.alg_eigs.tol = sqrt(alg.tol_min * alg.tol_max); + alg.alg_eigs.verbosity = alg.verbosity - 2; + end + end + + function [mps, lambda, GL, GR] = fixedpoint(alg, mpo, mps) + % Find the fixed point MPS of an infinite MPO, given an initial guess. + % + % Usage + % ----- + % :code:`[mps, lambda, GL, GR] = fixedpoint(alg, mpo, mps)` + % + % Arguments + % --------- + % alg : :class:`.IDmrg2` + % IDMRG2 algorithm. + % + % mpo : :class:`.InfMpo` + % matrix product operator. + % + % mps : :class:`.UniformMps` + % initial guess for MPS fixed point. + % + % Returns + % ------- + % mps : :class:`.UniformMps` + % MPS fixed point. + % + % lambda : :class:`double` + % eigenvalue. + % + % GL : :class:`cell` of :class:`.MpsTensor` + % left environment tensors. + % + % GR : :class:`cell` of :class:`.MpsTensor` + % right environment tensors. + + if period(mpo) ~= period(mps) + error('idmrg2:argerror', ... + 'periodicity of mpo (%d) should be equal to that of the mps (%d)', ... + period(mpo), period(mps)); + end + if period(mps) < 2 + error('idmrg2:argerror', ... + 'IDmrg2 is only defined for periodicity > 1'); + end + + t_total = tic; + disp_init(alg); + + [GL, GR] = environments(mpo, mps, mps); + + for iter = 1:alg.maxiter + t_iter = tic; + + C_ = mps.C{end}; + kwargs = {}; + lambdas = zeros(1, period(mps)); + + % sweep from left to right + for pos = 1:period(mps)-1 + AC2 = computeAC2(mps, 1, pos); + H = AC2_hamiltonian(mpo, mps, GL, GR, pos); + [AC2, lambdas(pos)] = ... + eigsolve(alg.alg_eigs, @(x) H{1}.apply(x), AC2, 1, alg.which); + [mps.AL{pos}.var, C, mps.AR{pos+1}.var] = ... + tsvd(AC2.var, [1 2], [3 4], alg.trunc{:}); + + mps.C{pos} = normalize(C); + mps.AC{pos + 1} = computeAC(mps, 1, pos + 1, 'R'); + + TL = transfermatrix(mpo, mps, mps, pos, 'Type', 'LL'); + GL{pos + 1} = apply(TL, GL{pos}); + TR = transfermatrix(mpo, mps, mps, pos + 1, 'Type', 'RR'); + GR{pos + 1} = apply(TR.', GR{mod1(pos + 2, length(GR))}); + end + + % update edge + AC2 = contract(mps.AC{end}, [-1 -2 1], inv(mps.C{end}), [1 2], ... + mps.AL{1}, [2 -3 3], mps.C{1}, [3 -4], 'Rank', [2 2]); + H = AC2_hamiltonian(mpo, mps, GL, GR, period(mps)); + [AC2, lambdas(end)] = ... + eigsolve(alg.alg_eigs, @(x) H{1}.apply(x), AC2, 1, alg.which); + + [mps.AL{end}.var, C, mps.AR{1}.var] = ... + tsvd(AC2, [1 2], [3 4], alg.trunc{:}); + + mps.C{end} = normalize(C); + mps.AC{end} = computeAC(mps, 1, period(mps), 'L'); + mps.AC{1} = computeAC(mps, 1, 1, 'R'); + mps.AL{1} = multiplyright(mps.AC{1}, inv(mps.C{1})); + + TL = transfermatrix(mpo, mps, mps, period(mps), 'Type', 'LL'); + GL{1} = apply(TL, GL{end}); + TR = transfermatrix(mpo, mps, mps, 1, 'Type', 'RR'); + GR{1} = apply(TR.', GR{2}); + + % sweep from right to left + for pos = period(mps)-1:-1:1 + AC2 = computeAC2(mps, 1, pos, 'L'); + H = AC2_hamiltonian(mpo, mps, GL, GR, pos); + [AC2, lambdas(pos)] = ... + eigsolve(alg.alg_eigs, @(x) H{1}.apply(x), AC2, 1, alg.which); + + [mps.AL{pos}.var, C, mps.AR{pos + 1}.var] = ... + tsvd(AC2, [1 2], [3 4], alg.trunc{:}); + mps.C{pos} = normalize(C); + mps.AC{pos} = computeAC(mps, 1, pos, 'L'); + mps.AC{pos + 1} = computeAC(mps, 1, pos + 1, 'R'); + + TL = transfermatrix(mpo, mps, mps, pos, 'Type', 'LL'); + GL{pos + 1} = apply(TL, GL{pos}); + TR = transfermatrix(mpo, mps, mps, pos + 1, 'Type', 'RR'); + GR{pos + 1} = apply(TR.', GR{mod1(pos + 2, period(mps))}); + end + + % update edge + AC2 = contract(mps.C{end-1}, [-1 1], mps.AR{end}, [1 -2 2], ... + inv(mps.C{end}), [2 3], mps.AC{1}, [3 -3 -4], 'Rank', [2 2]); + H = AC2_hamiltonian(mpo, mps, GL, GR, period(mps)); + [AC2, lambdas(1)] = ... + eigsolve(alg.alg_eigs, @(x) H{1}.apply(x), AC2, 1, alg.which); + + [mps.AL{end}.var, C, mps.AR{1}.var] = ... + tsvd(AC2, [1 2], [3 4], alg.trunc{:}); + mps.C{end} = normalize(C); + mps.AC{1} = computeAC(mps, 1, 1, 'R'); + + mps.AR{end} = multiplyleft(multiplyright(mps.AL{end}, mps.C{end}), ... + inv(mps.C{end - 1})); + + + TL = transfermatrix(mpo, mps, mps, period(mps), 'Type', 'LL'); + GL{1} = apply(TL, GL{end}); + TR = transfermatrix(mpo, mps, mps, 1, 'Type', 'RR'); + GR{1} = apply(TR.', GR{2}); + + % error measure + infspace = infimum(space(C_, 1), space(mps.C{end}, 1)); + e1 = C_.eye(space(mps.C{end}, 1), infspace); + e2 = C_.eye(space(C_, 1), infspace); + + eta = distance(e2' * C_ * e2, e1' * mps.C{end} * e1); + lambda = prod(sqrt(lambdas)); + + if iter > alg.miniter && eta < alg.tol + mps = canonicalize(mps, 'Order', 'rl'); + disp_conv(alg, iter, lambda, eta, toc(t_total)); + return + end + + alg = updatetols(alg, iter, eta); + plot(alg, iter, mps, eta); + disp_iter(alg, iter, lambda, eta, toc(t_iter)); + + if alg.doSave && mod(iter, alg.saveIterations) == 0 + save(alg, mps, lambdas); + end + end + + mps = canonicalize(mps, 'Order', 'rl'); + disp_maxiter(alg, iter, lambda, eta, toc(t_total)); + end + end + + + methods + function [GL, GR, lambda] = environments(alg, mpo, mps, GL, GR) + arguments + alg + mpo + mps + GL = cell(1, period(mps)) + GR = cell(1, period(mps)) + end + + [GL, GR, lambda] = environments(mpo, mps, mps, GL, GR); + end + + function alg = updatetols(alg, iter, eta) + if alg.dynamical_tols + alg.alg_eigs.tol = between(alg.tol_min, eta * alg.eigs_tolfactor, ... + alg.tol_max / iter); + + if alg.verbosity > Verbosity.iter + fprintf('Updated subalgorithm tolerances: (%e)\n', ... + alg.alg_eigs.tol); + end + end + end + end + + %% Display + methods (Access = private) + function plot(alg, iter, mps, eta) + if ~alg.doplot, return; end + persistent axhistory axspectrum + + D = depth(mps); + W = period(mps); + + if isempty(alg.progressfig) || ~isvalid(alg.progressfig) || iter == 1 + alg.progressfig = figure('Name', 'Vumps'); + axhistory = subplot(D + 1, W, 1:W); + axspectrum = gobjects(D, W); + for d = 1:D, for w = 1:W + axspectrum(d, w) = subplot(D + 1, W, w + d * W); + end, end + linkaxes(axspectrum, 'y'); + end + + + if isempty(axhistory.Children) + semilogy(axhistory, iter, eta, '.', 'Color', colors(1), ... + 'DisplayName', 'Errors', 'MarkerSize', 10); + hold on + ylim(axhistory, [alg.tol / 5, max([1 eta])]); + yline(axhistory, alg.tol, '-', 'Convergence', ... + 'LineWidth', 2); + hold off + else + axhistory.Children(end).XData(end+1) = iter; + axhistory.Children(end).YData(end+1) = eta; + end + + plot_entanglementspectrum(mps, 1:period(mps), axspectrum); + drawnow + end + + function disp_init(alg) + if alg.verbosity < Verbosity.conv, return; end + fprintf('---- IDmrg2 ----\n'); + end + + function disp_iter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.iter, return; end + + s = settings; + if abs(imag(lambda)) < eps(lambda)^(3/4) * abs(lambda) + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('IDmrg2 %2d:\tE = %-0.4f\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), eta, time2str(t)); + otherwise + fprintf('IDmrg2 %4d:\tE = %-0.15f\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), eta, time2str(t, 's')); + + end + else + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('IDmrg2 %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('IDmrg2 %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + end + end + + function disp_conv(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.conv, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('IDmrg converged %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('IDmrg converged %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + fprintf('---------------\n'); + end + + function disp_maxiter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.warn, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('IDmrg max iterations %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('IDmrg max iterations %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + fprintf('---------------\n'); + end + + function save(alg, mps, lambdas) + fileName = alg.name; + + fileData = struct; + fileData.mps = mps; + fileData.lambdas = lambdas; + + % save + %if exist(fileName,'file') + % old_file=load(fileName); + % fileName_temp=[fileName(1:end-4),'_temp.mat']; + % save(fileName_temp, '-struct', 'old_file', '-v7.3'); + % saved_temp=1; + %else + % saved_temp=0; + %end + + save(fileName, '-struct', 'fileData', '-v7.3'); + + %if saved_temp + % delete(fileName_temp); + %end + + + end + end +end + diff --git a/src/algorithms/QPAnsatz.m b/src/algorithms/QPAnsatz.m new file mode 100644 index 0000000..056cc0f --- /dev/null +++ b/src/algorithms/QPAnsatz.m @@ -0,0 +1,135 @@ +classdef QPAnsatz + % `Quasi-Particle excitation ansatz `_. + % + % Properties + % ---------- + % alg_eigs : :class:`.KrylovSchur` or :class:`.Arnoldi` + % algorithm used for the eigsolver subroutines, defaults to + % :code:`Arnoldi('MaxIter', 100, 'KrylovDim', 30, 'Tol', 1e-8)`. + % + % alg_environments : :class:`.struct` + % algorithm used for the environment subroutines (see :meth:`.AbstractTensor.linsolve` + % for details), defaults to :code:`struct('Tol', 1e-10, 'Algorithm', 'bicgstabl')`. + % + % howmany : :class:`int` + % number of excitations to compute. + % + % which : :class:`char` + % eigenvalue selector (passed as the :code:`sigma` argument to :func:`.eigsolve`), + % defaults to :code:`'largestabs'`. + + properties + alg_eigs = Arnoldi('MaxIter', 100, 'KrylovDim', 30, 'Tol', 1e-8, 'Verbosity', Verbosity.diagnostics) + alg_environments = struct('Tol', 1e-10, 'Algorithm', 'bicgstabl'); + howmany = 1 + which = 'smallestreal' + end + + methods + function alg = QPAnsatz(kwargs) + arguments + kwargs.?QPAnsatz + end + + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + if isstruct(kwargs.(field{1})) + for field2 = fieldnames(kwargs.(field{1})).' + alg.(field{1}).(field2{1}) = kwargs.(field{1}).(field2{1}); + end + else + alg.(field{1}) = kwargs.(field{1}); + end + end + end + end + + function [qp, mu] = excitations(alg, mpo, qp) + % Find excitations + % + % Usage + % ----- + % :code:`[qp, mu] = excitations(alg, mpo, qp)` + % + % Arguments + % --------- + % alg : :class:`.QPAnsatz` + % Quasi-particle ansatz algorithm. + % + % mpo : :class:`.InfMpo` + % matrix product operator. + % + % mps : :class:`.UniformMps` + % initial guess for MPS fixed point. + % + % Returns + % ------- + % qp : :class:`.InfQP` + % vector of quasiparticle states. + % + % mu : :class:`double` + % vector of corresponding eigenvalues. + + if period(mpo) ~= period(qp) + error('QPAnsatz:argerror', ... + 'periodicity of mpo (%d) should be equal to that of the mps (%d)', ... + period(mpo), period(qp)); + end + + + % Renormalization + [GL, GR, lambda] = environments(mpo, qp.mpsleft, qp.mpsleft); + + for i = period(mpo):-1:1 + T = AC_hamiltonian(mpo, qp.mpsleft, GL, GR, i); + offsetL(i) = dot(qp.mpsleft.AC{i}, apply(T{1}, qp.mpsleft.AC{i})); + end + [GL, GR, lambda] = environments(mpo, qp.mpsright, qp.mpsright); + + for i = period(mpo):-1:1 + T = AC_hamiltonian(mpo, qp.mpsright, GL, GR, i); + offsetR(i) = dot(qp.mpsright.AC{i}, apply(T{1}, qp.mpsright.AC{i})); + end + offset = (offsetL + offsetR) / 2; + + GL = leftenvironment(mpo, qp.mpsleft, qp.mpsleft); + + % Algorithm + H_effective = @(qp) updateX(alg, mpo, qp, GL, GR, offset); + [qp, mu] = eigsolve(alg.alg_eigs, H_effective, qp, alg.howmany, alg.which); + + for i = alg.howmany:-1:2 + qp(i).X = X(i); + qp(i).B = computeB(qp(i)); + end + end + + function qp = updateX(alg, mpo, qp, GL, GR, offset) + qp.B = computeB(qp); + B = qp.B; + + H_c = B_hamiltonian(mpo, qp, GL, GR, 'Type', 'center'); + for i = period(qp):-1:1 + B{i} = MpsTensor(apply(H_c{i}, B{i}), 1); + end + + H_l = B_hamiltonian(mpo, qp, GL, GR, 'Type', 'left'); + for i = 1:period(qp) + B{i} = B{i} + repartition(apply(H_l{i}, qp.AR{i}), rank(B{i})); + end + + H_r = B_hamiltonian(mpo, qp, GL, GR, 'Type', 'right'); + for i = 1:period(qp) + B{i} = B{i} + repartition(apply(H_r{i}, qp.AL{i}), rank(B{i})); + end + + for i = 1:period(qp) + qp.B{i} = B{i} - qp.B{i} * offset(i); + end + + qp.X = computeX(qp); + end + end +end + diff --git a/src/algorithms/Vomps.m b/src/algorithms/Vomps.m new file mode 100644 index 0000000..9880ec8 --- /dev/null +++ b/src/algorithms/Vomps.m @@ -0,0 +1,359 @@ +classdef Vomps + % `Fixed point algorithm for maximizing overlap `_. + % + % Properties + % ---------- + % tol : :class:`double` + % tolerance for convergence criterion, defaults to :code:`1e-10`. + % + % miniter : :class:`int` + % minimum number of iteration, defaults to 5. + % + % maxiter : :class:`int` + % maximum number of iteration, defaults to 100. + % + % verbosity : :class:`.Verbosity` + % verbosity level of the algorithm, defaults to :code:`Verbosity.iter`. + % + % which : :class:`char` + % eigenvalue selector (passed as the :code:`sigma` argument to :func:`.eigsolve`), + % defaults to :code:`'largestabs'`. + % + % dynamical_tols : :class:`logical` + % indicate whether or not to use a dynamical tolerance scaling for the algorithm's + % subroutines based on the current error measure, defaults to :code:`true`. + % + % tol_min : :class:`double` + % smallest allowed convergence tolerance for soubroutines, defaults to :code:`1e-12`. + % + % tol_max : :class:`double` + % highest allowed convergence tolerance for soubroutines, defaults to :code:`1e-6`. + % + % eigs_tolfactor : :class:`double` + % relative scaling factor for determining the convergence tolerance of the local + % update solver subroutine based on the current error measure, defaults to + % :code:`1e-4`. + % + % canonical_tolfactor : :class:`double` + % relative scaling factor for determining the convergence tolerance of the + % canonicalization subroutine based on the current error measure, defaults to + % :code:`1e-8`. + % + % environments_tolfactor : :class:`double` + % relative scaling factor for determining the convergence tolerance of the environment + % solver subroutine based on the current error measure, defaults to :code:`1e-4`. + % + % multiAC : :class:`char` + % execution style for the local `AC` updates for a multi-site unit cell, options are: + % + % - :code:`'parallel'`: (default) update all `AC` tensors simultaneously. + % - :code:`'sequential'`: update one `AC` tensor at a time, sweeping through the unit + % cell. + % + % dynamical_multiAC : :class:`logical` + % automatically switch from :code:`'sequential'` to :code:`'parallel'` if the error + % measure becomes small enough, defaults to :code:`false`. + % + % tol_multiAC : :class:`char` + % tolerance for automatically switching from :code:`'sequential'` to + % :code:`'parallel'` if the error measure falls below this value, defaults to + % :code:`Inf`. + + %% Options + properties + tol = 1e-5 + miniter = 2 + maxiter = 10 + verbosity = Verbosity.iter + which = 'largestabs' + + dynamical_tols = true + tol_min = 1e-12 + tol_max = 1e-6 + eigs_tolfactor = 1e-4 + canonical_tolfactor = 1e-8 + environments_tolfactor = 1e-4 + + multiAC = 'parallel' + dynamical_multiAC = false; + tol_multiAC = Inf + end + + properties (Access = private) + alg_canonical = struct('Method', 'polar') + alg_environments = struct + end + + methods + function v = Vomps(kwargs) + arguments + kwargs.?Vumps + end + + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + v.(field{1}) = kwargs.(field{1}); + end + end + + if ~isfield('alg_canonical', kwargs) + v.alg_canonical.Tol = sqrt(v.tol_min * v.tol_max); + v.alg_canonical.Verbosity = v.verbosity - 2; + end + + if ~isfield('alg_environments', kwargs) + v.alg_environments.Tol = sqrt(v.tol_min * v.tol_max); + v.alg_environments.Verbosity = v.verbosity - 2; + end + end + + function [mps2, GL, GR] = approximate(alg, mpo, mps1, mps2) + % Approximate the product of an MPS and an MPO as an MPS. + % + % Usage + % ----- + % :code:`[mps2, GL, GR] = approximate(alg, mpo, mps1, mps2)` + % + % Arguments + % --------- + % alg : :class:`.Vumps` + % VUMPS algorithm. + % + % mpo : :class:`.InfMpo` + % matrix product operator. + % + % mps1 : :class:`.UniformMps` + % MPS to which the MPO is applied. + % + % mps2 : :class:`.UniformMps` + % initial guess for MPS approximation. + % + % Returns + % ------- + % mps2 : :class:`.UniformMps` + % MPS approximation, such that :code:`mps2` :math:`\approx` + % :code:`mpo * mps1`. + % + % GL : :class:`cell` of :class:`.MpsTensor` + % left environment tensors. + % + % GR : :class:`cell` of :class:`.MpsTensor` + % right environment tensors. + + if period(mpo) ~= period(mps1) || period(mpo) ~= period(mps2) + error('vumps:argerror', ... + 'periodicitys should match: mpo (%d), mps1 (%d), mps2(%d)', ... + period(mpo), period(mps1), period(mps2)); + end + + t_total = tic; + disp_init(alg); + + mps2 = canonicalize(mps2); + [GL, GR] = environments(alg, mpo, mps1, mps2); + + for iter = 1:alg.maxiter + t_iter = tic; + + AC = updateAC(alg, iter, mpo, mps1, GL, GR); + C = updateC (alg, iter, mpo, mps1, GL, GR); + mps2 = updatemps(alg, iter, mps2, AC, C); + + [GL, GR, lambda] = environments(alg, mpo, mps1, mps2, GL, GR); + eta = convergence(alg, mpo, mps1, mps2, GL, GR); + + if iter > alg.miniter && eta < alg.tol + disp_conv(alg, iter, lambda, eta, toc(t_total)); + return + end + alg = updatetols(alg, iter, eta); + disp_iter(alg, iter, lambda, eta, toc(t_iter)); + end + + disp_maxiter(alg, iter, lambda, eta, toc(t_total)); + end + end + + %% Subroutines + methods + function AC = updateAC(alg, iter, mpo, mps, GL, GR) + if strcmp(alg.multiAC, 'sequential') + sites = mod1(iter, period(mps)); + else + sites = 1:period(mps); + end + + H_AC = AC_hamiltonian(mpo, mps, GL, GR, sites); + ACs = arrayfun(@(x) x.AC(sites), mps, 'UniformOutput', false); + AC = vertcat(ACs{:}); + for i = length(sites):-1:1 + for d = 1:depth(mpo) + AC{d, i} = H_AC{i}(d).apply(AC{prev(d, depth(mpo)), i}); + end + end + end + + function C = updateC(alg, iter, mpo, mps, GL, GR) + if strcmp(alg.multiAC, 'sequential') + sites = mod1(iter, period(mps)); + else + sites = 1:period(mps); + end + + H_C = C_hamiltonian(mpo, mps, GL, GR, sites); + Cs = arrayfun(@(x) x.C(sites), mps, 'UniformOutput', false); + C = vertcat(Cs{:}); + for i = length(sites):-1:1 + for d = 1:depth(mpo) + C{d, i} = H_C{i}(d).apply(C{prev(d, depth(mpo)), i}); + end + end + end + + function mps = updatemps(alg, iter, mps, AC, C) + if strcmp(alg.multiAC, 'sequential') + sites = mod1(iter, period(mps)); + else + sites = 1:period(mps); + end + + for d = size(AC, 1):-1:1 + for i = length(AC):-1:1 + [Q_AC, ~] = leftorth(AC{d, i}, 'polar'); + [Q_C, ~] = leftorth(C{d, i}, 1, 2, 'polar'); + mps(d).AL{sites(i)} = multiplyright(Q_AC, Q_C'); + end + end + + kwargs = namedargs2cell(alg.alg_canonical); + mps = canonicalize(mps, kwargs{:}); + end + + function [GL, GR, lambda] = environments(alg, mpo, mps1, mps2, GL, GR) + arguments + alg + mpo + mps1 + mps2 + GL = cell(1, period(mps1)) + GR = cell(1, period(mps1)) + end + + kwargs = namedargs2cell(alg.alg_environments); + [GL, GR, lambda] = environments(mpo, mps1, mps2, GL, GR, ... + kwargs{:}); + end + + function eta = convergence(alg, mpo, mps1, mps2, GL, GR) + H_AC = AC_hamiltonian(mpo, mps1, GL, GR); + H_C = C_hamiltonian(mpo, mps1, GL, GR); + eta = zeros(1, period(mps1)); + for w = 1:period(mps1) + AC_ = apply(H_AC{w}, mps1.AC{w}); + lambda_AC = dot(AC_, mps2.AC{w}); + AC_ = normalize(AC_ ./ lambda_AC); + + ww = prev(w, period(mps1)); + C_ = apply(H_C{ww}, mps1.C{ww}); + lambda_C = dot(C_, mps2.C{ww}); + C_ = normalize(C_ ./ lambda_C); + + eta(w) = distance(AC_ , ... + repartition(multiplyleft(mps2.AR{w}, C_), rank(AC_))); + end + eta = max(eta, [], 'all'); + end + end + + %% Option handling + methods + function alg = updatetols(alg, iter, eta) + if alg.dynamical_tols + alg.alg_canonical.Tol = between(alg.tol_min, ... + eta * alg.canonical_tolfactor, alg.tol_max / iter); + alg.alg_environments.Tol = between(alg.tol_min, ... + eta * alg.environments_tolfactor, alg.tol_max / iter); + + if alg.verbosity > Verbosity.iter + fprintf('Updated subalgorithm tolerances: (%e,\t%e,\t%e)\n', ... + alg.alg_eigs.Tol, alg.alg_canonical.Tol, alg.alg_environments.Tol); + end + end + + if alg.dynamical_multiAC + if eta < alg.tol_multiAC + alg.multiAC = 'sequential'; + else + alg.multiAC = 'parallel'; + end + end + end + end + + %% Display + methods (Access = private) + function disp_init(alg) + if alg.verbosity < Verbosity.conv, return; end + fprintf('---- VOMPS ----\n'); + end + + function disp_iter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.iter, return; end + + s = settings; + if abs(imag(lambda)) < eps(lambda)^(3/4) * abs(lambda) + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vomps %2d:\tE = %-0.4f\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), eta, time2str(t)); + otherwise + fprintf('Vomps %4d:\tE = %-0.15f\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), eta, time2str(t, 's')); + + end + else + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vomps %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('Vomps %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + end + end + + function disp_conv(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.conv, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vomps converged %2d:\terror = %0.1e\t(%s)\n', ... + iter, eta, time2str(t)); + otherwise + fprintf('Vomps converged %4d:\terror = %0.4e\t(%s)\n', ... + iter, eta, time2str(t)); + + end + fprintf('---------------\n'); + end + + function disp_maxiter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.warn, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vomps max iterations %2d:\terror = %0.1e\t(%s)\n', ... + iter, eta, time2str(t)); + otherwise + fprintf('Vomps max iterations %4d:\terror = %0.4e\t(%s)\n', ... + iter, eta, time2str(t)); + + end + fprintf('---------------\n'); + end + end +end + diff --git a/src/algorithms/Vumps.m b/src/algorithms/Vumps.m new file mode 100644 index 0000000..a936b22 --- /dev/null +++ b/src/algorithms/Vumps.m @@ -0,0 +1,484 @@ +classdef Vumps < handle + % `Variational fixed point algorithm for uniform matrix product states `_. + % + % Properties + % ---------- + % tol : :class:`double` + % tolerance for convergence criterion, defaults to :code:`1e-10`. + % + % miniter : :class:`int` + % minimum number of iteration, defaults to :code:`5`. + % + % maxiter : :class:`int` + % maximum number of iteration, defaults to code:`100`. + % + % verbosity : :class:`.Verbosity` + % verbosity level of the algorithm, defaults to :code:`Verbosity.iter`. + % + % doplot : :class:`logical` + % plot progress, defaults to :code:`false`. + % + % which : :class:`char` + % eigenvalue selector (passed as the :code:`sigma` argument to :func:`.eigsolve`), + % defaults to :code:`'largestabs'`. + % + % dynamical_tols : :class:`logical` + % indicate whether or not to use a dynamical tolerance scaling for the algorithm's + % subroutines based on the current error measure, defaults to :code:`true`. + % + % tol_min : :class:`double` + % smallest allowed convergence tolerance for soubroutines, defaults to :code:`1e-12`. + % + % tol_max : :class:`double` + % highest allowed convergence tolerance for soubroutines, defaults to :code:`1e-10`. + % + % eigs_tolfactor : :class:`double` + % relative scaling factor for determining the convergence tolerance of the local + % update solver subroutine based on the current error measure, defaults to + % :code:`1e-6`. + % + % canonical_tolfactor : :class:`double` + % relative scaling factor for determining the convergence tolerance of the + % canonicalization subroutine based on the current error measure, defaults to + % :code:`1e-8`. + % + % environments_tolfactor : :class:`double` + % relative scaling factor for determining the convergence tolerance of the environment + % solver subroutine based on the current error measure, defaults to :code:`1e-6`. + % + % multiAC : :class:`char` + % execution style for the local `AC` updates for a multi-site unit cell, options are: + % + % - :code:`'parallel'`: (default) update all `AC` tensors simultaneously. + % - :code:`'sequential'`: update one `AC` tensor at a time, sweeping through the unit + % cell. + % + % dynamical_multiAC : :class:`logical` + % automatically switch from :code:`'sequential'` to :code:`'parallel'` if the error + % measure becomes small enough, defaults to :code:`false`. + % + % tol_multiAC : :class:`char` + % tolerance for automatically switching from :code:`'sequential'` to + % :code:`'parallel'` if the error measure falls below this value, defaults to + % :code:`Inf`. + % + % alg_eigs : :class:`.KrylovSchur` or :class:`.Arnoldi` + % algorithm used for the eigsolver subroutines, defaults to + % :code:`Arnoldi('MaxIter', 100, 'KrylovDim', 20)`. + + %% Options + properties + tol = 1e-10 + miniter = 5 + maxiter = 100 + verbosity = Verbosity.iter + doplot = false + which = 'largestabs' + + dynamical_tols = true + tol_min = 1e-12 + tol_max = 1e-10 + eigs_tolfactor = 1e-6 + canonical_tolfactor = 1e-8 + environments_tolfactor = 1e-6 + + multiAC = 'parallel' + dynamical_multiAC = false; + tol_multiAC = Inf + + doSave = false + saveIterations = 1 + saveMethod = 'full' + name = 'VUMPS' + + alg_eigs = Arnoldi('MaxIter', 100, 'KrylovDim', 20) + end + + properties (Access = private) + alg_canonical = struct('Method', 'polar') + alg_environments = struct + + progressfig + end + + + %% + methods + function alg = Vumps(kwargs) + arguments + kwargs.?Vumps + end + + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + alg.(field{1}) = kwargs.(field{1}); + end + end + + if ~isfield('alg_eigs', kwargs) + alg.alg_eigs.tol = sqrt(alg.tol_min * alg.tol_max); + alg.alg_eigs.verbosity = alg.verbosity - 2; + end + + if ~isfield('alg_canonical', kwargs) + alg.alg_canonical.tol = sqrt(alg.tol_min * alg.tol_max); + alg.alg_canonical.verbosity = alg.verbosity - 2; + end + + if ~isfield('alg_environments', kwargs) + alg.alg_environments.tol = sqrt(alg.tol_min * alg.tol_max); + alg.alg_environments.verbosity = alg.verbosity - 2; + end + + if isfield('verbosity', kwargs) + alg.alg_eigs.verbosity = alg.verbosity - 2; + alg.alg_environments.verbosity = alg.verbosity - 2; + end + end + + function [mps, lambda, GL, GR, eta] = fixedpoint(alg, mpo, mps) + % Find the fixed point MPS of an infinite MPO, given an initial guess. + % + % Usage + % ----- + % :code:`[mps, lambda, GL, GR, eta] = fixedpoint(alg, mpo, mps)` + % + % Arguments + % --------- + % alg : :class:`.Vumps` + % VUMPS algorithm. + % + % mpo : :class:`.InfMpo` + % matrix product operator. + % + % mps : :class:`.UniformMps` + % initial guess for MPS fixed point. + % + % Returns + % ------- + % mps : :class:`.UniformMps` + % MPS fixed point. + % + % lambda : :class:`double` + % eigenvalue. + % + % GL : :class:`cell` of :class:`.MpsTensor` + % left environment tensors. + % + % GR : :class:`cell` of :class:`.MpsTensor` + % right environment tensors. + % + % eta : :class:`double` + % final error measure at convergence. + + if period(mpo) ~= period(mps) + error('vumps:argerror', ... + 'periodicity of mpo (%d) should be equal to that of the mps (%d)', ... + period(mpo), period(mps)); + end + + t_total = tic; + disp_init(alg); + + mps = canonicalize(mps); + [GL, GR] = environments(alg, mpo, mps); + + for iter = 1:alg.maxiter + t_iter = tic; + + AC = updateAC(alg, iter, mpo, mps, GL, GR); + C = updateC (alg, iter, mpo, mps, GL, GR); + mps = updatemps(alg, iter, mps, AC, C); + + [GL, GR, lambda] = environments(alg, mpo, mps, GL, GR); + eta = convergence(alg, mpo, mps, GL, GR); + + if iter > alg.miniter && eta < alg.tol + disp_conv(alg, iter, lambda, eta, toc(t_total)); + return + end + alg = updatetols(alg, iter, eta); + plot(alg, iter, mps, eta); + disp_iter(alg, iter, lambda, eta, toc(t_iter)); + + if alg.doSave && mod(iter, alg.saveIterations) == 0 + save_iteration(alg, mps, lambda, iter, eta, toc(t_total)); + end + end + disp_maxiter(alg, iter, lambda, eta, toc(t_total)); + end + end + + + %% Subroutines + methods + function AC = updateAC(alg, iter, mpo, mps, GL, GR) + if strcmp(alg.multiAC, 'sequential') + sites = mod1(iter, period(mps)); + else + sites = 1:period(mps); + end + H_AC = AC_hamiltonian(mpo, mps, GL, GR, sites); + ACs = arrayfun(@(x) x.AC(sites), mps, 'UniformOutput', false); + AC = vertcat(ACs{:}); + for i = length(sites):-1:1 + if alg.verbosity >= Verbosity.detail + fprintf('\nAC{%d} eigenvalue solver:\n------------------------\n', sites(i)); + end + [AC{1, i}, ~] = eigsolve(alg.alg_eigs, @(x) H_AC{i}.apply(x), AC{1, i}, ... + 1, alg.which); + for d = 2:depth(mpo) + AC{d, i} = H_AC{i}(d).apply(AC{d-1, i}); + end + end + end + + function C = updateC(alg, iter, mpo, mps, GL, GR) + if strcmp(alg.multiAC, 'sequential') + sites = mod1(iter, period(mps)); + else + sites = 1:period(mps); + end + + H_C = C_hamiltonian(mpo, mps, GL, GR, sites); + Cs = arrayfun(@(x) x.C(sites), mps, 'UniformOutput', false); + C = vertcat(Cs{:}); + for i = length(sites):-1:1 + if alg.verbosity >= Verbosity.detail + fprintf('\nC{%d} eigenvalue solver:\n-----------------------\n', sites(i)); + end + [C{1, i}, ~] = eigsolve(alg.alg_eigs, @(x) H_C{i}.apply(x), C{1, i}, 1, ... + alg.which); + for d = 2:depth(mpo) + C{d, i} = H_C{i}(d).apply(C{d-1, i}); + end + end + end + + function mps = updatemps(alg, iter, mps, AC, C) + if alg.verbosity >= Verbosity.detail + fprintf('\nCanonicalize:\n------------------\n'); + end + + if strcmp(alg.multiAC, 'sequential') + sites = mod1(iter, period(mps)); + else + sites = 1:period(mps); + end + + for d = size(AC, 1):-1:1 + for i = size(AC, 2):-1:1 + [Q_AC, ~] = leftorth(AC{d, i}, 'polar'); + [Q_C, ~] = leftorth(C{d, i}, 1, 2, 'polar'); + mps(d).AL{sites(i)} = multiplyright(Q_AC, Q_C'); + end + end + + kwargs = namedargs2cell(alg.alg_canonical); + mps = canonicalize(mps, kwargs{:}); + end + + function [GL, GR, lambda] = environments(alg, mpo, mps, GL, GR) + arguments + alg + mpo + mps + GL = cell(depth(mpo), period(mps)) + GR = cell(depth(mpo), period(mps)) + end + if alg.verbosity >= Verbosity.detail + fprintf('\nEnvironments:\n------------------\n'); + end + kwargs = namedargs2cell(alg.alg_environments); + D = depth(mpo); + lambda = zeros(D, 1); + for d = 1:D + dd = next(d, D); + [GL(d, :), GR(d, :), lambda(d)] = environments(mpo.slice(d), ... + mps(d), mps(dd), GL(d, :), GR(d, :), kwargs{:}); + end + lambda = prod(lambda); + end + + function eta = convergence(alg, mpo, mps, GL, GR) + % TODO: also implement galerkin error + H_AC = AC_hamiltonian(mpo, mps, GL, GR); + H_C = C_hamiltonian(mpo, mps, GL, GR); + eta = zeros(depth(mpo), period(mps)); + for d = 1:depth(mpo) + dd = next(d, depth(mpo)); + for w = 1:period(mps) + AC_ = apply(H_AC{w}(d), mps(d).AC{w}); + lambda_AC = dot(AC_, mps(dd).AC{w}); + AC_ = normalize(AC_ ./ lambda_AC); + + ww = prev(w, period(mps)); + C_ = apply(H_C{ww}(d), mps(d).C{ww}); + lambda_C = dot(C_, mps(dd).C{ww}); + C_ = normalize(C_ ./ lambda_C); + + eta(dd, w) = distance(AC_ , ... + repartition(multiplyleft(mps(dd).AR{w}, C_), rank(AC_))); + end + end + eta = max(eta, [], 'all'); + end + end + + + %% Option handling + methods + function alg = updatetols(alg, iter, eta) + if alg.dynamical_tols + alg.alg_eigs.tol = between(alg.tol_min, eta * alg.eigs_tolfactor / iter, ... + alg.tol_max); + alg.alg_canonical.Tol = between(alg.tol_min, ... + eta * alg.canonical_tolfactor / iter, alg.tol_max); + alg.alg_environments.Tol = between(alg.tol_min, ... + eta * alg.environments_tolfactor / iter, alg.tol_max); + + if alg.verbosity > Verbosity.iter + fprintf('Updated subalgorithm tolerances: (%e,\t%e,\t%e)\n', ... + alg.alg_eigs.tol, alg.alg_canonical.Tol, alg.alg_environments.Tol); + end + end + + if alg.dynamical_multiAC + if eta < alg.tol_multiAC + alg.multiAC = 'sequential'; + else + alg.multiAC = 'parallel'; + end + end + end + end + + %% Display + methods (Access = private) + function plot(alg, iter, mps, eta) + if ~alg.doplot, return; end + persistent axhistory axspectrum + + D = depth(mps); + W = period(mps); + + if isempty(alg.progressfig) || ~isvalid(alg.progressfig) || iter == 1 + alg.progressfig = figure('Name', 'Vumps'); + axhistory = subplot(D + 1, W, 1:W); + axspectrum = gobjects(D, W); + for d = 1:D, for w = 1:W + axspectrum(d, w) = subplot(D + 1, W, w + d * W); + end, end + linkaxes(axspectrum, 'y'); + end + + + if isempty(axhistory.Children) + semilogy(axhistory, iter, eta, '.', 'Color', colors(1), ... + 'DisplayName', 'Errors', 'MarkerSize', 10); + hold on + ylim(axhistory, [alg.tol / 5, max([1 eta])]); + yline(axhistory, alg.tol, '-', 'Convergence', ... + 'LineWidth', 2); + hold off + else + axhistory.Children(end).XData(end+1) = iter; + axhistory.Children(end).YData(end+1) = eta; + end + + plot_entanglementspectrum(mps, 1:D, 1:W, axspectrum); + drawnow + end + + function disp_init(alg) + if alg.verbosity < Verbosity.conv, return; end + fprintf('---- VUMPS ----\n'); + end + + function disp_iter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.iter, return; end + + s = settings; + if abs(imag(lambda)) < eps(lambda)^(3/4) * abs(lambda) + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vumps %2d:\tE = %-0.4f\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), eta, time2str(t)); + otherwise + fprintf('Vumps %4d:\tE = %-0.15f\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), eta, time2str(t, 's')); + + end + else + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vumps %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('Vumps %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + end + end + + function disp_conv(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.conv, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vumps converged %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('Vumps converged %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + fprintf('---------------\n'); + end + + function disp_maxiter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.warn, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vumps max iterations %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('Vumps max iterations %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + fprintf('---------------\n'); + end + + function save_iteration(alg, mps, lambda, iter, eta, t) + fileName = alg.name; + + fileData = struct; + fileData.mps = mps; + fileData.lambda = lambda; + fileData.iteration = iter; + fileData.eta = eta; + fileData.time = t; + % save + + %if exist(fileName,'file') + % old_file=load(fileName); + % fileName_temp=[fileName(1:end-4),'_temp.mat']; + % save(fileName_temp, '-struct', 'old_file', '-v7.3'); + % saved_temp=1; + %else + % saved_temp=0; + %end + + save(fileName, '-struct', 'fileData', '-v7.3'); + + %if saved_temp + % delete(fileName_temp); + %end + end + end +end + diff --git a/src/algorithms/Vumps2.m b/src/algorithms/Vumps2.m new file mode 100644 index 0000000..f655920 --- /dev/null +++ b/src/algorithms/Vumps2.m @@ -0,0 +1,369 @@ +classdef Vumps2 < handle + % Variational fixed point algorithm for uniform matrix product states. + + %% Options + properties + tol = 1e-10 + miniter = 5 + maxiter = 100 + verbosity = Verbosity.iter + doplot = false + which = 'largestabs' + + dynamical_tols = true + tol_min = 1e-12 + tol_max = 1e-7 + eigs_tolfactor = 1e-6 + canonical_tolfactor = 1e-8 + environments_tolfactor = 1e-6 + + trunc = {'TruncTotalDim', 100} + notrunc = false + + multiAC {mustBeMember(multiAC, {'sequential'})} = 'sequential' + dynamical_multiAC = false; + tol_multiAC = Inf + + doSave = false + saveIterations = 1 + saveMethod = 'full' + name = 'VUMPS' + + alg_eigs = Arnoldi('MaxIter', 100, 'KrylovDim', 20) + end + + properties (Access = private) + alg_canonical = struct('Method', 'polar') + alg_environments = struct + + progressfig + end + + + %% + methods + function alg = Vumps2(kwargs) + arguments + kwargs.?Vumps2 + end + + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + alg.(field{1}) = kwargs.(field{1}); + end + end + + if ~isfield('alg_eigs', kwargs) + alg.alg_eigs.tol = sqrt(alg.tol_min * alg.tol_max); + alg.alg_eigs.verbosity = alg.verbosity - 2; + end + + if ~isfield('alg_canonical', kwargs) + alg.alg_canonical.Tol = sqrt(alg.tol_min * alg.tol_max); + alg.alg_canonical.Verbosity = alg.verbosity - 2; + end + + if ~isfield('alg_environments', kwargs) + alg.alg_environments.Tol = sqrt(alg.tol_min * alg.tol_max); + alg.alg_environments.Verbosity = alg.verbosity - 2; + end + end + + function [mps, lambda, GL, GR, eta] = fixedpoint(alg, mpo, mps) + if period(mpo) ~= period(mps) + error('vumps:argerror', ... + 'periodicity of mpo (%d) should be equal to that of the mps (%d)', ... + period(mpo), period(mps)); + end + + if period(mps) < 2 + error('vumps2:argerror', ... + 'vumps2 needs a 2 site unitcell'); + end + + t_total = tic; + disp_init(alg); + + mps = canonicalize(mps); + [GL, GR] = environments(alg, mpo, mps); + + for iter = 1:alg.maxiter + t_iter = tic; + + AC2 = updateAC2(alg, iter, mpo, mps, GL, GR); + C = updateC (alg, iter, mpo, mps, GL, GR); + mps = updatemps(alg, iter, mps, AC2, C); + + [GL, GR, lambda] = environments(alg, mpo, mps); + eta = convergence(alg, mpo, mps, GL, GR); + + if iter > alg.miniter && eta < alg.tol + disp_conv(alg, iter, lambda, eta, toc(t_total)); + return + end + alg = updatetols(alg, iter, eta); + plot(alg, iter, mps, eta); + disp_iter(alg, iter, lambda, eta, toc(t_iter)); + + if alg.doSave && mod(iter, alg.saveIterations) == 0 + save_iteration(alg, mps, lambda, iter, eta); + end + end + + disp_maxiter(alg, iter, lambda, eta, toc(t_total)); + end + end + + + %% Subroutines + methods + function AC2 = updateAC2(alg, iter, mpo, mps, GL, GR) + if strcmp(alg.multiAC, 'sequential') + sites = mod1(iter, period(mps)); + else + sites = 1:period(mps); + sites = sites(mod(sites, 2) == mod(iter, 2)); + end + H_AC2 = AC2_hamiltonian(mpo, mps, GL, GR, sites); + for i = length(sites):-1:1 + AC2{i} = computeAC2(mps, 1, sites(i)); + [AC2{i}, ~] = eigsolve(alg.alg_eigs, @(x) H_AC2{i}.apply(x), AC2{i}, ... + 1, alg.which); + end + end + + function C = updateC(alg, iter, mpo, mps, GL, GR) + if strcmp(alg.multiAC, 'sequential') + sites = mod1(iter, period(mps)); + else + sites = 1:period(mps); + sites = sites(mod(sites, 2) == mod(iter, 2)); + end + sites = next(sites, period(mps)); + H_C = C_hamiltonian(mpo, mps, GL, GR, sites); + for i = length(sites):-1:1 + [C{i}, ~] = eigsolve(alg.alg_eigs, @(x) H_C{i}.apply(x), mps.C{sites(i)}, ... + 1, alg.which); + end + end + + function mps = updatemps(alg, iter, mps, AC2, C) + if strcmp(alg.multiAC, 'sequential') + sites = mod1(iter, period(mps)); + else + sites = 1:period(mps); + sites = sites(mod(sites, 2) == mod(iter, 2)); + end + for i = length(AC2):-1:1 + [Q_AC, ~] = leftorth(AC2{i}, 'polar'); + [Q_C, ~] = leftorth(C{i}, 1, 2, 'polar'); + AL = multiplyright(Q_AC, Q_C'); + if alg.notrunc + assert(isempty(alg.trunc) || strcmp(alg.trunc{1}, 'TruncTotalDim'), 'tba', 'notrunc only defined in combination with TruncTotalDim'); + alg.trunc{2} = max(alg.trunc{2}, dims(rightvspace(mps, sites(i)))); + end + [AL1, C, AL2] = tsvd(AL.var, [1 2], [3 4], alg.trunc{:}); + mps.AL{sites(i)} = multiplyright(MpsTensor(AL1), C); + mps.AL{next(sites(i), period(mps))} = MpsTensor(AL2); + end + + kwargs = namedargs2cell(alg.alg_canonical); +% mps.C = []; + newAL = cell(size(mps.AL)); + for i = 1:numel(newAL) + newAL{i} = mps.AL{i}; + end + mps = UniformMps(newAL); + end + + function [GL, GR, lambda] = environments(alg, mpo, mps, GL, GR) + arguments + alg + mpo + mps + GL = cell(1, period(mps)) + GR = cell(1, period(mps)) + end + + kwargs = namedargs2cell(alg.alg_environments); + [GL, GR, lambda] = environments(mpo, mps, mps, GL, GR, ... + kwargs{:}); + end + + function eta = convergence(alg, mpo, mps, GL, GR) + H_AC = AC_hamiltonian(mpo, mps, GL, GR); + H_C = C_hamiltonian(mpo, mps, GL, GR); + eta = zeros(1, period(mps)); + for w = 1:period(mps) + AC_ = apply(H_AC{w}, mps.AC{w}); + lambda_AC = dot(AC_, mps.AC{w}); + AC_ = normalize(AC_ ./ lambda_AC); + + ww = prev(w, period(mps)); + C_ = apply(H_C{ww}, mps.C{ww}); + lambda_C = dot(C_, mps.C{ww}); + C_ = normalize(C_ ./ lambda_C); + + eta(w) = distance(AC_ , ... + repartition(multiplyleft(mps.AR{w}, C_), rank(AC_))); + end + eta = max(eta, [], 'all'); + end + end + + + %% Option handling + methods + function alg = updatetols(alg, iter, eta) + if alg.dynamical_tols + alg.alg_eigs.tol = between(alg.tol_min, eta * alg.eigs_tolfactor, ... + alg.tol_max / iter); + alg.alg_canonical.Tol = between(alg.tol_min, ... + eta * alg.canonical_tolfactor, alg.tol_max / iter); + alg.alg_environments.Tol = between(alg.tol_min, ... + eta * alg.environments_tolfactor, alg.tol_max / iter); + + if alg.verbosity > Verbosity.iter + fprintf('Updated subalgorithm tolerances: (%e,\t%e,\t%e)\n', ... + alg.alg_eigs.Tol, alg.alg_canonical.Tol, alg.alg_environments.Tol); + end + end + + if alg.dynamical_multiAC + if eta < alg.tol_multiAC + alg.multiAC = 'sequential'; + else + alg.multiAC = 'parallel'; + end + end + end + end + + %% Display + methods (Access = private) + function plot(alg, iter, mps, eta) + if ~alg.doplot, return; end + persistent axhistory axspectrum + + D = depth(mps); + W = period(mps); + + if isempty(alg.progressfig) || ~isvalid(alg.progressfig) || iter == 1 + alg.progressfig = figure('Name', 'Vumps'); + axhistory = subplot(D + 1, W, 1:W); + axspectrum = gobjects(D, W); + for d = 1:D, for w = 1:W + axspectrum(d, w) = subplot(D + 1, W, w + d * W); + end, end + linkaxes(axspectrum, 'y'); + end + + + if isempty(axhistory.Children) + semilogy(axhistory, iter, eta, '.', 'Color', colors(1), ... + 'DisplayName', 'Errors', 'MarkerSize', 10); + hold on + ylim(axhistory, [alg.tol / 5, max([1 eta])]); + yline(axhistory, alg.tol, '-', 'Convergence', ... + 'LineWidth', 2); + hold off + else + axhistory.Children(end).XData(end+1) = iter; + axhistory.Children(end).YData(end+1) = eta; + end + + plot_entanglementspectrum(mps, 1:D, 1:W, axspectrum); + drawnow + end + + function disp_init(alg) + if alg.verbosity < Verbosity.conv, return; end + fprintf('---- VUMPS2 ---\n'); + end + + function disp_iter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.iter, return; end + + s = settings; + if abs(imag(lambda)) < eps(lambda)^(3/4) * abs(lambda) + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vumps2 %2d:\tE = %-0.4f\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), eta, time2str(t)); + otherwise + fprintf('Vumps2 %4d:\tE = %-0.15f\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), eta, time2str(t, 's')); + + end + else + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vumps2 %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('Vumps2 %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + end + end + + function disp_conv(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.conv, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vumps converged %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('Vumps converged %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + fprintf('---------------\n'); + end + + function disp_maxiter(alg, iter, lambda, eta, t) + if alg.verbosity < Verbosity.warn, return; end + s = settings; + switch s.matlab.commandwindow.NumericFormat.ActiveValue + case 'short' + fprintf('Vumps max iterations %2d:\tE = %-0.4f %+0.4fi\terror = %0.1e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + otherwise + fprintf('Vumps max iterations %4d:\tE = %-0.15f %+0.15fi\terror = %0.4e\t(%s)\n', ... + iter, real(lambda), imag(lambda), eta, time2str(t)); + + end + fprintf('---------------\n'); + end + + function save_iteration(alg, mps, lambda, iter, eta) + fileName = alg.name; + + fileData = struct; + fileData.mps = mps; + fileData.lambda = lambda; + fileData.iteration = iter; + fileData.eta = eta; + % save + + %if exist(fileName,'file') + % old_file=load(fileName); + % fileName_temp=[fileName(1:end-4),'_temp.mat']; + % save(fileName_temp, '-struct', 'old_file', '-v7.3'); + % saved_temp=1; + %else + % saved_temp=0; + %end + + save(fileName, '-struct', 'fileData', '-v7.3'); + + %if saved_temp + % delete(fileName_temp); + %end + end + end +end + diff --git a/src/algorithms/eigsolvers/Arnoldi.m b/src/algorithms/eigsolvers/Arnoldi.m new file mode 100644 index 0000000..e04f6eb --- /dev/null +++ b/src/algorithms/eigsolvers/Arnoldi.m @@ -0,0 +1,482 @@ +classdef Arnoldi + % Arnoldi Krylov algorithm for linear algebra problems. + % + % Properties + % ---------- + % tol : :class:`double` + % convergence tolerance, defaults to :code:`1e-10`. + % + % maxiter : :class:`int` + % maximum number of iterations, defaults to :code:`100`. + % + % krylovdim : :class:`int` + % Krylov subspace dimension, defaults to :code:`20`. + % + % deflatedim : :class:`int` + % number of Krylov vectors to keep when deflating. + % + % reorth : :class:`int` + % reorthogonalize basis if larger than this number, defaults to :code:`20`. + % + % nobuild : :class:`int` + % frequency of convergence check when building, defaults to :code:`3`. + % + % verbosity : :class:`.Verbosity` + % display information, defaults to :code:`Verbosity.warn`. + + properties + tol = 1e-10 + maxiter = 100 + krylovdim = 20 + deflatedim + reorth = 20 + nobuild = 3 + verbosity = Verbosity.warn + end + + methods + function alg = Arnoldi(kwargs) + arguments + kwargs.?Arnoldi + end + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + alg.(field{1}) = kwargs.(field{1}); + end + end + end + + function varargout = eigsolve(alg, A, v0, howmany, sigma) + % Find a few eigenvalues and eigenvectors of an operator using an Arnoldi + % routine. + % + % Usage + % ----- + % :code:`[V, D, flag] = eigsolve(A, v, howmany, sigma)` + % + % :code:`D = eigsolve(A, v, ...)` + % + % Arguments + % --------- + % A : :class:`matrix` or :class:`function_handle` + % A square matrix. + % A function handle which implements one of the following, depending on sigma: + % + % - :code:`A \ x`, if `sigma` is 0 or 'smallestabs' + % - :code:`(A - sigma * I) \ x`, if sigma is a nonzero scalar + % - :code:`A * x`, for all other cases + % + % v : :class:`vector` + % initial guess for the eigenvector. + % + % howmany : :class:`int` + % amount of eigenvalues and eigenvectors that should be computed. By default + % this is 1, and this should not be larger than the total dimension of A. + % + % sigma : :class:`char` or numeric + % selector for the eigenvalues, should be either one of the following: + % + % - 'largestabs', 'lm': default, eigenvalues of largest magnitude + % - 'largestreal', 'lr': eigenvalues with largest real part + % - 'largestimag', 'li': eigenvalues with largest imaginary part. + % - 'smallestabs', 'sm': default, eigenvalues of smallest magnitude + % - 'smallestreal', 'sr': eigenvalues with smallest real part + % - 'smallestimag', 'si': eigenvalues with smallest imaginary part. + % - numeric : eigenvalues closest to sigma. + % + % Returns + % ------- + % V : (1, howmany) :class:`vector` + % vector of eigenvectors. + % + % D : :class:`numeric` + % vector of eigenvalues if only a single output argument is asked, diagonal + % matrix of eigenvalues otherwise. + % + % flag : :class:`int` + % convergence info flag: + % + % - flag = 0: all eigenvalues are converged. + % - flag = 1: invariant subspace was found and the algorithm was aborted. + % - flag = 2: algorithm did not converge after maximum number of iterations. + + arguments + alg + A + v0 + howmany = 1 + sigma = 'lm' + end + + t_total = tic; + + if isnumeric(v0) + v0_vec = v0; + if isa(A, 'function_handle') + A_fun = @A; + else + A_fun = @(v) A * v; + end + else + v0_vec = vectorize(v0); + if isa(A, 'function_handle') + A_fun = @(v) vectorize(A(devectorize(v, v0))); + else + A_fun = @(v) vectorize(A * devectorize(v, v0)); + end + end + + if norm(v0_vec) < eps(underlyingType(v0_vec))^(3/4) + error('eigsolve:inputnorm', 'starting vector should not have zero norm.'); + end + + sz = size(v0_vec); + + if sz(1) < howmany + howmany = sz(1); + if alg.verbosity >= Verbosity.warn + warning('eigsolve:size', 'requested %d out of %d eigenvalues.', ... + howmany, sz(1)); + end + end + + if sz(1) < alg.krylovdim + alg.krylovdim = sz(1); + if alg.verbosity >= Verbosity.warn + warning('eigsolve:size', ... + 'Krylov subspace dimension is larger than total number of eigenvalues, reducing Krylov dimension to %d.', ... + sz(1)); + end + end + + % some input validation + if isempty(alg.nobuild), alg.nobuild = ceil(alg.krylovdim / 10); end + if isempty(alg.deflatedim), alg.deflatedim = max(round(3/5 * alg.krylovdim), howmany); end + alg.deflatedim = max(alg.deflatedim, howmany); + assert(alg.deflatedim < alg.krylovdim, 'eigsolve:argerror', ... + 'Deflate size should be smaller than krylov dimension.') + + v = v0_vec / norm(v0_vec, 'fro'); + + % preallocation + V = zeros(length(v), alg.krylovdim, 'like', v); % Krylov subspace basis + H = zeros(alg.krylovdim, alg.krylovdim, underlyingType(v)); % Hessenberg matrix + + ctr_outer = 0; + ctr_inner = 0; + flag = 0; + + while ctr_outer < alg.maxiter + t_outer = tic; + ctr_outer = ctr_outer + 1; + + flag_inner = 0; + while ctr_inner < alg.krylovdim % build Krylov subspace + t_inner = tic; + ctr_inner = ctr_inner + 1; + + V(:, ctr_inner) = v; + v = A_fun(v); + for i = 1:ctr_inner + H(i, ctr_inner) = dot(V(:, i), v); + end + v = v - V * H(:, ctr_inner); + + % reorthogonalize new vector + if ctr_inner >= alg.reorth + if isnumeric(V) + c = V' * v; + else + c = zeros(size(V, 2), 1); + for i = 1:ctr_inner + c(i) = dot(V(:, i), v); + end + end + H(:, ctr_inner) = H(:, ctr_inner) + c; + v = v - V * c; + end + + % normalize + beta = norm(v, 'fro'); + v = v ./ beta; + + if ctr_inner == alg.krylovdim, break; end + + if ctr_inner >= howmany + invariantsubspace = beta < eps(underlyingType(beta))^(3/4); + if invariantsubspace || ctr_inner == alg.krylovdim + break; + end + + % check for convergence during subspace build + if ~mod(ctr_inner, alg.nobuild) + [U, lambda] = eig(H(1:ctr_inner, 1:ctr_inner), 'vector'); + select = selecteigvals(lambda, howmany, sigma); + conv = beta * max(abs(U(ctr_inner, select))); + + if conv < alg.tol + V = V(:, 1:ctr_inner) * U(:, select); + D = diag(lambda(select)); + if alg.verbosity >= Verbosity.conv + fprintf('Conv %2d (%2d/%2d):\tlambda = %.5e + %.5ei;\terror = %.5e;\ttime = %s.\n', ... + ctr_outer, ctr_inner, alg.krylovdim, ... + real(lambda(1)), imag(lambda(1)), conv, ... + time2str(toc(t_total))); + end + flag_inner = 1; + break + end + + if alg.verbosity >= Verbosity.detail + fprintf('Iter %2d (%2d/%2d):\tlambda = %.5e + %.5ei;\terror = %.5e;\ttime = %s.\n', ... + ctr_outer, ctr_inner, alg.krylovdim, ... + real(lambda(1)), imag(lambda(1)), conv, ... + time2str(toc(t_inner))); + end + end + end + + H(ctr_inner + 1, ctr_inner) = beta; + end + if flag_inner + break + end + + % stopping criterium reached - irrespective of convergence + if ctr_outer == alg.maxiter || ctr_inner ~= alg.krylovdim + [U, lambda] = eig(H(1:ctr_inner, 1:ctr_inner), 'vector'); + select = selecteigvals(lambda, howmany, sigma); + conv = max(abs(beta * U(end, select))); + V = V(:, 1:ctr_inner) * U(:, select); + D = diag(lambda(select)); + + if conv > alg.tol + if invariantsubspace + flag = 1; + else + flag = 2; + end + end + break + end + + % deflate Krylov subspace + [U1, T] = schur(H, 'real'); + E = ordeig(T); + select1 = false(size(E)); + select1(selecteigvals(E, alg.deflatedim, sigma)) = true; + [U1, T] = ordschur(U1, T, select1); + + V = V * U1; + [U, lambda] = eig(T(1:alg.deflatedim, 1:alg.deflatedim), 'vector'); + select = selecteigvals(lambda, howmany, sigma); + + conv = max(abs(beta * U1(alg.krylovdim, 1:alg.deflatedim) * U(:, select))); + + % check for convergence + if conv < alg.tol + V = V(:, 1:alg.deflatedim) * U(:, select); + D = diag(lambda(select)); + if alg.verbosity >= Verbosity.conv + fprintf('Conv %2d:\tlambda = %.5e + %.5ei;\terror = %.5e;\ttime = %s.\n', ... + ctr_outer, real(lambda(1)), imag(lambda(1)), conv, time2str(toc(t_outer))); + end + break + end + + if alg.verbosity >= Verbosity.iter + fprintf('Iter %2d:\tlambda = %.5e + %.5ei;\terror = %.5e;\ttime = %s.\n', ... + ctr_outer, real(lambda(1)), imag(lambda(1)), conv, time2str(toc(t_total))); + end + + % deflate Krylov subspace + H = zeros(alg.krylovdim, alg.krylovdim, underlyingType(v)); + H(1:alg.deflatedim, 1:alg.deflatedim) = ... + T(1:alg.deflatedim, 1:alg.deflatedim); + H(alg.deflatedim + 1, 1:alg.deflatedim) = ... + beta * U1(alg.krylovdim, 1:alg.deflatedim); + V(:, alg.deflatedim + 1:end) = 0 * V(:, alg.deflatedim + 1:end); + ctr_inner = alg.deflatedim; + end + + % process results + if nargout <= 1 + varargout = {diag(D)}; + else + if ~isnumeric(v0) + for i = howmany:-1:1 + varargout{1}(:, i) = devectorize(V(:, i), v0); + end + else + for i = howmany:-1:1 + varargout{1}(:, i) = V(:, i); + end + end + varargout{2} = D; + if nargout == 3 + varargout{3} = flag; + end + end + + % display + if nargout < 3 + if flag == 1 + warning('Found invariant subspace (error = %.5e).\n', conv); + elseif flag == 2 + warning('Reached maxiter without convergence.\n'); + end + end + if ~flag && alg.verbosity > Verbosity.warn + fprintf('eigsolve converged.\n'); + end + end + + function [w0, flag] = expsolve(alg, A, t, v0) + if isempty(alg.nobuild), alg.nobuild = ceil(alg.krylovdim / 10); end + if isempty(alg.deflatedim), alg.deflatedim = max(round(3/5 * alg.krylovdim), howmany); end + + beta0 = norm(v0, 'fro'); + w0 = v0; + + w = w0; + v = w0; + beta = norm(w, 'fro'); + v = w / beta; + + % preallocation + V = zeros(1, alg.krylovdim, 'like', v0); % Krylov subspace basis + H = zeros(alg.krylovdim, alg.krylovdim, underlyingType(v0)); % Hessenberg matrix + + % time step parameters + eta = alg.tol; + totalerr = 0; + sgn = sign(t); + tau = abs(t); + tau0 = 0; + dtau = tau - tau0; + + delta = 1.2; + gamma = 0.8; + + ctr_outer = 0; + ctr_inner = 0; + flag = 0; + + while ctr_outer < alg.maxiter + ctr_outer = ctr_outer + 1; + + while ctr_inner < alg.krylovdim % build Krylov subspace + ctr_inner = ctr_inner + 1; + + V(1:length(w0), ctr_inner) = w; + w = A(w); + for i = 1:ctr_inner + H(i, ctr_inner) = dot(V(:, i), w); + end + w = w - V * H(:, ctr_inner); + + % reorthogonalize new vector + if ctr_inner >= alg.reorth + c = zeros(size(V, 2), 1); + for i = 1:ctr_inner + c(i) = dot(V(:, i), w); + end + H(:, ctr_inner) = H(:, ctr_inner) + c; + w = w - V * c; + end + + % normalize + beta = norm(w, 'fro'); + w = w / beta; + + if ctr_inner == alg.krylovdim + dtau = min(dtau, tau - tau0); + + HH = zeros(ctr_inner + 1); + HH(1:ctr_inner, 1:ctr_inner) = H(1:ctr_inner, 1:ctr_inner) * (sgn * dtau); + HH(1, ctr_inner + 1) = 1; + expH = expm(HH); + + vareps = abs(beta * H(ctr_inner, ctr_inner) * expH(ctr_inner, ctr_inner + 1)); + omega = vareps / (dtau * eta); + q = ctr_inner / 2; + + while omega > 1 + vareps_prev = vareps; + dtau_prev = dtau; + dtau = dtau * (gamma / omega)^(1 / (q + 1)); + + HH = zeros(ctr_inner + 1); + HH(1:ctr_inner, 1:ctr_inner) = H(1:ctr_inner, 1:ctr_inner) * (sgn * dtau); + HH(1, ctr_inner+1) = 1; + expH = expm(HH); + + vareps = abs(beta * H(ctr_inner, ctr_inner) * expH(ctr_inner, ctr_inner + 1)); + omega = vareps / (dtau * eta); + q = max(0, log(vareps / vareps_prev) / log(dtau / dtau_prev) - 1); + end + + % take time step + totalerr = totalerr + vareps; + w = V * expH(1:ctr_inner, ctr_inner); + w = w + expH(ctr_inner, end) * w0; + w0 = w0 + beta * w; + tau0 = tau0 + dtau; + + % increase time step for next iteration + if omega < gamma, dtau = dtau * (gamma / omega) ^(1 / (q + 1)); end + + if alg.verbosity > Verbosity.iter + fprintf('Iter %d: t = %.2e,\terror = %.5e.\n', ... + ctr_inner, tau0, totalerr); + end + + elseif H(ctr_inner, ctr_inner) <= (tau - tau0) * eta || alg.nobuild + HH = zeros(ctr_inner + 1); + HH(1:ctr_inner, 1:ctr_inner) = H(1:ctr_inner, 1:ctr_inner) * (sgn * (tau - tau0)); + HH(1, ctr_inner+1) = 1; + expH = expm(HH); + + vareps = abs(beta * H(ctr_inner, ctr_inner) * expH(ctr_inner, ctr_inner + 1)); + omega = vareps / ((tau - tau0) * eta); + + if omega < 1 % take time step + totalerr = totalerr + vareps; + w = V(:, 1:ctr_inner) * expH(1:ctr_inner, ctr_inner); + w = w + expH(ctr_inner, end) * v0; + w0 = w0 + beta * w; + tau0 = tau; + end + end + + if tau0 >= tau + if alg.verbosity >= Verbosity.conv + fprintf('Conv %d: error = %.5e.\n', ctr_outer, totalerr); + end + return + end + end + + if ctr_outer == alg.maxiter + if alg.verbosity >= Verbosity.conv + fprintf('Maxiter %d: error = %.5e\n', ctr_outer, totalerr); + end + return + end + + % reinitialize + beta = norm(w, 'fro'); + if beta < alg.tol + warning('converged to fixed point.'); + return + end + v = w / beta; + V = zeros(1, alg.krylovdim, 'like', v0); % Krylov subspace basis + H = zeros(alg.krylovdim, alg.krylovdim, underlyingType(v0)); % Hessenberg matrix + + end + end + end +end + diff --git a/src/algorithms/eigsolvers/KrylovSchur.m b/src/algorithms/eigsolvers/KrylovSchur.m new file mode 100644 index 0000000..c1a4581 --- /dev/null +++ b/src/algorithms/eigsolvers/KrylovSchur.m @@ -0,0 +1,190 @@ +classdef KrylovSchur + % KrylovSchur wrapper for Matlab implementation of eigs + % + % Properties + % ---------- + % tol : :class:`double` + % convergence tolerance, defaults to :code:`1e-10`. + % + % maxiter : :class:`int` + % maximum number of iterations, defaults to :code:`100`. + % + % krylovdim : :class:`int` + % Krylov subspace dimension, defaults to :code:`20`. + % + % verbosity : :class:`.Verbosity` + % display information, defaults to :code:`Verbosity.warn`. + + properties + tol = 1e-10 + maxiter = 100 + krylovdim = 20 + verbosity = Verbosity.warn + end + + methods + function alg = KrylovSchur(kwargs) + arguments + kwargs.?KrylovSchur + end + fields = fieldnames(kwargs); + if ~isempty(fields) + for field = fields.' + alg.(field{1}) = kwargs.(field{1}); + end + end + end + + function varargout = eigsolve(alg, A, v0, howmany, sigma, kwargs) + % Find a few eigenvalues and eigenvectors of an operator using the builtin + % Matlab eigs routine. + % + % Usage + % ----- + % :code:`[V, D, flag] = eigsolve(A, v, howmany, sigma, kwargs)` + % + % :code:`D = eigsolve(A, v, ...)` + % + % Arguments + % --------- + % A : :class:`matrix` or :class:`function_handle` + % A square matrix. + % A function handle which implements one of the following, depending on sigma: + % + % - :code:`A \ x`, if :code:`sigma` is 0 or 'smallestabs' + % - :code:(A - sigma * I) \ x`, if sigma is a nonzero scalar + % - :code:A * x`, for all other cases + % + % v : :class:`vector` + % initial guess for the eigenvector. + % + % howmany : :class:`int` + % amount of eigenvalues and eigenvectors that should be computed. By default + % this is 1, and this should not be larger than the total dimension of A. + % + % sigma : :class:`char` or :class:`numeric` + % selector for the eigenvalues, should be either one of the following: + % + % - 'largestabs', 'lm': default, eigenvalues of largest magnitude + % - 'largestreal', 'lr': eigenvalues with largest real part + % - 'largestimag', 'li': eigenvalues with largest imaginary part. + % - 'smallestabs', 'sm': default, eigenvalues of smallest magnitude + % - 'smallestreal', 'sr': eigenvalues with smallest real part + % - 'smallestimag', 'si': eigenvalues with smallest imaginary part. + % - numeric : eigenvalues closest to sigma. + % + % Keyword Arguments + % ----------------- + % IsSymmetric : :class:`logical` + % flag to speed up the algorithm if the operator is symmetric, false by + % default. + % + % Returns + % ------- + % V : (1, howmany) :class:`vector` + % vector of eigenvectors. + % + % D : :class:`numeric` + % vector of eigenvalues if only a single output argument is asked, diagonal + % matrix of eigenvalues otherwise. + % + % flag : :class:`int` + % convergence info flag: + % + % if flag = 0 then all eigenvalues are converged, otherwise not. + + arguments + alg + A + v0 + howmany = 1 + sigma = 'lm' + kwargs.IsSymmetric logical = false + end + + nargoutchk(0, 3); + + if isnumeric(v0) + v0_vec = v0; + if isa(A, 'function_handle') + A_fun = @A; + else + A_fun = @(v) A * v; + end + else + v0_vec = vectorize(v0); + if isa(A, 'function_handle') + A_fun = @(v) vectorize(A(devectorize(v, v0))); + else + A_fun = @(v) vectorize(A * devectorize(v, v0)); + end + end + + sz = size(v0_vec); + + if sz(1) < howmany + howmany = sz(1); + if alg.verbosity >= Verbosity.warn + warning('eigsolve:size', 'requested %d out of %d eigenvalues.', ... + howmany, sz(1)); + end + end + + if sz(1) < alg.krylovdim + alg.krylovdim = sz(1); + if alg.verbosity >= Verbosity.warn + warning('eigsolve:size', ... + 'Krylov subspace dimension is larger than total number of eigenvalues, reducing Krylov dimension to %d.', ... + sz(1)); + end + end + + % call builtin eigs + if howmany > sz(1) - 2 + % annoying bug with eigs and subspace dimension specification + [V, D, flag] = eigs(A_fun, sz(1), howmany, sigma, ... + 'Tolerance', alg.tol, 'MaxIterations', alg.maxiter, ... + 'IsFunctionSymmetric', ... + kwargs.IsSymmetric, 'StartVector', v0_vec, ... + 'Display', alg.verbosity >= Verbosity.iter); + else + [V, D, flag] = eigs(A_fun, sz(1), howmany, sigma, ... + 'Tolerance', alg.tol, 'MaxIterations', alg.maxiter, ... + 'SubspaceDimension', alg.krylovdim, 'IsFunctionSymmetric', ... + kwargs.IsSymmetric, 'StartVector', v0_vec, ... + 'Display', alg.verbosity >= Verbosity.iter); + end + + % process results + if nargout <= 1 + varargout = {diag(D)}; + else + if ~isnumeric(v0) + for i = howmany:-1:1 + varargout{1}(:, i) = devectorize(V(:, i), v0); + end + else + for i = howmany:-1:1 + varargout{1}(:, i) = V(:, i); + end + end + varargout{2} = D; + if nargout == 3 + varargout{3} = flag; + end + end + + % display + if nargout < 3 + if flag + warning('eigsolve did not converge.'); + end + end + if ~flag && alg.verbosity > Verbosity.warn + fprintf('eigsolve converged.\n'); + end + end + end +end + + diff --git a/src/algorithms/eigsolvers/selecteigvals.m b/src/algorithms/eigsolvers/selecteigvals.m new file mode 100644 index 0000000..16be40c --- /dev/null +++ b/src/algorithms/eigsolvers/selecteigvals.m @@ -0,0 +1,20 @@ +function select = selecteigvals(lambda, howmany, which) + +switch which + case {'largestabs', 'lm'} + [~, p] = sort(abs(lambda), 'descend'); + case {'largestreal', 'lr'} + [~, p] = sort(real(lambda), 'descend'); + case {'largestimag', 'li'} + [~, p] = sort(imag(lambda), 'descend'); + case {'smallestabs', 'sm'} + [~, p] = sort(abs(lambda), 'ascend'); + case {'smallestreal', 'sr'} + [~, p] = sort(real(lambda), 'ascend'); + case {'smallestimag', 'si'} + [~, p] = sort(imag(lambda), 'ascend'); +end + +select = p(1:howmany); + +end \ No newline at end of file diff --git a/src/caches/DLL.m b/src/caches/DLL.m index 0c3a775..b562d76 100644 --- a/src/caches/DLL.m +++ b/src/caches/DLL.m @@ -4,10 +4,15 @@ % % Based on the work by Richard Lange (2022). Least-Recently Used (LRU) Cache % (https://www.mathworks.com/matlabcentral/fileexchange/68836-least-recently-used-lru-cache), - % MATLAB Central File Exchange. Retrieved June 18, 2022. + % MATLAB Central File Exchange. Retrieved June 18, 2022. + % + % Properties + % ---------- + % val : :class:`any` + % data stored in this element properties - val % data stored in this element + val end properties (Access = private) @@ -22,12 +27,12 @@ % % Arguments % --------- - % val : any + % val : :class:`any` % data stored in this element. % % Returns % ------- - % dll : :class:`DLL` + % dll : :class:`.DLL` % data wrapped in a doubly-linked list format. obj.val = val; @@ -41,12 +46,12 @@ % % Arguments % --------- - % obj : :class:`DLL` + % obj : :class:`.DLL` % object to remove from the list. % % Returns % ------- - % obj : :class:`DLL` + % obj : :class:`.DLL` % removed object, with detached links. obj.prev.next = obj.next; @@ -60,15 +65,15 @@ % % Arguments % --------- - % obj : :class:`DLL` + % obj : :class:`.DLL` % list to append to. % - % other : :class:`DLL` + % other : :class:`.DLL` % object to append. % % Returns % ------- - % obj : :class:`DLL` + % obj : :class:`.DLL` % updated list. other.next = obj.next; @@ -88,12 +93,12 @@ % % Arguments % --------- - % obj : :class`DLL` + % obj : :class`.DLL` % current element in the list. % % Returns % ------- - % other : :class`DLL` + % other : :class`.DLL` % next element in the list. other = obj.next; @@ -110,12 +115,12 @@ % % Arguments % --------- - % obj : :class`DLL` + % obj : :class`.DLL` % current element in the list. % % Returns % ------- - % other : :class`DLL` + % other : :class`.DLL` % previous element in the list. other = obj.prev; diff --git a/src/caches/GetMD5/GetMD5.c b/src/caches/GetMD5/GetMD5.c index 329c191..798ca05 100644 --- a/src/caches/GetMD5/GetMD5.c +++ b/src/caches/GetMD5/GetMD5.c @@ -590,7 +590,7 @@ void ArrayCore(MD5_CTX *context, const mxArray *V) // mxGetDimensions replies [1x1] for opaque arrays, e.g. the string class. // They are forwarded to the helper function, where SIZE() replies the true // dimensions. - if (!dataOpaq) { + if (true) { // Consider class as name, not as ClassID, because the later might change // with the Matlab version: Len = strlen(ClassName); @@ -606,7 +606,7 @@ void ArrayCore(MD5_CTX *context, const mxArray *V) } MD5_Update(context, (uchar_T *) header, lenHeader * sizeof(int64_T)); mxFree(header); - } + } // Include the contents of the array: switch (ClassID) { @@ -639,6 +639,8 @@ void ArrayCore(MD5_CTX *context, const mxArray *V) break; default: // FUNCTION, VOID, OPAQUE, UNKNOWN CLASS: ----------------------- + + // Treat deep recursion as an error: if (++RecursionCount > MAX_RECURSION) { ERROR("DeepRecursion", "Cannot serialize recursive data type.\n" diff --git a/src/caches/GetMD5/GetMD5.m b/src/caches/GetMD5/GetMD5.m index 2174349..2da3a08 100644 --- a/src/caches/GetMD5/GetMD5.m +++ b/src/caches/GetMD5/GetMD5.m @@ -10,7 +10,7 @@ function GetMD5(varargin) % Data % input data on which the checksum is computed. % -% Mode : char +% Mode : :code:`char` % optional declaration of the type of the input data. % % - 'File' : `data` is a file name as a `char`. @@ -22,7 +22,7 @@ function GetMD5(varargin) % - 'Array' : Include the class and size information of `data` in the MD5 sum. This can be % applied for (nested) structs, objects, cells and sparse arrays also. % -% Format : char +% Format : :code:`char` % Format of the output, default value is 'hex'. % % - 'hex' : (1, 32) lowercase hexadecimal char. @@ -38,7 +38,7 @@ function GetMD5(varargin) % % Notes % ----- -% For sparse arrays, function handles, java and user-defined objects :func:`GetMD5_helper` +% For sparse arrays, function handles, java and user-defined objects :func:`.GetMD5_helper` % is called to convert into a data format that can be handled. % % The C-Mex-file is compiled automatically when this function is called for the first time. @@ -54,10 +54,10 @@ function GetMD5(varargin) % % See also % -------- -% Other methods for checksums can be found: :code:`CalcCRC32`, :code:`DataHash`, ... +% Other methods for checksums can be found: :code:`CalcCRC32`, :code:`DataHash`, etc. % -% For more checksum methods see: -% http://www.mathworks.com/matlabcentral/fileexchange/31272-datahash +% For more checksum methods see +% `here `_. % Dummy code, which calls the auto-compilation only: --------------------------- % This M-function is not called, if the compiled MEX function is in the path. diff --git a/src/caches/GetMD5/GetMD5.mexa64 b/src/caches/GetMD5/GetMD5.mexa64 index b80a80a..ea69356 100755 Binary files a/src/caches/GetMD5/GetMD5.mexa64 and b/src/caches/GetMD5/GetMD5.mexa64 differ diff --git a/src/caches/GetMD5/GetMD5_helper.m b/src/caches/GetMD5/GetMD5_helper.m index 4442e41..a4da9c7 100644 --- a/src/caches/GetMD5/GetMD5_helper.m +++ b/src/caches/GetMD5/GetMD5_helper.m @@ -1,23 +1,29 @@ function S = GetMD5_helper(V) -% GetMD5_helper: Convert non-elementary array types for GetMD5 +% Convert non-elementary array types for GetMD5 % The C-Mex function GetMD5 calls this function to obtain meaningful unique data % for function handles, java or user-defined objects and sparse arrays. The % applied processing can depend on the needs of the users, therefore it is % implemented as an M-function, which is easier to modify than the C-code. % -% INPUT: -% V: Array of any type, which is not handled in the C-Mex. -% OUTPUT: -% S: Array or struct containing elementary types only. -% The implementation migth be changed by the user! -% Default: -% - Sparse arrays: Struct containing the indices and values. -% - Function handle: The reply of FUNCTIONS and the size and date of the -% file. -% - User defined and java objects: V.hashCode if existing, else: struct(V). +% Arguments +% --------- +% V +% array of any type, which is not handled in the C-Mex. % -% NOTE: -% For objects the function getByteStreamFromArray() might be exhaustive and +% Returns +% ------- +% S +% array or struct containing elementary types only. +% The implementation migth be changed by the user! +% Default: +% +% - Sparse arrays: Struct containing the indices and values. +% - Function handle: The reply of FUNCTIONS and the size and date of the file. +% - User defined and java objects: :code:`V.hashCode` if existing, else: :code:`struct(V)`. +% +% Note +% ---- +% For objects the function :code:`getByteStreamFromArray()` might be exhaustive and % efficient, but unfortunately it is not documented. % % Tested: Matlab/64 7.8, 7.13, 8.6, 9.1, Win7/64 diff --git a/src/caches/GetMD5/InstallMex.m b/src/caches/GetMD5/InstallMex.m index 7c13880..dd3d139 100644 --- a/src/caches/GetMD5/InstallMex.m +++ b/src/caches/GetMD5/InstallMex.m @@ -1,46 +1,69 @@ function Ok = InstallMex(SourceFile, varargin) -% INSTALLMEX - Compile and install Mex file +% Compile and install Mex file % The C, C++ or FORTRAN mex file is compiled and additional installation % routines are started. Advanced users can call MEX() manually instead, but some % beginners are overwhelmed by instructions for a compilation sometimes. % Therefore this function can be called automatically from an M-function, when % the compiled Mex-Function does not exist already. % -% Ok = InstallMex(SourceFile, ...) -% INPUT: -% SourceFile: Name of the source file, with or without absolute or partial -% path. The default extension '.c' is appended on demand. -% Optional arguments in arbitrary order: -% Function name: Function is started after compiling, e.g. a unit-test. -% Cell string: Additional arguments for the compilation, e.g. libraries. -% '-debug': Enable debug mode. -% '-force32': Use the compatibleArrayDims flag under 64 bit Matlab. -% '-replace': Overwrite existing mex file without confirmation. +% Usage +% ----- +% :code:`Ok = InstallMex(SourceFile, ...)` % -% OUTPUT: -% Ok: Logical flag, TRUE if compilation was successful. Optional. +% Arguments +% --------- +% SourceFile +% Name of the source file, with or without absolute or partial path. The default extension +% '.c' is appended on demand. % -% COMPATIBILITY: +% Optional Arguments +% ------------------ +% Function name +% Function is started after compiling, e.g. a unit-test. +% Cell string +% additional arguments for the compilation, e.g. libraries. +% '-debug' +% enable debug mode. +% '-force32' +% use the compatibleArrayDims flag under 64 bit Matlab. +% '-replace' +% overwrite existing mex file without confirmation. +% +% Returns +% ------- +% Ok +% logical flag, :code:`true` if compilation was successful. +% +% Note +% ---- +% % - A compiler must be installed and setup before: mex -setup % - For Linux and MacOS the C99 style is enabled for C-files. % - The optimization flag -O is set. -% - Defined compiler directives: -% * MATLABVER: is the current version, e.g. 708 for v7.8. -% * _LITTLE_ENDIAN or _BIG_ENDIAN: according to processor type -% * HAS_HG2: Defined for Matlab >= R2014b with HG2 graphics. -% * -R2017b or -largeArrayDims for 64 bit addressing and C Matrix API +% - Defined compiler directives +% - MATLABVER: is the current version, e.g. 708 for v7.8. +% - _LITTLE_ENDIAN or _BIG_ENDIAN: according to processor type +% - HAS_HG2: Defined for Matlab >= R2014b with HG2 graphics. +% - -R2017b or -largeArrayDims for 64 bit addressing and C Matrix API % -R2018a for C Data API (this is set, when the string "[R2018a API]" % appears anywhere inside the source file. % -% EXAMPLES: +% Examples +% -------- +% % Compile func1.c with LAPACK libraries: -% InstallMex('func1', {'libmwlapack.lib', 'libmwblas.lib'}) +% +% :code:`InstallMex('func1', {'libmwlapack.lib', 'libmwblas.lib'})` +% % Compile func2.cpp, enable debugging and call a test function: -% InstallMex('func2.cpp', '-debug', 'Test_func2'); +% +% :code:`InstallMex('func2.cpp', '-debug', 'Test_func2');` +% % These commands can be appended after the help section of an M-file, when the % compilation should be started automatically, if the compiled MEX is not found. % -% NOTES: +% Note +% ---- % Suggestions for improvements and comments are welcome! % Feel free to add this function to your FEX submissions, when you change the % URL in the variable "Precompiled" accordingly. diff --git a/src/caches/GetMD5/uTest_GetMD5.m b/src/caches/GetMD5/uTest_GetMD5.m index 8c3f411..da1f286 100644 --- a/src/caches/GetMD5/uTest_GetMD5.m +++ b/src/caches/GetMD5/uTest_GetMD5.m @@ -3,13 +3,20 @@ function uTest_GetMD5(doSpeed) % This is a routine for automatic testing. It is not needed for processing and % can be deleted or moved to a folder, where it does not bother. % -% uTest_GetMD5(doSpeed) -% INPUT: -% doSpeed: Optional logical flag to trigger time consuming speed tests. -% Default: TRUE. If no speed test is defined, this is ignored. -% OUTPUT: -% On failure the test stops with an error. -% The speed is compared to a Java method. +% Usage +% ----- +% :code:`uTest_GetMD5(doSpeed)` +% +% Arguments +% --------- +% doSpeed +% Optional logical flag to trigger time consuming speed tests. Defaults to :code:`true`. +% If no speed test is defined, this is ignored. +% +% Note +% ---- +% On failure the test stops with an error. +% The speed is compared to a Java method. % % Tested: Matlab 6.5, 7.7, 7.8, 7.13, WinXP/32, Win7/64 % Author: Jan Simon, Heidelberg, (C) 2009-2019 matlab.2010(a)n(MINUS)simon.de @@ -209,13 +216,13 @@ function uTest_GetMD5(doSpeed) if MatlabV >= 901 % R2016b Data = string('hello'); S1 = GetMD5(Data, 'Array'); - if ~isequal(S1, '2614526bcbd4af5a8e7bf79d1d0d92ab') + if ~isequal(S1, '54d669bbc5d2f755929a1ade4869f80b') error([ErrID, ':String'], 'Bad result for string.'); end Data = string({'hello', 'world'}); S1 = GetMD5(Data, 'Array'); - if ~isequal(S1, 'a1bdbbe9a15c249764847ead9bf47326') + if ~isequal(S1, '0b633f8d7bc5bc54171f8ce6b60a096b') error([ErrID, ':String'], 'Bad result for string.'); end fprintf(' ok: String class\n'); diff --git a/src/caches/LRU.m b/src/caches/LRU.m index fc586ee..8ccc726 100644 --- a/src/caches/LRU.m +++ b/src/caches/LRU.m @@ -1,31 +1,47 @@ classdef LRU < handle - % LRU a least-recently-used cache. Stores data up to a preset memory limit, then removes + % A least-recently-used cache. Stores data up to a preset memory limit, then removes % the least-recently-used elements to free up space for additional data. + % + % Properties + % ---------- + % sentinel : :class:`.DLL` + % sentinel of DLL, +sentinel is MRU, -sentinel is LRU + % + % map : :class:`containers.Map` + % map of key --> dll + % + % itemlimit : :class:`int` + % maximum size of cache in number of items + % + % memlimit : :class:`double` + % maximum size of cache in bytes + % + % mem : :class:`double` + % current memory usage in bytes. properties (Access = private) - sentinel % sentinel of DLL, +sentinel is MRU, -sentinel is LRU - map % map of key --> dll - itemlimit = Inf % maximum size of cache in number of items - memlimit = 6 * 2^30 % maximum size of cache in bytes - mem = 0; % current memory usage in bytes. + sentinel + map + itemlimit = Inf + memlimit = 20 * 2^30 + mem = 0; end - methods function cache = LRU(itemlimit, memlimit) % Construct a new LRU cache. % % Arguments % --------- - % itemlimit : int + % itemlimit : :class:`int` % maximum size of cache in number of items. % - % memlimit : numeric + % memlimit : :class:`double` % maximum size of cache in number of bytes. % % Returns % ------- - % cache : :class:`LRU` + % cache : :class:`.LRU` % empty LRU cache. % Initialize data @@ -46,25 +62,25 @@ % % Arguments % --------- - % cache : :class:`LRU` + % cache : :class:`.LRU` % data cache. % - % key : :class:`uint8` + % key : :class:`.uint8` % data key. % % Returns % ------- - % val : any + % val : :class:`any` % value that is stored with a key, or empty if key not in cache. - try + if isKey(cache.map, key) dll = cache.map(key); val = dll.val{2}; % Re-insert dll to the front of the list pop(dll); append(cache.sentinel, dll); - catch + else val = []; end end @@ -74,18 +90,18 @@ % % Arguments % --------- - % cache : :class:`LRU` + % cache : :class:`.LRU` % data cache. % % key : :class:`uint8` % data key. % - % val : any + % val : :class:`any` % data value. % % Returns % ------- - % cache : :class:`LRU` + % cache : :class:`.LRU` % updated cache. % remove previously stored data @@ -105,9 +121,9 @@ cache.mem = cache.mem + memsize(val, 'B'); while cache.mem > cache.memlimit || length(cache.map) > cache.itemlimit dll_oldest = -cache.sentinel; - pop(dll_oldest); cache.mem = cache.mem - memsize(dll_oldest.val{2}, 'B'); - cache.map.remove(key); + cache.map.remove(dll_oldest.val{1}); + pop(dll_oldest); end end diff --git a/src/environments/CtmrgEnvironment.m b/src/environments/CtmrgEnvironment.m new file mode 100644 index 0000000..18bb2da --- /dev/null +++ b/src/environments/CtmrgEnvironment.m @@ -0,0 +1,64 @@ +classdef CtmrgEnvironment + % Data structure for managing CTMRG environments. + % + % Properties + % ---------- + % corners : :class:`cell` of :class:`.Tensor` + % cell array of corner tensors. + % + % edges : :class:`cell` of :class:`.Tensor` + % cell array of edge tensors. + % + % Todo + % ---- + % Document. + + properties + corners + edges + end + + methods + + function obj = CtmrgEnvironment(varargin) + + if nargin == 0, return; end + + if iscell(varargin{1}) + obj.corners = varargin{1}; + obj.edges = varargin{2}; + end + + obj.corners = cellfun(@(x)x ./ norm(x), obj.corners,'UniformOutput',false); + obj.edges = cellfun(@(x)x ./ norm(x), obj.edges,'UniformOutput',false); + end + + function out = rot90(in) + %rotate unit cell + out = CtmrgEnvironment(flip(permute(in.corners,[1,3,2]),3),flip(permute(in.edges,[1,3,2]),3)); + %relabel corners and edges accordingly + in = out; + for i = 1:4 + out.corners(i,:,:) = in.corners(prev(i,4),:,:); + out.edges(i,:,:) = in.edges(prev(i,4),:,:); + end + + end + + function chi = bond_dimensions(obj) + chi = reshape(cellfun(@(x) dims(x,1),obj.corners(1,:,:)), size(obj.corners,2:3)); + end + + function h = height(obj) + % vertical period over which the peps is translation invariant. + h = height(obj.corners{1,:,:}); + end + + function w = width(obj) + % horizontal period over which the peps is translation invariant. + w = width(obj.corners{1,:,:}); + end + + end +end + diff --git a/src/environments/FiniteEnvironment.m b/src/environments/FiniteEnvironment.m new file mode 100644 index 0000000..71e7f72 --- /dev/null +++ b/src/environments/FiniteEnvironment.m @@ -0,0 +1,70 @@ +classdef FiniteEnvironment + % Data structure for managing the environments in finite MPS algorithms. + % + % Properties + % ---------- + % GL : :class:`cell` of :class:`.MpsTensor` + % environment tensors corresponding to the left-gauged part of the MPS. + % + % GR : :class:`cell` of :class:`.MpsTensor` + % environment tensors corresponding to the right-gauged part of the MPS. + % + % Todo + % ---- + % Document + + properties + GL + GR + end + + methods + function envs = FiniteEnvironment(varargin) + if nargin == 0, return; end + if nargin == 2 + envs.GL = varargin{1}; + envs.GR = varargin{2}; + assert(isequal(size(envs.GL), size(envs.GR))); + return + end + error('undefined syntax'); + end + + function envs = movegaugecenter(envs, mpo, mps1, mps2, pos) + for i = 2:pos + if ~isempty(envs.GL{i}), continue; end + T = transfermatrix(mpo, mps1, mps2, i-1); + envs.GL{i} = apply(T, envs.GL{i-1}); + end + for i = length(mps1):-1:(pos+1) + if ~isempty(envs.GR{i}), continue; end + T = transfermatrix(mpo, mps1, mps2, i).'; + envs.GR{i} = apply(T, envs.GR{i+1}); + end + end + + function envs = invalidate(envs, pos) + envs.GL(pos+1:end) = cell(1, length(envs.GL) - pos); + envs.GR(1:pos) = cell(1, pos); + end + end + +% methods (Static) +% function envs = initialize(mpo, mps1, mps2) +% arguments +% mpo +% mps1 +% mps2 = mps1 +% end +% +% GL = cell(1, length(mpo) + 1); +% GR = cell(1, length(mpo) + 1); +% +% GL{1} = mpo.L; +% GR{end} = mpo.R; +% +% envs = FiniteEnvironment(GL, GR); +% end +% end +end + diff --git a/src/models/fermionoperators/c_min.m b/src/models/fermionoperators/c_min.m new file mode 100644 index 0000000..4851bda --- /dev/null +++ b/src/models/fermionoperators/c_min.m @@ -0,0 +1,34 @@ +function c = c_min(kwargs) +% Fermionic annihilation operator. +% +% Keyword arguments +% ----------------- +% 'Side' : :class:`char` +% side, 'left or 'right'. +% +% Returns +% c : :class:`.Tensor` +% annihilation operator represented as a 3-leg tensor with :math:`fZ_2` symmetry. + +arguments + kwargs.side = 'left' +end + +pspace = fZ2Space([0 1], [1 1], false); +vspace = fZ2Space(1, 1, false); + +switch kwargs.side + case 'right' + c = Tensor.zeros([vspace pspace], pspace); + c = fill_matrix(c, {0 1}); + + case 'left' + c = Tensor.zeros(pspace, [pspace vspace]); + c = fill_matrix(c, {1 0}); + + otherwise + error('models:argerror', 'invalid side') +end + +end + diff --git a/src/models/fermionoperators/c_number.m b/src/models/fermionoperators/c_number.m new file mode 100644 index 0000000..03a1edc --- /dev/null +++ b/src/models/fermionoperators/c_number.m @@ -0,0 +1,13 @@ +function n = c_number() +% Fermionic number operator. +% +% Returns +% ------- +% n : :class:`.Tensor` +% number operator represented as a 2-leg tensor with :math:`fZ_2` symmetry. + +pspace = fZ2Space([0 1], [1 1], false); + +n = fill_matrix(Tensor.zeros(pspace, pspace), {0, 1}); + +end diff --git a/src/models/fermionoperators/c_plus.m b/src/models/fermionoperators/c_plus.m new file mode 100644 index 0000000..28a8cc8 --- /dev/null +++ b/src/models/fermionoperators/c_plus.m @@ -0,0 +1,34 @@ +function c_dagger = c_plus(kwargs) +% Fermionic creation operator. +% +% Keyword arguments +% ----------------- +% 'Side' : :class:`char` +% side, 'left or 'right'. +% +% Returns +% c_dagger : :class:`.Tensor` +% creation operator represented as a 3-leg tensor with :math:`fZ_2` symmetry. + +arguments + kwargs.side = 'left' +end + +pspace = fZ2Space([0 1], [1 1], false); +vspace = fZ2Space(1, 1, false); + +switch kwargs.side + case 'right' + c_dagger = Tensor.zeros([vspace pspace], pspace); + c_dagger = fill_matrix(c_dagger, {1 0}); + + case 'left' + c_dagger = Tensor.zeros(pspace, [pspace vspace]); + c_dagger = fill_matrix(c_dagger, {0 1}); + + otherwise + error('models:argerror', 'invalid side') +end + +end + diff --git a/src/models/quantum1dHeisenberg.m b/src/models/quantum1dHeisenberg.m new file mode 100644 index 0000000..e2ee882 --- /dev/null +++ b/src/models/quantum1dHeisenberg.m @@ -0,0 +1,90 @@ +function mpo = quantum1dHeisenberg(kwargs) +% Hamiltonian for the 1D Heisenberg model. +% +% .. math:: +% H = J \sum_{\langle ij \rangle} \vec{S}_i \cdot \vec{S}_j + h \sum_{i} S_i^z +% +% Keyword arguments +% ----------------- +% 'Spin' : :class:`double` +% halfinteger or integer spin label, defaults to :code:`1`. +% +% 'J' : :class:`double` +% exchange coupling, defaults to :code:`1`. +% +% 'h' : :class:`double` +% magnetic field, defaults to :code:`0`. +% +% 'L' : :class:`int` +% system size, defaults to :code:`Inf`. +% +% 'Symmetry' : :class:`char` +% symmetry group ('U1' or 'SU2'), defaults to :code:`'SU2'`. +% +% Returns +% ------- +% mpo : :class:`.InfJMpo` +% Heisenberg Hamiltonian as a Jordan block MPO. + +arguments + kwargs.Spin = 1 + kwargs.J = 1 + kwargs.h = 0 + kwargs.L = Inf % size of system + kwargs.Symmetry {mustBeMember(kwargs.Symmetry, {'Z1', 'U1', 'SU2'})} = 'SU2' +end + +J = kwargs.J; +h = kwargs.h; + +Q = SU2(2 * kwargs.Spin + 1); + +switch kwargs.Symmetry + case 'SU2' + assert(isscalar(J) || all(J == J(1)), ... + 'Different spin couplings not invariant under SU2'); + J = J(1); + assert(h == 0, 'Magnetic field not invariant under SU2'); + + H2 = J * sigma_exchange(kwargs.Spin, 'SU2'); + H1 = []; + + case 'U1' + if isscalar(J) + Jxy = J; + Jz = J; + else + assert(length(J) == 2) + Jxy = J(1); + Jz = J(2); + end + assert(h == 0, 'TBA'); + + Splus = sigma_plus(kwargs.Spin, 'U1'); + Smin = sigma_min(kwargs.Spin, 'U1'); + Sz = sigma_z(kwargs.Spin, 'U1'); + + H2 = Jxy/2 * (contract(Splus, [-1 1 -3], conj(Splus), [-4 1 -2], 'Rank', [2 2]) + ... + contract(Smin, [-1 1 -3], conj(Smin), [-4 1 -2], 'Rank', [2 2])); + if Jz ~= 0 + H2 = H2 + Jz * contract(Sz, [-1 -3], Sz, [-2 -4], 'Rank', [2 2]); + end + + if h == 0 + H1 = []; + else + H1 = h * Sz; + end + + otherwise + error('TBA'); +end + +mpo = InfJMpo.twosite(H2, H1); + +if isfinite(kwargs.L) + mpo = open_boundary_conditions(mpo, L); +end + +end + diff --git a/src/models/quantum1dHubbard.m b/src/models/quantum1dHubbard.m new file mode 100644 index 0000000..85a2bac --- /dev/null +++ b/src/models/quantum1dHubbard.m @@ -0,0 +1,68 @@ +function mpo = quantum1dHubbard(u, mu, kwargs) +% Hamiltonian for the 1D Hubbard model. +% +% .. math:: +% H = -\sum_{\langle ij \rangle} (c^+_i c_j + c^+_j c_i) + u \sum_i (1 - 2n_i^{\uparrow}) \cdot (1-2n_i^{\downarrow}) - \mu \sum_i (n_i^{\uparrow} + n_i^{\downarrow}) +% +% Arguments +% --------- +% u : :class:`double` +% interaction strength. +% +% mu : :class:`double` +% chemical potential. +% +% Keyword arguments +% ----------------- +% 'Filling' : :class:`double` +% rational filling factor. +% +% 'Symmetry' : :class:`char` +% symmetry group, defaults to :code:`'fZ2xSU2xU1'`. +% +% Returns +% ------- +% mpo : :class:`.InfJMpo` +% Hubbard Hamiltonian as a Jordan block MPO. + +arguments + u + mu = 0 + kwargs.filling = 1 + kwargs.symmetry = 'fZ2xSU2xU1' +end + +switch kwargs.symmetry + case 'fZ2xSU2xU1' + [p, q] = rat(kwargs.filling); + if q > 30 + warning('filling %f is not a nice rational (%d // %d)', kwargs.filling, p, q) + end + + pcharges = ProductCharge(fZ2(0, 1, 0), SU2(1, 2, 1), U1([0, 1, 2] * q - p)); + pspace = GradedSpace.new(pcharges, ones(size(pcharges)), false); + + acharge = ProductCharge(fZ2(1), SU2(2), U1(q)); + aspace = GradedSpace.new(acharge, 1, false); + + creation_L = fill_tensor(Tensor(pspace, [pspace aspace]), {sqrt(2) 1}); + annihilation_R = fill_tensor(Tensor([aspace pspace], pspace), {sqrt(2) 1}); + + hopping = contract(creation_L, [-1 1 -4], annihilation_R, [1 -2 -3], 'Rank', [2 2]); + hopping = hopping + hopping'; + + interaction = fill_tensor(Tensor(pspace, pspace), {1 1 -1}); + + chemical_potential = fill_tensor(Tensor(pspace, pspace), {0 2 1}); + + mpo = repmat(InfJMpo.twosite(-hopping, ... + u * interaction - mu * chemical_potential), 1, 2*q); + + otherwise + error('tba:model', 'symmetry %s not implemented', kwargs.symmetry); +end + + + + +end diff --git a/src/models/quantum1dHubbard_energy.m b/src/models/quantum1dHubbard_energy.m new file mode 100644 index 0000000..80503f0 --- /dev/null +++ b/src/models/quantum1dHubbard_energy.m @@ -0,0 +1,7 @@ +function E = quantum1dHubbard_energy(u) +% computes the Hubbard groundstate energy at half-filling +f = @(x) x.^(-1) .* besselj(0, x) .* besselj(1, x) ./ (1 + exp(2 * u * x)); +I = integral(f, 0, Inf); +E = -(u + 4 * I); + +end diff --git a/src/models/quantum1dIsing.m b/src/models/quantum1dIsing.m new file mode 100644 index 0000000..3c83a8c --- /dev/null +++ b/src/models/quantum1dIsing.m @@ -0,0 +1,107 @@ +function mpo = quantum1dIsing(kwargs) +% Hamiltonian for the 1D transverse-field Ising model. +% +% .. math:: +% H = -J \left(\sum_{\langle ij \rangle} S_i^x S_j^x + h \sum_{i} S_i^z \right). +% +% Keyword arguments +% ----------------- +% 'J' : :class:`double` +% :math:`ZZ` coupling, defaults to :code:`1`. +% +% 'h' : :class:`double` +% relative transverse field strength, defaults to :code:`1`. +% +% 'L' : :class:`int` +% system size, defaults to :code:`Inf`. +% +% 'Symmetry' : :class:`char` +% symmetry group ('Z1', 'Z2' or 'fZ2'), defaults to :code:`'Z1'`. +% +% Returns +% ------- +% mpo : :class:`.InfJMpo` +% Ising Hamiltonian as a Jordan block MPO. + +arguments + kwargs.J = 1 + kwargs.h = 1 + kwargs.L = Inf % size of system + kwargs.Symmetry {mustBeMember(kwargs.Symmetry, {'Z1', 'Z2', 'fZ2'})} = 'Z1' +end + +J = kwargs.J; +h = kwargs.h; + +sigma_x = [0 1; 1 0]; +sigma_z = [1 0; 0 -1]; + +switch kwargs.Symmetry + case 'Z1' + pSpace = CartesianSpace.new(2); + vSpace = one(pSpace); + trivSpace = one(pSpace); + + S = Tensor([vSpace pSpace], [pSpace vSpace]); + Sx = fill_matrix(S, sigma_x); + Sz = fill_matrix(S, sigma_z); + + cod = SumSpace([vSpace vSpace vSpace], pSpace); + dom = SumSpace(pSpace, [vSpace vSpace vSpace]); + O = MpoTensor.zeros(cod, dom); + O(1, 1, 1, 1) = 1; + O(3, 1, 3, 1) = 1; + O(1, 1, 2, 1) = -J * Sx; + O(2, 1, 3, 1) = Sx; + O(1, 1, 3, 1) = (-J * h) * Sz; + + case 'Z2' + pSpace = GradedSpace.new(Z2(0, 1), [1 1], false); + vSpace = GradedSpace.new(Z2(1), 1, false); + trivSpace = one(pSpace); + + Sx_l = fill_matrix(Tensor([trivSpace pSpace], [pSpace vSpace]), {1 1}); + Sx_r = fill_matrix(Tensor([vSpace pSpace], [pSpace trivSpace]), {1 1}); + Sz = fill_matrix(Tensor([trivSpace pSpace], [pSpace trivSpace]), {1 -1}); + + cod = SumSpace([one(vSpace) vSpace one(vSpace)], pSpace); + dom = SumSpace(pSpace, [one(vSpace), vSpace, one(vSpace)]); + O = MpoTensor.zeros(cod, dom); + O(1, 1, 1, 1) = 1; + O(3, 1, 3, 1) = 1; + O(1, 1, 2, 1) = -J * Sx_l; + O(2, 1, 3, 1) = Sx_r; + O(1, 1, 3, 1) = (-J * h) * Sz; + + case 'fZ2' + c_left = c_min('side', 'left'); + c_right = c_min('side', 'right'); + cdag_left = c_plus('side', 'left'); + cdag_right = c_plus('side', 'right'); + + % twosite terms + cdagc = contract(cdag_left, [-1 1 -4], c_right, [1 -2 -3], 'Rank', [2 2]); + ccdag = contract(c_left, [-1 1 -4], cdag_right, [1 -2 -3], 'Rank', [2 2]); + cc = contract(c_left, [-1 1 -4], c_right, [1 -2 -3], 'Rank', [2 2]); + cdagcdag = contract(cdag_left, [-1 1 -4], cdag_right, [1 -2 -3], 'Rank', [2 2]); + H_twosite = -J * (cdagc + ccdag + cdagcdag + cc); + + % onesite terms + n = c_number(); + H_onesite = -J * 2 * h * (n - 1 / 2 * n.eye(n.codomain, n.domain)); + + mpo = InfJMpo.twosite(H_twosite, H_onesite); + return + + otherwise + error('models:argerror', 'invalid symmetry'); +end + +mpo = InfJMpo(O); + +if isfinite(kwargs.L) + mpo = open_boundary_conditions(InfJMpo(O), kwargs.L); +end + +end + diff --git a/src/models/quantum1dIsing_dispersion.m b/src/models/quantum1dIsing_dispersion.m new file mode 100644 index 0000000..3beb03e --- /dev/null +++ b/src/models/quantum1dIsing_dispersion.m @@ -0,0 +1,13 @@ +function e = quantum1dIsing_dispersion(k, kwargs) +% Compute the dispersion relation of the transverse field Ising model in 1d. +arguments + k + kwargs.J = 1.0 + kwargs.h = 0.5 +end +g = kwargs.h; + +e = kwargs.J * sqrt(g^2 + 1 - 2 * g * cos(k)) * 2; + +end + diff --git a/src/models/quantum1dIsing_energy.m b/src/models/quantum1dIsing_energy.m new file mode 100644 index 0000000..81de703 --- /dev/null +++ b/src/models/quantum1dIsing_energy.m @@ -0,0 +1,9 @@ +function E0 = quantum1dIsing_energy(J, h) +% Compute the groundstate energy of the transverse field Ising model in 1d. +% +% + +E0 = -integral(@(k) quantum1dIsing_dispersion(k, 'J', J, 'h', h), 0, pi) / (2 * pi); + +end + diff --git a/src/models/spinoperators/pauliterm.m b/src/models/spinoperators/pauliterm.m new file mode 100644 index 0000000..01e7b0c --- /dev/null +++ b/src/models/spinoperators/pauliterm.m @@ -0,0 +1,6 @@ +function p = pauliterm(spin, i, j) + +p = sqrt((spin + 1) * (i + j - 1) - i * j) / 2; + +end + diff --git a/src/models/spinoperators/sigma_exchange.m b/src/models/spinoperators/sigma_exchange.m new file mode 100644 index 0000000..2ca4b3c --- /dev/null +++ b/src/models/spinoperators/sigma_exchange.m @@ -0,0 +1,38 @@ +function S = sigma_exchange(spin, symmetry) +% Spin exchange operator. +% +% Arguments +% --------- +% spin : :class:`double` +% halfinteger or integer spin label, defaults to :code:`1/2`. +% +% symmetry : :class:`char` +% symmetry group ('Z1' or 'SU2'), defaults to :code:`'SU2'`. +% +% Returns +% S : :class:`.Tensor` +% two-site exchange interaction represented as a 4-leg tensor. + +arguments + spin = 1/2 + symmetry = 'Z1' +end + +switch symmetry +% case 'Z1' + + case 'SU2' + pspace = GradedSpace.new(SU2(2 * spin + 1), 1, false); + aspace = GradedSpace.new(SU2(3), 1, false); + + Sleft = Tensor.ones(pspace, [pspace aspace]); + Sright = -Tensor.ones([aspace pspace], pspace); + + S = (spin^2 + spin) * contract(Sleft, [-1 1 -4], Sright, [1 -2 -3], 'Rank', [2 2]); + + otherwise + error('models:TBA', 'not implemented'); +end + +end + diff --git a/src/models/spinoperators/sigma_min.m b/src/models/spinoperators/sigma_min.m new file mode 100644 index 0000000..87aaa23 --- /dev/null +++ b/src/models/spinoperators/sigma_min.m @@ -0,0 +1,43 @@ +function S = sigma_min(spin, symmetry) +% Spin lowering operator. +% +% Arguments +% --------- +% spin : :class:`double` +% halfinteger or integer spin label, defaults to :code:`1/2`. +% +% symmetry : :class:`char` +% symmetry group ('Z1' or 'U1'), defaults to :code:`'Z1'`. +% +% Returns +% S : :class:`.Tensor` +% lowering operator represented as a 3-leg tensor. + +arguments + spin = 1/2 + symmetry = 'Z1' +end + +switch symmetry + case 'Z1' + + case 'U1' + charges = U1((-2 * spin):2:(2 * spin)); + degeneracies = ones(size(charges)); + pspace = GradedSpace.new(charges, degeneracies, false); + aspace = GradedSpace.new(U1(2), 1, false); + + S = Tensor.zeros([pspace aspace], pspace); + [mblocks, bcharges] = matrixblocks(S); + for i = 1:length(charges) + if charges(i) == U1(-2 * spin), continue; end + mblocks{bcharges == charges(i)} = 2 * pauliterm(spin, i - 1, i); + end + S = S.fill_matrix(mblocks, bcharges); + + otherwise + error('models:TBA', 'not implemented'); +end + +end + diff --git a/src/models/spinoperators/sigma_plus.m b/src/models/spinoperators/sigma_plus.m new file mode 100644 index 0000000..55c5617 --- /dev/null +++ b/src/models/spinoperators/sigma_plus.m @@ -0,0 +1,43 @@ +function S = sigma_plus(spin, symmetry) +% Spin raising operator. +% +% Arguments +% --------- +% spin : :class:`double` +% halfinteger or integer spin label, defaults to :code:`1/2`. +% +% symmetry : :class:`char` +% symmetry group ('Z1' or 'U1'), defaults to :code:`'Z1'`. +% +% Returns +% S : :class:`.Tensor` +% raising operator represented as a 3-leg tensor. + +arguments + spin = 1/2 + symmetry = 'Z1' +end + +switch symmetry +% case 'Z1' + + case 'U1' + charges = U1((-2 * spin):2:(2 * spin)); + degeneracies = ones(size(charges)); + pspace = GradedSpace.new(charges, degeneracies, false); + aspace = GradedSpace.new(U1(-2), 1, false); + + S = Tensor.zeros([pspace aspace], pspace); + [mblocks, bcharges] = matrixblocks(S); + for i = 1:length(charges) + if charges(i) == U1(2 * spin), continue; end + mblocks{charges(i) == bcharges} = 2 * pauliterm(spin, i, i + 1); + end + S = S.fill_matrix(mblocks, bcharges); + + otherwise + error('models:TBA', 'not implemented'); +end + +end + diff --git a/src/models/spinoperators/sigma_z.m b/src/models/spinoperators/sigma_z.m new file mode 100644 index 0000000..43972bc --- /dev/null +++ b/src/models/spinoperators/sigma_z.m @@ -0,0 +1,25 @@ +function S = sigma_z(spin, symmetry) +arguments + spin = 1/2 + symmetry = 'Z1' +end + +switch symmetry + case 'U1' + charges = U1((-2 * spin):2:(2 * spin)); + degeneracies = ones(size(charges)); + pspace = GradedSpace.new(charges, degeneracies, false); + + S = Tensor.zeros(pspace, pspace); + [mblocks, bcharges] = matrixblocks(S); + for i = 1:length(charges) + mblocks{charges(i) == bcharges} = spin + 1 - i; + end + S = S.fill_matrix(mblocks, bcharges); + + otherwise + error('models:TBA', 'not implemented'); +end + +end + diff --git a/src/models/statmech2dIsing.m b/src/models/statmech2dIsing.m new file mode 100644 index 0000000..ee0f13c --- /dev/null +++ b/src/models/statmech2dIsing.m @@ -0,0 +1,81 @@ +function O = statmech2dIsing(kwargs) +% MPO encoding the transfer matrix of the partition function of the 2D classical Ising model +% +% .. math:: +% \mathcal{Z} = \sum_{\{s\}} \prod_{\langle ij \rangle} \exp \left( \beta s_i s_j \right). +% +% Keyword arguments +% ----------------- +% 'beta' : :class:`double` +% inverse temperature. +% +% 'L' : :class:`int` +% system size, defaults to :code:`Inf`. +% +% 'Symmetry' : :class:`char` +% symmetry group ('Z1', 'Z2'), defaults to :code:`'Z1'`. +% +% Returns +% ------- +% mpo : :class:`.InfMpo` or :class:`.Finite` +% MPO transfer matrix of the Ising partition function. + +arguments + kwargs.beta = log(1 + sqrt(2)) / 2; + kwargs.L = Inf % size of system + kwargs.Symmetry {mustBeMember(kwargs.Symmetry, {'Z1', 'Z2'})} = 'Z1' +end + +if ~isfinite(kwargs.L) + O = InfMpo({MpoTensor(bulk_mpo(kwargs.beta, kwargs.Symmetry))}); + +else + assert(kwargs.L > 3, 'needs to be implemented'); + r = boundary_mpo(kwargs.beta, kwargs.Symmetry); + l = r'; + o = bulk_mpo(kwargs.beta, kwargs.Symmetry); + + O = FiniteMpo(MpsTensor(l), repmat({MpoTensor(o)}, 1, kwargs.L-2), MpsTensor(r)); +end + +end + +function O = bulk_mpo(beta, symmetry) + +if strcmp(symmetry, 'Z1') + sz = [2 2 2 2]; + t = sqrtm([exp(beta) exp(-beta); exp(-beta) exp(beta)]); + o = contract(diracdelta(sz), 1:4, t, [-1 1], t, [-2 2], t, [-3 3], t, [-4 4]); + O = fill_tensor(Tensor.zeros(sz), o); + +elseif strcmp(symmetry, 'Z2') + s = GradedSpace.new(Z2(0, 1), [1 1], false); + t = sqrtm(fill_matrix(Tensor(s, s), num2cell(2 * [cosh(beta) sinh(beta)]))); + o = Tensor.ones([s s], [s s]) / 2; + + O = contract(o, 1:4, t, [-1 1], t, [-2 2], t, [3 -3], t, [4 -4], 'Rank', [2 2]); +else + error('invalid symmetry'); +end + +end + +function O = boundary_mpo(beta, symmetry) + +if strcmp(symmetry, 'Z1') + sz = [2 2 2]; + t = sqrtm([exp(beta) exp(-beta); exp(-beta) exp(beta)]); + o = contract(diracdelta(sz), 1:3, t, [-1 1], t, [-2 2], t, [-3 3]); + O = fill_tensor(Tensor.zeros(sz), o); + +elseif strcmp(symmetry, 'Z2') + s = GradedSpace.new(Z2(0, 1), [1 1], false); + t = sqrtm(fill_matrix(Tensor(s, s), num2cell(2 * [cosh(beta) sinh(beta)]))); + o = Tensor.ones([s s], s) / sqrt(2); + + O = contract(o, 1:3, t, [-1 1], t, [-2 2], t, [3 -3], 'Rank', [2 1]); +else + error('invalid symmetry'); +end + +end \ No newline at end of file diff --git a/src/models/statmech2dIsing_free_energy.m b/src/models/statmech2dIsing_free_energy.m new file mode 100644 index 0000000..44c564a --- /dev/null +++ b/src/models/statmech2dIsing_free_energy.m @@ -0,0 +1,15 @@ +function f = statmech2dIsing_free_energy(beta) +% Compute the free energy of the classical 2d Ising model using Onsagers solution. + +INTEGRAL_STEP_SIZE = 1e-6; + +theta = 0:INTEGRAL_STEP_SIZE:(pi / 2); + +sh = sinh(2 * beta); +ch = cosh(2 * beta); +x = 2 * sh / ch^2; + +f = -1 / beta * (log(2 * cosh(2 * beta)) + ... + 1 / pi * trapz(theta, log(1/2 * (1 + sqrt(1 - x^2 * sin(theta).^2))))); + +end diff --git a/src/mps/FiniteMpo.m b/src/mps/FiniteMpo.m new file mode 100644 index 0000000..ce18dca --- /dev/null +++ b/src/mps/FiniteMpo.m @@ -0,0 +1,309 @@ +classdef FiniteMpo + % Finite matrix product operator. + % + % Properties + % ---------- + % L : :class:`.MpsTensor` + % left end tensor. + % + % O : :class:`cell` of :class:`.MpoTensor` or :class:`.PepsSandwich` + % bulk MPO tensors. + % + % R : :class:`.MpsTensor` + % right end tensor. + % + % Todo + % ---- + % Document. + + properties + L MpsTensor + O + R MpsTensor + end + + methods + function mpo = FiniteMpo(L, O, R) + if nargin == 0, return; end + if ~iscell(O), O = {O}; end + mpo.L = L; + mpo.O = O; + mpo.R = R; + end + + function v = apply(mpo, v) + N = length(mpo); + for d = 1:depth(mpo) + if N == 0 + v = applytransfer(mpo(d).L, mpo(d).R, v); + elseif N == 1 + v = applychannel(mpo(d).O{1}, mpo(d).L, mpo(d).R, v); + else + v = applympo(mpo(d).O{:}, mpo(d).L, mpo(d).R, v); + end + end + end + + function v = apply_regularized(mpo, fp1, fp2, v) + v = apply(mpo, v); + v = v - overlap(v, fp2) * repartition(fp1, rank(v)); + end + + function varargout = eigsolve(mpo, v0, howmany, sigma, options) + arguments + mpo + v0 = [] + howmany = 1 + sigma = 'largestabs' + + options.Tol = eps(underlyingType(mpo))^(3/4) + options.Algorithm = 'Arnoldi' + options.MaxIter = 100 + options.KrylovDim = 20 + options.IsSymmetric logical = false + options.DeflateDim + options.ReOrth = 2 + options.NoBuild + options.Verbosity = 0 + end + + if isempty(v0), v0 = initialize_fixedpoint(mpo(1)); end + + kwargs = namedargs2cell(options); + [varargout{1:nargout}] = ... + eigsolve(@(x) mpo.apply(x), v0, howmany, sigma, kwargs{:}); + end + + function v = initialize_fixedpoint(mpo) + % Initialize a dense tensor for the fixedpoint of a :class:`.FiniteMPO`. + + N = prod(cellfun(@(x) size(x, 4), mpo.O)); + for i = N:-1:1 + v(i) = MpsTensor.randnc(domain(slice(mpo, i, 1:N)), []); + end + end + + function mps = initialize_mps(mpo, kwargs) + arguments + mpo + kwargs.MaxVspace + end + + pspaces = arrayfun(@(x) pspace(mpo, x), 1:length(mpo), 'UniformOutput', false); + + vspacefirst = rightvspace(mpo.L)'; + vspacelast = leftvspace(mpo.R); + + newkwargs = namedargs2cell(kwargs); + mps = FiniteMps.randnc(pspaces{:}, 'LeftVspace', vspacefirst, ... + 'RightVspace', vspacelast, newkwargs{:}); + mps = normalize(mps); + end + + function envs = initialize_envs(mpo) + arguments + mpo + end + + GL = cell(1, length(mpo) + 1); + GR = cell(1, length(mpo) + 1); + + GL{1} = mpo.L; + GR{end} = mpo.R; + + envs = FiniteEnvironment(GL, GR); + end + + function T = transfermatrix(mpo, mps1, mps2, sites) + arguments + mpo + mps1 + mps2 = mps1 + sites = 1:length(mps1) + end + + assert(all(diff(sites) == 1), 'sites must be neighbouring and increasing.'); + A1 = mps1.A(sites); + A2 = mps2.A(sites); + O = mpo.O(sites); %#ok + T = FiniteMpo.mps_channel_operator(A1, O, A2); %#ok + end + + function H = AC_hamiltonian(mpo, mps, envs, pos) + envs = movegaugecenter(envs, mpo, mps, mps, pos); + H = FiniteMpo(envs.GL{pos}, mpo.O(pos), envs.GR{pos + 1}); + end + + function s = pspace(mpo, x) + s = pspace(mpo.O{x}); + end + + function s = domain(mpo) + sO = flip(cellfun(@(x) domainspace(x), mpo(1).O, 'UniformOutput', false)); + s = [leftvspace(mpo(1).R) [sO{:}] rightvspace(mpo(1).L)]'; + end + + function s = codomain(mpo) + sO = cellfun(@(x) codomainspace(x), mpo(1).O, 'UniformOutput', false); + s = [leftvspace(mpo(end).L) [sO{:}] rightvspace(mpo(end).R)]; + end + + function d = depth(mpo) + d = builtin('length', mpo); + end + + function l = length(mpo) + l = length(mpo(1).O); + end + + function type = underlyingType(mpo) + type = underlyingType(mpo(1).L); + end + + function mpo = ctranspose(mpo) + if depth(mpo) > 1 + mpo = flip(mpo); + end + + for d = 1:depth(mpo) + mpo(d).L = tpermute(mpo(d).L', ... + [1, flip(2:nspaces(mpo(d).L)-1), nspaces(mpo(d).L)]); + mpo(d).O = cellfun(@ctranspose, mpo(d).O, ... + 'UniformOutput', false); + mpo(d).R = tpermute(mpo(d).R', ... + [1, flip(2:nspaces(mpo(d).R)-1), nspaces(mpo(d).R)]); + end + end + + function mpo = transpose(mpo) + if depth(mpo) > 1 + mpo = flip(mpo); + end + + for d = 1:depth(mpo) + [mpo(d).L, mpo(d).R] = swapvars(mpo(d).L, mpo(d).R); + mpo(d).O = cellfun(@transpose, ... + fliplr(mpo(d).O), 'UniformOutput', false); + end + end + + function mpo = slice(mpo, i, j) + if isempty(mpo(1).O), return; end + if strcmp(i, ':') + i = 1:size(mpo(end).O, 2); + end + if strcmp(j, ':') + j = 1:size(mpo(1).O{1}, 4); + end + assert(all(1 <= i) && all(i <= size(mpo(end).O{1}, 2))); + assert(all(1 <= j) && all(j <= size(mpo(1).O{1}, 4))); + + mpo(end).O{1} = mpo(end).O{1}(:, i, :, :); + mpo(1).O{1} = mpo(1).O{1}(:, :, :, j); + end + + function bool = iszero(mpo) + if isempty(mpo(1).O) + bool = false; + return + end + for i = 1:depth(mpo) + bool = any(cellfun(@nnz, mpo(i).O) == 0); + if bool, return; end + end + end + + function bool = iseye(mpo, i) + if isempty(mpo(1).O) + bool = true; + return + end + for d = 1:depth(mpo) + bool = all(cellfun(@(x) iseye(x(:,i,:,i)), mpo(d).O)); + if ~bool, return; end + end + end +% +% function v = applyleft(mpo, v) +% +% end +% +% function mpo1 = plus(mpo1, mpo2) +% +% end +% +% function v = applyright(mpo, v) +% arguments +% mpo +% v MpsTensor +% end +% +% assert(depth(mpo) == 1, 'mps:TBA', 'Not implemented yet.'); +% assert(v.plegs == width(mpo), 'mps:ArgError', 'Incompatible sizes.'); +% +% w = width(mpo); +% +% mpopart = cell(2, w); +% for i = 1:w +% mpopart{1, i} = mpo.O{i}; +% mpopart{2, i} = [2 * i, 2 * (i + 1) + 1, -(1 + i), 2 * i + 1]; +% end +% +% v = MpsTensor(contract(... +% v, [1, 2:2:(2 * w), 2 * (w + 1), -(1:v.alegs) - (w + 2)], ... +% mpo.L, [-1 3 1], ... +% mpo.R, [-(w + 2), 2 * (w + 1) + 1, 2 * (w + 1)], ... +% mpopart{:}, 'Rank', rank(v))); +% end +% + function t = Tensor(mpo) + assert(depth(mpo) == 1, 'not implemented for 1 < depth'); + W = length(mpo); + if W == 0 + t = contracttransfer(mpo.L, mpo.R); + else + t = contractmpo(mpo.O{:}, mpo.L, mpo.R); + end + end + + end + + methods (Static) + function mpo = randnc(pspaces, vspaces) + assert(length(pspaces) == length(vspaces) + 1); + + L = MpsTensor(Tensor.randnc([pspaces(1) vspaces(1)'], pspaces(1))); + O = cell(1, length(pspaces)-2); + for i = 1:length(O) + O{i} = MpoTensor(Tensor.randnc([vspaces(i) pspaces(i+1)], ... + [pspaces(i+1) vspaces(i+1)])); + end + R = MpsTensor(Tensor.randnc([pspaces(end)' vspaces(end)], pspaces(end)')); + mpo = FiniteMpo(L, O, R); + end + + function T = mps_channel_operator(Atop, O, Abot) + arguments + Atop % top mps tensors + O % cell of mpotensors (unrotated) + Abot % bottom mps tensors (unconjugated) + end + + assert(length(Atop) == length(O) && length(O) == length(Abot), ... + 'FiniteMpo:argerror', 'unmatching sizes'); + + for i = numel(Atop):-1:1 + atop = Atop{i}; + o = rot90(O{i}); + twistinds = find(isdual(space(atop, 2:(nspaces(atop) - 1 - atop.alegs)))); + abot = twist(Abot{i}, twistinds + 1)'; % IMPORTANT: ' needs to be outside of twist + + assert(isequal(pspace(abot)', leftvspace(o))); + assert(isequal(rightvspace(o)', pspace(atop))); + + T(i) = FiniteMpo(abot, {o}, atop); + end + end + end +end + diff --git a/src/mps/FiniteMps.m b/src/mps/FiniteMps.m new file mode 100644 index 0000000..1eeaeb2 --- /dev/null +++ b/src/mps/FiniteMps.m @@ -0,0 +1,328 @@ +classdef FiniteMps + % Finite matrix product state. + % + % Properties + % ---------- + % A : :class:`cell` of :class:`.MpsTensor` + % set of tensors that define a finite MPS. + % + % center : :class:`int` + % location of center gauge, such that every tensor to the left (right) of + % :code:`center` is in left (right) gauge. + % + % Todo + % ---- + % Document. + + properties + A (1,:) cell + center + end + + %% Constructors + methods + function mps = FiniteMps(varargin) + if nargin == 0, return; end + if nargin == 1 + if iscell(varargin{1}) + mps.A = varargin{1}; + else + mps.A = varargin(1); + end + elseif nargin == 2 + if iscell(varargin{1}) + mps.A = varargin{1}; + else + mps.A = varargin(1); + end + mps.center = varargin{2}; + end + end + end + + methods (Static) + function mps = new(fun, pspaces, kwargs) + arguments + fun + end + arguments (Repeating) + pspaces + end + arguments + kwargs.L + kwargs.MaxVspace + kwargs.LeftVspace = one(pspaces{1}) + kwargs.RightVspace = one(pspaces{1}) + end + + if isempty(fun), fun = @randnc; end + + if isfield(kwargs, 'L') + pspaces = repmat(pspaces, 1, kwargs.L); + end + if isfield(kwargs, 'MaxVspace') && ~iscell(kwargs.MaxVspace) + kwargs.MaxVspace = {kwargs.MaxVspace}; + end + + L = length(pspaces); + + vspacesL = cell(1, L + 1); + vspacesL{1} = kwargs.LeftVspace; + for i = 1:L + vspacesL{i+1} = vspacesL{i} * pspaces{i}; + if isfield(kwargs, 'MaxVspace') + vspacesL{i + 1} = infimum(vspacesL{i + 1}, ... + kwargs.MaxVspace{mod1(i + 1, length(kwargs.MaxVspace))}); + end + end + + vspacesR = cell(1, L + 1); + vspacesR{end} = kwargs.RightVspace; + for i = L:-1:1 + vspacesR{i} = pspaces{i}' * vspacesR{i+1}; + if isfield(kwargs, 'MaxVspace') + vspacesR{i} = infimum(vspacesR{i}, ... + kwargs.MaxVspace{mod1(i, length(kwargs.MaxVspace))}); + end + end + + vspaces = cellfun(@infimum, vspacesL, vspacesR, 'UniformOutput', false); + + for w = length(pspaces):-1:1 + rankdeficient = vspaces{w} * pspaces{w} < vspaces{w + 1} || ... + vspaces{w} > pspaces{w}' * vspaces{w + 1}; + if rankdeficient + error('mps:rank', ... + 'Cannot create a full rank mps with given spaces.'); + end + + A{w} = MpsTensor.new(fun, [vspaces{w} pspaces{w}], vspaces{w + 1}); + end + mps = FiniteMps(A); + end + + function mps = new_from_spaces(fun, spaces, kwargs) + arguments + fun + end + arguments (Repeating) + spaces + end + arguments + kwargs.MaxVspace + end + + if isfield(kwargs, 'MaxVspace') && ~iscell(kwargs.MaxVspace) + kwargs.MaxVspace = {kwargs.MaxVspace}; + end + + assert(mod(length(spaces) - 1, 2)); + if isempty(fun), fun = @randnc; end + + L = (length(spaces) - 1) / 2; + + for w = L:-1:1 + Vleft = spaces{2 * w - 1}; + P = spaces{2 * w}; + Vright = spaces{2 * w + 1}; + rankdeficient = Vleft * P < Vright || Vleft > P' * Vright; + if rankdeficient + error('mps:rank', ... + 'Cannot create a full rank mps with given spaces.'); + end + A{w} = MpsTensor.new(fun, [Vleft P], Vright); + end + + mps = FiniteMps(A); + end + + function mps = randnc(varargin) + mps = FiniteMps.new(@randnc, varargin{:}); + end + end + + + %% Properties + methods + function p = length(mps) + p = length(mps.A); + end + + function [svals, charges] = schmidt_values(mps, w) + arguments + mps + w {mustBeInteger} = floor(length(mps) / 2) + end + + assert(0 <= w && w <= length(mps)); + if isempty(mps.center), mps = movegaugecenter(mps, w); end + + if w < mps.center + mps = movegaugecenter(mps, w + 1); + S = tsvd(mps.A(w + 1), 1, 2:nspaces(mps.A(w + 1))); + else + mps = movegaugecenter(mps, w); + S = tsvd(mps.A(w), 1:nspaces(mps.A(w))-1, nspaces(mps.A(w))); + end + [svals, charges] = matrixblocks(S); + svals = cellfun(@diag, svals, 'UniformOutput', false); + end + end + + + %% Derived operators + methods + function T = transfermatrix(mps1, mps2, sites) + arguments + mps1 + mps2 = mps1 + sites = 1:length(mps1) + end + + assert(all(diff(sites) == 1), 'sites must be neighbouring and increasing.'); + T = cellfun(@transfermatrix, mps1.A(sites), mps2.A(sites)); + end + end + + + %% Methods + methods + function mps = movegaugecenter(mps, newcenter, alg) + arguments + mps + newcenter {mustBeInteger} = floor(length(mps) / 2) + alg = 'polar' + end + + assert(newcenter >= 1 && newcenter <= length(mps), ... + 'mps:gauge', ... + sprintf('new center (%d) must be in 1:%d', newcenter, length(mps))); + + if isempty(mps.center) + low = 1; + high = length(mps); + else + low = mps.center; + high = mps.center; + end + + for i = low:(newcenter - 1) + [mps.A{i}, L] = leftorth(mps.A{i}, alg); + mps.A{i + 1} = multiplyleft(mps.A{i + 1}, L); + end + for i = high:-1:(newcenter + 1) + [R, mps.A{i}] = rightorth(mps.A{i}, alg); + [mps.A{i - 1}] = multiplyright(mps.A{i - 1}, R); + end + mps.center = newcenter; + end + + function rho = fixedpoint(mps, type, w) + arguments + mps + type {mustBeMember(type, {'l', 'r'})} + w = floor(length(mps) / 2); + end + + if strcmp(type, 'l') + if mps.center > w + rho = mps.A.eye(leftvspace(mps, w), leftvspace(mps, w)); + else + T = transfermatrix(mps, mps, mps.center:w); + rho = apply(T, []); + end + else + if mps.center < w + rho = mps.A.eye(rightvspace(mps, w)', rightvspace(mps, w)'); + else + T = transfermatrix(mps, mps, w:mps.center).'; + rho = apply(T, []); + end + end + + end + + function o = overlap(mps1, mps2, rholeft, rhoright) + arguments + mps1 + mps2 + rholeft = [] + rhoright = [] + end + + assert(length(mps1) == length(mps2), 'mps should have equal length'); + assert(length(mps1) >= 2, 'edge case to be added'); + + n = floor(length(mps1) / 2); + Tleft = transfermatrix(mps1, mps2, 1:n); + rholeft = apply(Tleft, rholeft); + Tright = transfermatrix(mps1, mps2, n+1:length(mps1)).'; + rhoright = apply(Tright, rhoright); + + o = overlap(rholeft, rhoright); + end + + function E = expectation_value(mps1, O, mps2) + arguments + mps1 + O + mps2 = mps1 + end + + if isa(O, 'FiniteMpo') + n = floor(length(mps1) / 2); + Tleft = transfermatrix(O, mps1, mps2, 1:n); + rholeft = apply(Tleft, O.L); + Tright = transfermatrix(O, mps1, mps2, n+1:length(mps1)).'; + rhoright = apply(Tright, O.R); + + E = overlap(rholeft, rhoright); + + else + error('unknown operator'); + end + + end + + function n = norm(mps) + if isempty(mps.center) + n = sqrt(abs(overlap(mps, mps))); + return + end + + n = norm(mps.A{mps.center}); + end + + function [mps, n] = normalize(mps) + if isempty(mps.center), mps = movegaugecenter(mps); end + n = norm(mps); + mps.A{mps.center} = mps.A{mps.center} ./ n; + end + end + + + %% Converters + methods + function psi = Tensor(mps) + % Convert a finite mps to a dense tensor. + + tensors = mps.A; + indices = cell(size(tensors)); + + nout = nspaces(mps.A{1}) - 1; + indices{1} = [-(1:nout), 1]; + + for i = 2:length(indices)-1 + plegs = mps.A{i}.plegs; + indices{i} = [i-1 -(1:plegs)-nout i]; + nout = nout + plegs; + end + + plegs = mps.A{end}.plegs; + indices{end} = [length(indices)-1 -(1:plegs+1)-nout]; + + args = [tensors; indices]; + psi = contract(args{:}); + end + end +end diff --git a/src/mps/InfJMpo.m b/src/mps/InfJMpo.m new file mode 100644 index 0000000..06b820c --- /dev/null +++ b/src/mps/InfJMpo.m @@ -0,0 +1,420 @@ +classdef InfJMpo < InfMpo + % Infinite translation invariant matrix product operator with a Jordan block structure. + % + % Todo + % ---- + % Document. + + methods + function mpo = InfJMpo(varargin) + mpo@InfMpo(varargin{:}); + if nargin > 0 + assert(istriu(mpo.O{1})); + assert(iseye(mpo.O{1}(1, 1, 1, 1)) && iseye(mpo.O{1}(end, 1, end, 1))); + assert(isconnected(mpo)); + end + end + + function bool = isconnected(mpo) + bool = true; + end + + function [GL, lambda] = leftenvironment(mpo, mps1, mps2, GL, linopts) + arguments + mpo + mps1 + mps2 = mps1 + GL = cell(1, period(mps1)) + linopts.Algorithm = 'bicgstab' + linopts.MaxIter = 500 + linopts.Verbosity = Verbosity.warn + linopts.Tol = eps(underlyingType(mps1))^(3/4) + end + + linkwargs = namedargs2cell(linopts); + L = period(mps1); + + T = transfermatrix(mpo, mps1, mps2, 'Type', 'LL'); + fp_left = fixedpoint(mpo, mps1, 'l_LL', 1); + fp_right = fixedpoint(mpo, mps1, 'r_LL', L); + + if isempty(GL) || isempty(GL{1}) % initialize environment + GL = cell(1, period(mps1)); + GL{1} = repartition(MpsTensor(SparseTensor.zeros(domain(T), [])), ... + rank(fp_left)); + GL{1}.var(1) = fp_left; + end + + for i = 2:size(GL{1}.var, 2) % TODO: specify interface to get dimensions of single MpsTensor + rhs = apply(slice(T, i, 1:i-1), slice(GL{1}, 1, 1:i-1, 1)); + Tdiag = slice(T, i, i); + if iszero(Tdiag) + GL{1}.var(i) = rhs; + else + if iseye(T, i) + lambda = overlap(rhs, fp_right); + rhs = rhs - lambda * fp_left; + end + [GL{1}.var(i), flag, relres, iter] = linsolve(@(x) x - apply(Tdiag, x), rhs, GL{1}.var(i), ... + linkwargs{:}); + if iseye(T, i) + GL{1}.var(i) = GL{1}.var(i) - overlap(GL{1}.var(i), fp_right) * fp_left; + end + + if flag == 1 && linopts.Verbosity >= Verbosity.warn + fprintf('GL(%d) reached maxiter, error = %3e\n', i, relres); + elseif flag == 2 && linopts.Verbosity >= Verbosity.warn + fprintf('GL(%d) stagnated, error = %3e\n', i, relres); + elseif linopts.Verbosity >= Verbosity.conv + fprintf('GL(%d) converged after %d iterations, error = %3e\n', i, iter, relres); + end + end + end + + if nnz(GL{1}.var) == numel(GL{1}.var) + GL{1}.var = full(GL{1}.var); + end + + for w = 1:period(mps1)-1 + T = transfermatrix(mpo, mps1, mps2, w, 'Type', 'LL'); + GL{next(w, period(mps1))} = apply(T, GL{w}); + end + end + + function [GR, lambda] = rightenvironment(mpo, mps1, mps2, GR, linopts) + arguments + mpo + mps1 + mps2 = mps1 + GR = cell(1, period(mps1)) + linopts.Algorithm = 'bicgstab' + linopts.MaxIter = 500 + linopts.Verbosity = Verbosity.warn + linopts.Tol = eps(underlyingType(mps1))^(3/4) + end + + linkwargs = namedargs2cell(linopts); + L = period(mpo); + + fp_left = fixedpoint(mpo, mps1, 'l_RR', 1); + fp_right = repartition(fixedpoint(mpo, mps1, 'r_RR', L), [3 0]); + + T = transfermatrix(mpo, mps1, mps2, 'Type', 'RR').'; + N = size(T(1).O{1}, 2); + + if isempty(GR) || isempty(GR{1}) + GR = cell(1, period(mps1)); + GR{1} = MpsTensor(SparseTensor.zeros(domain(T), [])); + GR{1}.var(1, N, 1) = fp_right; + end + + for i = N-1:-1:1 + rhs = apply(slice(T, i, i+1:N), slice(GR{1}, 1, i+1:N, 1)); + Tdiag = slice(T, i, i); + if iszero(Tdiag) + GR{1}.var(i) = rhs; + else + if iseye(T, i) + lambda = overlap(rhs, fp_left); + rhs = rhs - lambda * fp_right; + end + [GR{1}.var(i), flag, relres, iter] = ... + linsolve(@(x) x - apply(Tdiag, x), rhs, GR{1}.var(i), linkwargs{:}); + if iseye(T, i) + GR{1}.var(i) = GR{1}.var(i) - ... + overlap(GR{1}.var(i), fp_left) * fp_right; + end + + if flag == 1 && linopts.Verbosity >= Verbosity.warn + fprintf('GR(%d) reached maxiter, error = %3e\n', i, relres); + elseif flag == 2 && linopts.Verbosity >= Verbosity.warn + fprintf('GR(%d) stagnated, error = %3e\n', i, relres); + elseif linopts.Verbosity >= Verbosity.conv + fprintf('GR(%d) converged after %d iterations, error = %3e\n', i, iter, relres); + end + end + end + + if nnz(GR{1}.var) == numel(GR{1}.var) + GR{1}.var = full(GR{1}.var); + end + + for w = period(mps1):-1:2 + T = transfermatrix(mpo, mps1, mps2, w, 'Type', 'RR').'; + GR{w} = apply(T, GR{next(w, period(mps1))}); + end + end + + function GBL = leftquasienvironment(mpo, qp, GL, GR, linopts) + arguments + mpo + qp + GL + GR + linopts.Algorithm = 'bicgstab' + linopts.MaxIter = 500 + linopts.Verbosity = Verbosity.warn + linopts.Tol = eps(underlyingType(qp))^(3/5) + end + + linkwargs = namedargs2cell(linopts); + expP = exp(-1i * qp.p); + L = period(mpo); + + needsRegularization = istrivial(qp); + if needsRegularization || true + fp_left = fixedpoint(mpo, qp, 'l_RL_0', 1); + fp_right = fixedpoint(mpo, qp, 'r_RL_1', L); + end + + T = transfermatrix(mpo, qp, qp, 'Type', 'RL'); + TB = transfermatrix(mpo, qp, qp, 'Type', 'BL'); + + % initialize and precompute GL * TB + GBL = cell(size(GL)); + GBL{1} = MpsTensor(SparseTensor.zeros(domain(T), auxspace(qp, 1)), 1); + for w = 1:L + GBL{next(w, L)} = ... + (apply(TB(w), GL{w}) + apply(T(w), GBL{w})) * (expP^(1/L)); + end + + N = size(GBL{1}.var, 2); % TODO: specify interface to get dimensions of single MpsTensor + for i = 2:N % GBL{1}(1) = 0 because of quasiparticle gauge + rhs = apply(slice(T, i, 1:i-1), slice(GBL{1}, 1, 1:i-1, 1, 1)) * expP; + rhs = rhs + slice(GBL{1}, i); + + Tdiag = slice(T, i, i); + if iszero(Tdiag) + GBL{1}.var(i) = rhs; + else + if needsRegularization && iseye(T, i) + lambda = overlap(rhs, fp_right); + fp_left = repartition(fp_left, rank(rhs)); + rhs = rhs - lambda * fp_left; + H_effective = @(x) x - expP * ... + apply_regularized(Tdiag, fp_left, fp_right, x); + else + H_effective = @(x) x - expP * apply(Tdiag, x); + end + + [GBL{1}.var(i), flag, relres, iter, resvec] = ... + linsolve(H_effective, rhs, [], linkwargs{:}); + + if flag == 1 && linopts.Verbosity >= Verbosity.warn + fprintf('GBL(%d) reached maxiter, error = %3e\n', i, relres); + elseif flag == 2 && linopts.Verbosity >= Verbosity.warn + fprintf('GBL(%d) stagnated, error = %3e\n', i, relres); + elseif linopts.Verbosity >= Verbosity.conv + fprintf('GBL(%d) converged after %.1f iterations, error = %3e\n', i, iter, relres); + end + end + end + + if nnz(GBL{1}) == numel(GBL{1}.var) + GBL{1}.var = full(GBL{1}.var); + end + + GBL{1} = MpsTensor(GBL{1}, 1); + for w = 1:L-1 + GBL{next(w, L)} = expP^(1 / L) * ... + (apply(TB(w), GL{w}) + apply(T(w), GBL{w})); + end + end + + function GBR = rightquasienvironment(mpo, qp, GL, GR, linopts) + arguments + mpo + qp + GL + GR + linopts.Algorithm = 'bicgstab' + linopts.MaxIter = 500 + linopts.Verbosity = Verbosity.warn + linopts.Tol = eps(underlyingType(qp))^(3/5) + end + + linkwargs = namedargs2cell(linopts); + expP = exp(+1i*qp.p); + L = period(mpo); + + needsRegularization = istrivial(qp); + if needsRegularization || true + fp_left = fixedpoint(mpo, qp, 'l_LR_1', 1); + fp_right = repartition(fixedpoint(mpo, qp, 'r_LR_0', L), [3, 1]); + end + + T = transfermatrix(mpo, qp, qp, 'Type', 'LR').'; + TB = transfermatrix(mpo, qp, qp, 'Type', 'BR').'; + + GBR = cell(size(GR)); + GBR{1} = MpsTensor(SparseTensor.zeros(domain(T), auxspace(qp, 1)), 1); + for w = L:-1:1 + ww = next(w, L); + TB_w = transfermatrix(mpo, qp, qp, w, 'Type', 'BR').'; + T_w = transfermatrix(mpo, qp, qp, w, 'Type', 'LR').'; + GBR{w} = expP^(1/L) * (apply(TB_w, GR{ww}) + apply(T_w, GBR{ww})); + end + + N = size(GBR{1}.var, 2); % TODO: specify interface to get dimensions of single MpsTensor + for i = N:-1:1 + if i == N + rhs = slice(GBR{1}, 1, i, 1, 1); + else + rhs = apply(slice(T, i, i+1:N), slice(GBR{1}, 1, i+1:N, 1, 1)) * expP; + rhs = rhs + slice(GBR{1}, 1, i, 1, 1); + end + + Tdiag = slice(T, i, i); + if iszero(Tdiag) + GBR{1}.var(i) = rhs; + else + if needsRegularization && iseye(T, i) + lambda = overlap(rhs, fp_left); + fp_right = repartition(fp_right, rank(rhs)); + rhs = rhs - lambda * fp_right; + H_effective = @(x) x - expP * ... + apply_regularized(Tdiag, fp_right, fp_left, x); + else + H_effective = @(x) x - expP * apply(Tdiag, x); + end + + [GBR{1}.var(i), flag, relres, iter, resvec] = ... + linsolve(H_effective, rhs, [], linkwargs{:}); + + if flag == 1 && linopts.Verbosity >= Verbosity.warn + fprintf('GBR(%d) reached maxiter, error = %3e\n', i, relres); + elseif flag == 2 && linopts.Verbosity >= Verbosity.warn + fprintf('GBR(%d) stagnated, error = %3e\n', i, relres); + elseif linopts.Verbosity >= Verbosity.conv + fprintf('GBR(%d) converged after %.1f iterations, error = %3e\n', i, iter, relres); + end + end + end + + if nnz(GBR{1}) == numel(GBR{1}) + GBR{1} = full(GBR{1}); + end + + GBR{1} = MpsTensor(GBR{1}, 1); + for w = L:-1:2 + ww = next(w, L); + TB_w = transfermatrix(mpo, qp, qp, w, 'Type', 'BR').'; + T_w = transfermatrix(mpo, qp, qp, w, 'Type', 'LR').'; + GBR{w} = expP^(1/L) * (apply(TB_w, GR{ww}) + apply(T_w, GBR{ww})); + end + end + + function [GL, GR, lambda] = environments(mpo, mps1, mps2, GL, GR, linopts) + arguments + mpo + mps1 + mps2 = mps1 + GL = cell(1, period(mps1)) + GR = cell(1, period(mps1)) + linopts.Algorithm = 'bicgstab' + linopts.MaxIter = 500 + linopts.Verbosity = Verbosity.warn + linopts.Tol = eps(underlyingType(mps1))^(3/4) + end + + kwargs = namedargs2cell(linopts); + [GL, lambdaL] = leftenvironment(mpo, mps1, mps2, GL, kwargs{:}); + [GR, lambdaR] = rightenvironment(mpo, mps1, mps2, GR, kwargs{:}); + lambda = (lambdaL + lambdaR) / 2; + if ~isapprox(lambdaL, lambdaR, 'AbsTol', eps^(1/3), 'RelTol', eps(lambda)^(1/3)) + warning('lambdas disagree (%e, %e)', lambdaL, lambdaR); + end + end + + function mpo = renormalize(mpo, lambda) + mpo = mpo - lambda; + end + + function mpo = plus(a, b) + if isa(a, 'InfJMpo') && isnumeric(b) + if period(a) > 1 && isscalar(b) + b = repmat(b, 1, period(a)); + end + + for i = 1:period(a) + a.O{i}(1, 1, end, 1) = a.O{i}(1, 1, end, 1) + b(i); + end + mpo = a; + + elseif isnumeric(a) && isa(b, 'InfJMpo') + mpo = b + a; + end + end + + function mpo = minus(a, b) + mpo = a + (-b); + end + + function mpo = mtimes(mpo, b) + if isnumeric(mpo) || isnumeric(b) + mpo = mpo .* b; + return + end + + mpo = [mpo; b]; + end + + function finitempo = open_boundary_conditions(mpo, L) + Os = repmat(mpo.O, 1, L); + + Os{1} = Os{1}(1, :, :, :); + Os{end} = Os{end}(:, :, end, :); + + rspace = subspaces(rightvspace(mpo, period(mpo)), size(Os{end}, 3)); + rightedge = MpsTensor(Tensor.eye([one(rspace) rspace'], one(rspace))); + + lspace = subspaces(leftvspace(mpo, 1), size(Os{1}, 1)); + leftedge = MpsTensor(Tensor.eye([one(lspace) lspace], one(lspace))'); + + finitempo = FiniteMpo(leftedge, Os, rightedge); + end + end + + methods (Static) + function mpo = twosite(Htwosite, Honesite, kwargs) + arguments + Htwosite + Honesite = [] + kwargs.Trunc + end + newkwargs = namedargs2cell(kwargs); + local_ops = MpoTensor.decompose_local_operator(Htwosite, newkwargs{:}); + L = local_ops{1}; + R = local_ops{2}; + + P = pspace(L); + assert(P == pspace(R), 'operators:spacemismatch', ... + sprintf('incompatible physical spaces %s and %s', pspace(L), pspace(R))); + + total_leftvspace = [leftvspace(L) leftvspace(R) rightvspace(R)']; + total_rightvspace = [leftvspace(L)' rightvspace(L) rightvspace(R)]; + assert(all(conj(total_rightvspace) == total_leftvspace)) + + cod = SumSpace(total_leftvspace, P); + dom = SumSpace(P, conj(total_rightvspace)); + + O = MpoTensor.zeros(cod, dom); + O(1, 1, 1, 1) = 1; + O(3, 1, 3, 1) = 1; + O(1, 1, 2, 1) = L; + O(2, 1, 3, 1) = R; + + if ~isempty(Honesite) + local_op = MpoTensor.decompose_local_operator(Honesite, newkwargs{:}); + assert(leftvspace(local_op{1}) == subspaces(leftvspace(O), 1) && ... + rightvspace(local_op{1}) == subspaces(rightvspace(O), 3) && ... + pspace(local_op{1}) == subspaces(pspace(O), 1), ... + 'operators:spacemismatch', ... + 'onesite operator incompatible with twosite operator.'); + O(1, 1, 3, 1) = local_op{1}; + end + + mpo = InfJMpo(O); + end + end +end diff --git a/src/mps/InfMpo.m b/src/mps/InfMpo.m new file mode 100644 index 0000000..b31579d --- /dev/null +++ b/src/mps/InfMpo.m @@ -0,0 +1,536 @@ +classdef InfMpo + % Infinite translation invariant matrix product operator. + % + % Properties + % ---------- + % O : :class:`cell` of :class:`.MpoTensor` or :class:`.PepsSandwich` + % cell of MPO tensors in translation invariant unit cell. + % + % Todo + % ---- + % Document + + properties + O + end + + methods + function mpo = InfMpo(varargin) + if nargin == 0, return; end + + if nargin == 1 + O = varargin{1}; + + if isa(O, 'InfMpo') + mpo.O = O.O; + + elseif isa(O, 'MpoTensor') + mpo.O = {O}; + + elseif isa(O, 'AbstractTensor') + mpo.O = arrayfun(@MpoTensor, O, 'UniformOutput', false); + + elseif iscell(O) + mpo.O = O; + end + return + end + end + end + + methods + function p = period(mpo) + p = size(mpo.O, 2); + end + + function d = depth(mpo) + d = size(mpo.O, 1); + end + + function mpo = block(mpo) + if depth(mpo) == 1, return; end + O_ = mpo.O(1, :); + for d = 2:depth(mpo) + for w = period(mpo):-1:1 + vspaces = [rightvspace(O_{w})' rightvspace(mpo.O{d, w})']; + fuser(w) = Tensor.eye(vspaces, prod(vspaces)); + end + + for w = 1:period(mpo) + O_{w} = MpoTensor(contract(O_{w}, [1 2 5 -4], mpo.O{d, w}, [3 -2 4 2], ... + fuser(prev(w, period(mpo)))', [-1 3 1], fuser(w), [5 4 -3], ... + 'Rank', [2 2])); + end + end + mpo.O = O_; + end + + function s = pspace(mpo, w) + s = pspace(mpo.O{w}); + end + + function s = leftvspace(mpo, w) + s = leftvspace(mpo.O{w}); + end + + function s = rightvspace(mpo, w) + s = rightvspace(mpo.O{w}); + end + + function mpo = repmat(mpo, varargin) + mpo.O = repmat(mpo.O, varargin{:}); + end + + function mpo = horzcat(mpo, varargin) + Os = cellfun(@(x) x.O, varargin, 'UniformOutput', false); + mpo.O = horzcat(mpo.O, Os{:}); + end + + function mpo = vertcat(mpo, varargin) + Os = cellfun(@(x) x.O, varargin, 'UniformOutput', false); + mpo.O = vertcat(mpo.O, Os{:}); + end + + function mpo = mrdivide(mpo, scalar) + assert(depth(mpo) == 1); + if isscalar(scalar) + scalar = scalar .^ (ones(size(mpo.O)) ./ length(mpo)); + end + for i = 1:length(mpo.O) + mpo.O{i} = mpo.O{i} / scalar(i); + end + end + + function mpo = slice(mpo, d) + mpo.O = mpo.O(d, :); + end + + function mps = initialize_mps(mpo, vspaces) + arguments + mpo + end + arguments (Repeating) + vspaces + end + + pspaces = arrayfun(@(x) subspaces(pspace(mpo, x)), 1:period(mpo), 'UniformOutput', false); + if length(vspaces) ~= length(pspaces) + assert(length(vspaces) == 1, 'ArgError', ... + 'Invalid length of input spaces') + vspaces = repmat(vspaces, size(pspaces)); + end + args = [pspaces; vspaces]; + mps = UniformMps.randnc(args{:}); + end + end + + + %% Environments + methods + function fp = fixedpoint(operator, state, type, w) + arguments + operator + state + type + w = 1 + end + + fp = fixedpoint(state, type(1:4), w); + + % add leg to fit operator + switch type(1) + case 'l' + fp = insert_onespace(fp, 2, ~isdual(leftvspace(operator, w))); + case 'r' + fp = insert_onespace(fp, 2, ~isdual(rightvspace(operator, w))); + otherwise + error('invalid fixedpoint type (%s)', type); + end + + % add leg to fit quasiparticle auxiliary leg + if isa(state, 'InfQP') + switch type(6) + case '0' + dual = isdual(auxspace(state, w)); + case '1' + dual = ~isdual(auxspace(state, w)); + otherwise + error('invalid type (%s)', type); + end + fp = MpsTensor(insert_onespace(fp, nspaces(fp) + 1, dual), 1); + end + end + + function [GL, lambda] = leftenvironment(mpo, mps1, mps2, GL, eigopts) + arguments + mpo + mps1 + mps2 = mps1 + GL = cell(1, period(mps1)) + eigopts.KrylovDim = 30 + eigopts.MaxIter = 1000 + eigopts.ReOrth = 2 + eigopts.Tol = eps(underlyingType(mps1))^(3/4) + eigopts.Verbosity = Verbosity.warn + end + N = period(mps1); + T = transfermatrix(mpo, mps1, mps2, 'Type', 'LL'); + [GL{1}, lambda] = eigsolve(T, GL{1}, 1, 'largestabs', ... + 'KrylovDim', eigopts.KrylovDim, 'Tol', eigopts.Tol, ... + 'ReOrth', eigopts.ReOrth, 'MaxIter', eigopts.MaxIter); + + for i = 2:N + T = transfermatrix(mpo, mps1, mps2, i-1, 'Type', 'LL'); + GL{i} = apply(T, GL{i-1}) ./ lambda^(1/N); + end + end + + function [GR, lambda] = rightenvironment(mpo, mps1, mps2, GR, eigopts) + arguments + mpo + mps1 + mps2 = mps1 + GR = cell(1, period(mps1)) + eigopts.KrylovDim = 30 + eigopts.MaxIter = 1000 + eigopts.ReOrth = 2 + eigopts.Tol = eps(underlyingType(mps1))^(3/4) + eigopts.Verbosity = Verbosity.warn + end + + T = transfermatrix(mpo, mps1, mps2, 'Type', 'RR').'; + [GR{1}, lambda] = eigsolve(T, GR{1}, 1, 'largestabs', ... + 'KrylovDim', eigopts.KrylovDim, 'Tol', eigopts.Tol, ... + 'ReOrth', eigopts.ReOrth, 'MaxIter', eigopts.MaxIter); + N = period(mps1); + for i = N:-1:2 + T = transfermatrix(mpo, mps1, mps2, i, 'Type', 'RR').'; + GR{i} = apply(T, GR{next(i, N)}) ./ lambda^(1/N); + end + end + + function GBL = leftquasienvironment(mpo, qp, GL, GR, GBL, linopts) + arguments + mpo + qp + GL + GR + GBL = cell(1, period(mpo)) + linopts.Algorithm = 'bicgstab' + linopts.MaxIter = 500 + linopts.Verbosity = Verbosity.warn + linopts.Tol = eps(underlyingType(qp))^(3/4) + end + + assert(period(qp) == period(mpo), 'quasiparticles have different period'); + + linkwargs = namedargs2cell(linopts); + expP = exp(-1i * qp.p); + L = period(mpo); + + needsRegularization = istrivial(qp); + if needsRegularization || true + fp_left = fixedpoint(mpo, qp, 'l_RL_0', 1); + fp_right = fixedpoint(mpo, qp, 'r_RL_1', L); + end + + T = transfermatrix(mpo, qp, qp, 'Type', 'RL'); + TB = transfermatrix(mpo, qp, qp, 'Type', 'BL'); + + % initialize and precompute GL * TB + GBL = cell(size(GL)); + GBL{1} = MpsTensor(SparseTensor.zeros(domain(T), auxspace(qp, 1)), 1); + for w = 1:L + TB_w = transfermatrix(mpo, qp, qp, w, 'Type', 'BL'); + T_w = transfermatrix(mpo, qp, qp, w, 'Type', 'RL'); + GBL{next(w, L)} = ... + (apply(TB_w, GL{w}) + apply(T_w, GBL{w})) * (expP^(1/L)); + end + +% rho = GL{1}; +% for pos = 1:L +% T_B = transfermatrix(mpo, qp, qp, pos, 'Type', 'BL'); +% if pos == 1 +% rho = apply(T_B, GL{pos}); +% else +% T_R = transfermatrix(mpo, qp, qp, pos, 'Type', 'RL'); +% rho = apply(T_B, GL{pos}) + apply(T_R, rho); +% end +% end + T = transfermatrix(mpo, qp, qp, 'Type', 'RL'); + TB = transfermatrix(mpo, qp, qp, 'Type', 'BL'); + + + rhs = GBL{1}; + if needsRegularization + lambda = overlap(rhs, fp_right); + rhs = rhs - lambda * fp_left; + GBL{1} = linsolve(@(x) x - expP * apply_regularized(T, fp_left, fp_right, x), ... + rhs, [], linkwargs{:}); + else + GBL{1} = expP * linsolve(@(x) x - expP * apply(T, x), ... + rhs, [], linkwargs{:}); + end + + if nnz(GBL{1}) == numel(GBL{1}.var) + GBL{1}.var = full(GBL{1}.var); + end + for w = 1:L-1 + GBL{next(w, L)} = expP^(1 / L) * ... + (apply(TB(w), GL{w}) + apply(T(w), GBL{w})); + end + end + + function GBR = rightquasienvironment(mpo, qp, GL, GR, GBR, linopts) + arguments + mpo + qp + GL + GR + GBR = cell(1, period(mpo)) + linopts.Algorithm = 'bicgstab' + linopts.MaxIter = 500 + linopts.Verbosity = Verbosity.warn + linopts.Tol = eps(underlyingType(qp))^(3/4) + end + + assert(period(qp) == period(mpo), 'quasiparticles have different period'); + + linkwargs = namedargs2cell(linopts); + + expP = exp(1i * qp.p); + + rho = GR{1}; + for pos = period(mpo):-1:1 + T_B = transfermatrix(mpo, qp, qp, pos, 'Type', 'BR').'; + if pos == period(mpo) + rho = apply(T_B, GR{pos}); + else + T_L = transfermatrix(mpo, qp, qp, pos, 'Type', 'LR').'; + rho = apply(T_B, GR{pos}) + apply(T_L, rho); + end + end + + T_L = transfermatrix(mpo, qp, qp, 1:period(mpo), 'Type', 'LR').'; + if istrivial(qp) + C = qp.mpsright.C(1); + FL = insert_onespace(multiplyleft(MpsTensor(GL{1}), C'), ... + 1, ~isdual(auxspace(qp, 1))); + FR = insert_onespace(multiplyleft(MpsTensor(GR{1}), C), ... + nspaces(GR{1}) + 1, isdual(auxspace(qp, 1))); + + rho = rho - overlap(rho, FL) * FR; + GBR{1} = linsolve(@(x) x - expP * apply_regularized(T_L, FR, FL, x), ... + expP * rho, [], linkwargs{:}); + else + GBR{1} = linsolve(@(x) x - expP * apply(T_L, x), ... + expP * rho, [], linkwargs{:}); + end + + N = period(mpo); + GBR{1} = MpsTensor(GBR{1}, 1); + for i = N:-1:2 + T_L = transfermatrix(mpo, qp, qp, i, 'Type', 'LR').'; + T_B = transfermatrix(mpo, qp, qp, i, 'Type', 'BR').'; + GBR{i} = apply(T_L, GBR{next(i, N)}) + apply(T_B, GR{next(i, N)}); + end + end + + function [GL, GR, lambda] = environments(mpo, mps1, mps2, GL, GR, eigopts) + arguments + mpo + mps1 + mps2 = mps1 + GL = cell(1, period(mps1)) + GR = cell(1, period(mps1)) + eigopts.KrylovDim = 30 + eigopts.MaxIter = 1000 + eigopts.ReOrth = 2 + eigopts.Tol = eps(underlyingType(mps1))^(3/4) + eigopts.Verbosity = Verbosity.warn + end + + kwargs = namedargs2cell(eigopts); + [GL, lambdaL] = leftenvironment(mpo, mps1, mps2, GL, kwargs{:}); + [GR, lambdaR] = rightenvironment(mpo, mps1, mps2, GR, kwargs{:}); + lambda = (lambdaL + lambdaR) / 2; + if abs(lambdaL - lambdaR)/abs(lambda) > eps(lambda)^(1/3) + warning('lambdas disagree'); + end + + for w = 1:period(mps1) + overlap = sqrt(contract(GL{w}, [1, 3:nspaces(GL{w}), 2], ... + mps1.C{prev(w, period(mps1))}, [2, nspaces(GL{w})+1], ... + mps2.C{prev(w, period(mps1))}', [nspaces(GL{w})+2, 1], ... + GR{w}, [nspaces(GL{w})+1, flip(3:nspaces(GL{w})), nspaces(GL{w})+2])); + GL{w} = GL{w} ./ overlap; + GR{w} = GR{w} ./ overlap; + end + end + end + + + %% Derived operators + methods + function T = transfermatrix(operator, state1, state2, sites, kwargs) + arguments + operator + state1 + state2 = state1 + sites = 1:period(state1) + kwargs.Type (1,2) char = 'RR' + end + + assert(all(diff(sites) == 1), 'sites must be neighbouring and increasing.'); + assert(length(kwargs.Type) == 2); + + % unpack tensors top state + switch kwargs.Type(1) + case 'L' + top = state1.AL(sites); + case 'R' + top = state1.AR(sites); + case 'B' + top = state1.B(sites); + otherwise + error('invalid transfermatrix top type (%s)', kwargs.Type(1)); + end + + % unpack tensors bottom state + switch kwargs.Type(2) + case 'L' + bot = state2.AL(sites); + case 'R' + bot = state2.AR(sites); + case 'B' + bot = state2.B(sites); + otherwise + error('invalid transfermatrix bot type (%s)', kwargs.Type(2)); + end + + % unpack tensors operator + mid = operator.O(sites); + + T = FiniteMpo.mps_channel_operator(top, mid, bot); + end + + function H = AC_hamiltonian(mpo, mps, GL, GR, sites) + arguments + mpo + mps + GL = fixedpoint(transfermatrix(mpo, mps, 'Type', 'LL')) + GR = fixedpoint(transfermatrix(mpo, mps, 'Type', 'RR').') + sites = 1:period(mps) + end + + H = cell(1, length(sites)); + for i = 1:length(sites) + for d = depth(mpo):-1:1 + gl = twistdual(GL{d, sites(i)}, 1); + p = 1:nspaces(gl); + p(2:(nspaces(gl)-1-gl.alegs)) = flip(p(2:(nspaces(gl)-1-gl.alegs))); + gl = tpermute(gl, p, rank(gl)); + + gr = GR{d, next(sites(i), period(mpo))}; + if isa(gr, 'Tensor') + disp('eh?') + end + gr = twistdual(gr, nspaces(gr)-gr.alegs); + p = 1:nspaces(gr); + p(2:(nspaces(gr)-1-gr.alegs)) = flip(p(2:(nspaces(gr)-1-gr.alegs))); + gr = tpermute(gr, p, rank(gr)); + + H{i}(d, 1) = FiniteMpo(gl, mpo.O(d, sites(i)), gr); + end + end + end + + function H = AC2_hamiltonian(mpo, mps, GL, GR, sites) + arguments + mpo + mps + GL = fixedpoint(transfermatrix(mpo, mps, 'Type', 'LL')) % BROKEN + GR = fixedpoint(transfermatrix(mpo, mps, 'Type', 'RR').') % BROKEN + sites = 1:period(mps) + end + + H = cell(1, length(sites)); + for i = 1:length(sites) + for d = depth(mpo):-1:1 + + gl = twistdual(GL{d, sites(i)}, 1); + p = 1:nspaces(gl); + p(2:(nspaces(gl)-1-gl.alegs)) = flip(p(2:(nspaces(gl)-1-gl.alegs))); + gl = tpermute(gl, p, rank(gl)); + + gr = GR{d, mod1(sites(i) + 2, period(mps))}; + gr = twistdual(gr, nspaces(gr)-gr.alegs); + p = 1:nspaces(gr); + p(2:(nspaces(gr)-1-gr.alegs)) = flip(p(2:(nspaces(gr)-1-gr.alegs))); + gr = tpermute(gr, p, rank(gr)); + + H{i}(d, 1) = FiniteMpo(gl, mpo.O(d, mod1(sites(i) + [0 1], period(mps))), gr); + end + end + end + + function H = C_hamiltonian(mpo, mps, GL, GR, sites) + arguments + mpo + mps + GL = fixedpoint(transfermatrix(mpo, mps, 'Type', 'LL')) + GR = fixedpoint(transfermatrix(mpo, mps, 'Type', 'RR').') + sites = 1:period(mps) + end + + H = cell(1, length(sites)); + for i = 1:length(sites) + for d = depth(mpo):-1:1 + gl = GL{d, next(sites(i), period(mps))}; + gl = twistdual(gl, 1); + gr = GR{d, next(sites(i), period(mps))}; + gr = twistdual(gr, nspaces(gr)); + H{i}(d, 1) = FiniteMpo(gl, {}, gr); + end + end + end + + function H = B_hamiltonian(mpo, qp, GL, GR, sites, envopts, kwargs) + arguments + mpo + qp + GL + GR + sites = 1:period(mpo) + envopts = {} + kwargs.Type + end + + switch kwargs.Type + case {'l', 'left'} + GBL = leftquasienvironment(mpo, qp, GL, GR, envopts{:}); + H = AC_hamiltonian(mpo, qp, GBL, GR, sites); + case {'c', 'center'} + H = AC_hamiltonian(mpo, qp, GL, GR, sites); + case {'r', 'right'} + GBR = rightquasienvironment(mpo, qp, GL, GR, envopts{:}); + H = AC_hamiltonian(mpo, qp, GL, GBR, sites); + otherwise + error('unknown type %s', kwargs.Type) + end + end + end + + methods (Static) + function mpo = fDimer() + pspace = GradedSpace.new(fZ2(0, 1), [1 1], false); + O = Tensor([pspace pspace], [pspace pspace]); + O1 = fill_tensor(O, @(~, f) ~any(f.uncoupled) || ... + (f.uncoupled(2) && sum(f.uncoupled) == 2)); + O2 = fill_tensor(O, @(~, f) ~any(f.uncoupled) || ... + (f.uncoupled(4) && sum(f.uncoupled) == 2)); + + mpo = InfMpo([O1; O2]); + end + end +end diff --git a/src/mps/InfQP.m b/src/mps/InfQP.m new file mode 100644 index 0000000..918ab25 --- /dev/null +++ b/src/mps/InfQP.m @@ -0,0 +1,361 @@ +classdef InfQP + % Infinite Quasi-Particle states + % + % Todo + % ---- + % Document. + + + %% Properties + properties + mpsleft UniformMps + mpsright UniformMps + X + B + VL + p + end + + properties (Dependent) + AL + AR + end + + %% Constructors + methods + function qp = InfQP(varargin) + if nargin == 0, return; end + + qp.mpsleft = varargin{1}; + qp.mpsright = varargin{2}; + qp.X = varargin{3}; + qp.VL = varargin{4}; + qp.B = varargin{5}; + qp.p = varargin{6}; + end + end + + methods (Static) + function qp = new(fun, mpsleft, mpsright, p, charge) + arguments + fun + mpsleft + mpsright = [] + p = 0 + charge = [] + end + + if isempty(mpsright), mpsright = mpsleft; end + assert(period(mpsleft) == period(mpsright)); + + dims = struct; + dims.charges = charge; + dims.degeneracies = ones(size(charge)); + + AL = mpsleft.AL; + for i = period(mpsleft):-1:1 + VL{i} = leftnull(AL{i}); + rVspace = rightvspace(VL{i}); + lVspace = leftvspace(mpsright, next(i, period(mpsleft))); + if isempty(charge) + aspace = one(rVspace); + else + aspace = rVspace.new(dims, false); + end + X{i} = Tensor.new(fun, rVspace', [aspace lVspace]); + end + + qp = InfQP(mpsleft, mpsright, X, VL, [], p); + qp.B = computeB(qp); + end + + function qp = randnc(varargin) + qp = InfQP.new(@randnc, varargin{:}); + end + end + + methods (Hidden) + function qp = zerosLike(qp, varargin) + qp = repmat(0 .* qp, varargin{:}); + end + end + + + %% Linear Algebra + methods + function n = norm(qp, p) + arguments + qp + p = 'fro' + end + assert(isscalar(qp)); + n = norm([qp.X{:}], p); +% n = sum(cellfun(@(x) norm(x, p), qp.X)); + end + + function qp = mrdivide(qp, lambda) + assert(isscalar(qp)); + qp.X = cellfun(@(x) x / lambda, qp.X, 'UniformOutput', false); + end + + function A = rdivide(A, B) + if isscalar(A) && ~isscalar(B) + A = repmat(A, size(B)); + end + if isscalar(B) && ~isscalar(A) + B = repmat(B, size(B)); + end + for i = 1:numel(A) + for j = 1:numel(A(i).X) + A(i).X{j} = A(i).X{j} ./ B(i); + end + end + end + + function qp_out = mtimes(A, B) + for i = flip(1:size(A, 1)) + for j = flip(1:size(B, 2)) + qp_out(i, j) = sum(A(i, :).' .* B(:, j), 'all'); + end + end + end + + function qp_out = times(A, B) + if isscalar(A) && ~isscalar(B) + A = repmat(A, size(B)); + end + if isscalar(B) && ~isscalar(A) + B = repmat(B, size(B)); + end + + if isnumeric(A) + qp_out = B; + for i = 1:numel(A) + qp_out(i).X = cellfun(@(x) A(i) * x, qp_out(i).X, 'UniformOutput', false); + end + else + qp_out = A; + for i = 1:numel(A) + qp_out(i).X = cellfun(@(x) x * B(i), qp_out(i).X, 'UniformOutput', false); + end + end + end + + function d = dot(qp1, qp2) + assert(isscalar(qp1) && isscalar(qp2)); + d = sum(cellfun(@dot, qp1.X, qp2.X)); + end + + function C = sum(A, dim) + arguments + A + dim = [] + end + + if isscalar(A), C = A; return; end + + if isempty(dim), dim = find(size(A) ~= 1, 1); end + + if strcmp(dim, 'all') + C = A(1); + for i = 2:numel(A) + C = C + A(i); + end + return + end + + if ismatrix(A) + if dim == 1 + C = A(1, :); + for i = 2:size(A, 1) + C = C + A(i, :); + end + return + end + + if dim == 2 + C = A(:, 1); + for i = 2:size(A, 2) + C = C + A(:, i); + end + return + end + end + + error('TBA'); + end + + function A = plus(A, B) + if isscalar(A) && ~isscalar(B) + A = repmat(A, size(B)); + end + if isscalar(B) && ~isscalar(A) + B = repmat(B, size(B)); + end + + for i = 1:numel(A) + A(i).X = cellfun(@plus, A(i).X, B(i).X, 'UniformOutput', false); + end + end + + function A = minus(A, B) + if isscalar(A) && ~isscalar(B) + A = repmat(A, size(B)); + end + if isscalar(B) && ~isscalar(A) + B = repmat(B, size(B)); + end + + for i = 1:numel(A) + A(i).X = cellfun(@minus, A(i).X, B(i).X, 'UniformOutput', false); + end + end + end + + + %% Derived Properties + methods + function s = auxspace(qp, i) + s = space(qp.X{i}, 3); + end + + function al = get.AL(qp) + al = qp.mpsleft.AL; + end + + function ar = get.AR(qp) + ar = qp.mpsright.AR; + end + + function B = computeB(qp) + for w = period(qp):-1:1 + B{w} = multiplyright(qp.VL{w}, qp.X{w}); + end + end + + function X = computeX(qp) + for w = period(qp):-1:1 + X{w} = tracetransfer(qp.VL{w}', qp.B{w}); + end + end + + function bool = istrivial(qp) + bool = qp.p == 0 && istrivial(auxspace(qp, 1)); + end + + function rho = fixedpoint(qp, type, w) + % compute the fixed point of the transfer matrix of a quasi-particle state. + % + % Usage + % ----- + % :code:`rho = fixedpoint(qp, type, w)` + % + % Arguments + % --------- + % mps : :class:`.InfQP` + % input quasi-particle state. + % + % type : :class:`char` + % specification of the type of transfer matrix: + % general format: sprintf(%c_%c%c, side, top, bot) where side is 'l' or 'r' to + % determine which fixedpoint, and top and bot are 'L' or 'R' to specify + % whether to use AL or AR in the transfer matrix. + % + % w : :class:`int` + % position within the mps unitcell of the fixed point. + + arguments + qp + type {mustBeMember(type, ... + {'l_LL' 'l_LR' 'l_RL' 'l_RR' 'r_LL' 'r_LR' 'r_RL' 'r_RR'})} + w = strcmp(type(1), 'l') * 1 + strcmp(type(1), 'r') * period(qp) + end + + switch type(1) + case 'l' + rho = fixedpoint(qp.mpsleft, type, w); + case 'r' + rho = fixedpoint(qp.mpsright, type, w); + otherwise + error('invalid type'); + end + end + end + + + %% Solvers + methods + function v = vectorize(qp, type) + % Collect all parameters in a vector, weighted to reproduce the correct + % inner product. + % + % Arguments + % --------- + % qp : :class:`.InfQP` + % input quasi-particle state. + % + % type : :class:`char`, 'real' or 'complex' + % optionally specify if complex entries should be seen as 1 or 2 parameters. + % Defaults to 'complex', with complex parameters. + % + % Returns + % ------- + % v : :class:`numeric` + % real or complex vector containing the parameters of the quasi-particle + % state. + + arguments + qp + type = 'complex' + end + + v = vectorize([qp.X{:}], type); + end + + function qp = devectorize(v, qp, type) + % Collect all parameters from a vector, and insert into a quasi-particle state. + % + % Arguments + % --------- + % v : :class:`numeric` + % real or complex vector containing the parameters of the quasi-particle + % state. + % + % qp : :class:`.InfQP` + % input quasi-particle state. + % + % type : :class:`char`, 'real' or 'complex' + % optionally specify if complex entries should be seen as 1 or 2 parameters. + % Defaults to 'complex', with complex parameters. + % + % Returns + % ------- + % qp : :class:`.InfQP` + % output quasi-particle state, filled with the parameters. + + arguments + v + qp + type = 'complex' + end + + Xs = devectorize(v, [qp.X{:}], type); + for i = 1:numel(qp.X) + qp.X{i} = Xs(i); + end + end + end + + + %% + methods + function p = period(qp) + p = length(qp.X); + end + + function type = underlyingType(qp) + type = underlyingType(qp.X{1}); + end + end +end + diff --git a/src/mps/MpoTensor.m b/src/mps/MpoTensor.m new file mode 100644 index 0000000..c90f8db --- /dev/null +++ b/src/mps/MpoTensor.m @@ -0,0 +1,475 @@ +classdef (InferiorClasses = {?Tensor, ?MpsTensor, ?SparseTensor}) MpoTensor < AbstractTensor + % Matrix product operator building block. + % + % This object represents the MPO tensor at a single site as the sum of rank (2,2) + % (sparse) tensors and some scalars, which will be implicitly used as unit tensors. + % + % .. code-block:: + % + % 4 + % v + % | + % 1 -<-- O --<- 3 + % | + % v + % 2 + % + % Todo + % ---- + % Document. + + properties + tensors = [] + scalars SparseArray = [] + end + + methods + function n = nspaces(~) + n = 4; + end + + function dom = domain(t) + dom = t.tensors.domain; + end + + function cod = codomain(t) + cod = t.tensors.codomain; + end + + function r = rank(t) + r = rank(t.tensors); + end + end + + methods + function t = MpoTensor(varargin) + if nargin == 0, return; end + + if nargin == 1 + if isnumeric(varargin{1}) + t.scalars = varargin{1}; + t.tensors = SparseTensor([], [], size(t.scalars)); + elseif isa(varargin{1}, 'MpoTensor') + t.scalars = varargin{1}.scalars; + t.tensors = varargin{1}.tensors; + elseif isa(varargin{1}, 'Tensor') || isa(varargin{1}, 'SparseTensor') + t.tensors = varargin{1}; + t.scalars = SparseArray.zeros(size(t.tensors, 1:4)); + end + return + end + + if nargin == 2 + t.tensors = varargin{1}; + t.scalars = varargin{2}; + N = max(ndims(t.tensors), ndims(t.scalars)); + assert(isequal(size(t.tensors, 1:N), size(t.scalars, 1:N)), 'mpotensors:dimerror', ... + 'scalars and tensors should have the same size.'); + if any(ismember(find(t.tensors), find(t.scalars))) + warning('mpotensor contains both scalar and tensor at the same site.'); + end + end + end + + function t = minus(t, t2) + t.tensors = t.tensors - t2.tensors; + t.scalars = t.scalars - t2.scalars; + end + + function t = plus(t, t2) + if isnumeric(t2) + t.scalars = t.scalars + t2; + return + end + if isnumeric(t) + t = t2 + t; + return + end + t.tensors = t.tensors + t2.tensors; + t.scalars = t.scalars + t2.scalars; + end + + function t = times(t, a) + if isnumeric(t) + t = a .* t; + return + end + + assert(isnumeric(a), 'mpotensor:argerror', 'invalid input types.'); + + t.tensors = t.tensors .* a; + t.scalars = t.scalars .* a; + end + + function t = rdivide(t, a) + assert(isnumeric(a), 'mpotensor:argerror', 'invalid input types.'); + t.tensors = t.tensors ./ a; + t.scalars = t.scalars ./ a; + end + + function t = mrdivide(t, a) + assert(isnumeric(a), 'mpotensor:argerror', 'invalid input types.'); + t.tensors = t.tensors / a; + t.scalars = t.scalars / a; + end + + function dst = applychannel(O, L, R, src) + arguments + O MpoTensor + L MpsTensor + R MpsTensor + src MpsTensor + end + + auxlegs_v = nspaces(src) - 3; + auxlegs_l = nspaces(L) - 3; + auxlegs_r = nspaces(R) - 3; + newrank = rank(src); newrank(2) = newrank(2) + auxlegs_l + auxlegs_r; + + dst = contract(src, [1 3 5 (-(1:auxlegs_v) - 3 - auxlegs_l)], ... + L, [-1 2 1 (-(1:auxlegs_l) - 3)], ... + O, [2 -2 4 3], ... + R, [5 4 -3 (-(1:auxlegs_r) - 3 - auxlegs_l - auxlegs_v)], ... + 'Rank', newrank); + dst = MpsTensor(dst, auxlegs_v + auxlegs_l + auxlegs_r); + end + + function v = applympo(varargin) + assert(nargin >= 3) + v = varargin{end}; + R = varargin{end-1}; + L = varargin{end-2}; + N = nargin - 1; + + auxlegs_v = nspaces(v) - N; + if isa(L, 'MpsTensor') + auxlegs_l = L.alegs; + else + auxlegs_l = 0; + end + if isa(R, 'MpsTensor') + auxlegs_r = R.alegs; + else + auxlegs_r = 0; + end + auxlegs_extra = auxlegs_l + auxlegs_r; + + Oinds = arrayfun(@(x) [2*x-2 -x 2*x 2*x-1], 2:N-1, 'UniformOutput', false); + O = [varargin(1:end-3); Oinds]; + v = contract(v, [1:2:2*N-1 (-(1:auxlegs_v) - N - auxlegs_l)], ... + L, [-1 2 1 (-(1:auxlegs_l) - N)], ... + O{:}, ... + R, [2*N-1 2*N-2 -N (-(1:auxlegs_r) - N - auxlegs_l - auxlegs_v)], ... + 'Rank', rank(v) + [0 auxlegs_extra]); + end + + function O = rot90(O) + O.tensors = tpermute(O.tensors, [2 3 4 1], [2 2]); + O.scalars = permute(O.scalars, [2 3 4 1]); + end + + function O = rot270(O) + O.tensors = tpermute(O.tensors, [4 1 2 3], [2 2]); + O.scalars = permute(O.scalars, [4 1 2 3]); + end + + function C = tensorprod(A, B, dimA, dimB, ca, cb, options) + arguments + A + B + dimA + dimB + ca = false + cb = false + options.NumDimensionsA = ndims(A) + end + + assert(~isa(A, 'MpoTensor') || ~isa(B, 'MpoTensor'), 'mpotensor:tensorprod', ... + 'cannot contract two mpotensors.'); + + if isa(A, 'MpoTensor') + C = tensorprod(A.tensors, B, dimA, dimB, ca, cb); + + if nnz(A.scalars) > 0 + assert(sum(dimA == 1 | dimA == 3, 'all') == 1, ... + 'Cannot deduce output space unless leg 1 xor leg 3 is connected.'); + assert(sum(dimA == 2 | dimA == 4, 'all') == 1, ... + 'Cannot deduce output space unless leg 2 xor leg 4 is connected.'); + + uncA = 1:nspaces(A); uncA(dimA) = []; + uncB = 1:nspaces(B); uncB(dimB) = []; + + rB = [length(dimB) length(uncB)]; + + iA = [uncA dimA]; + iB = [flip(dimB) uncB]; + + if mod1(dimA(1) + 1, 4) ~= dimA(2) + iA(end-1:end) = flip(iA(end-1:end)); + iB(1:2) = flip(iB(1:2)); + end + + [Ia, Ja, Va] = find(spmatrix(reshape(permute(A.scalars, iA), ... + [prod(size(A, uncA)) prod(size(A, dimA))]))); + [Ib, Jb, Vb] = find(reshape(tpermute(B, iB, rB), ... + [prod(size(B, dimB)) prod(size(B, uncB))])); + sz2 = [prod(size(A, uncA)) prod(size(B, uncB))]; + + for i = 1:length(Jb) + mask = find(Ja == Ib(i)); + if isempty(mask), continue; end + subs = [Ia(mask) repmat(Jb(i), length(mask), 1)]; + idx = sb2ind_(sz2, subs); + C(idx) = C(idx) + Va(mask) .* Vb(i); + end + end + else + C = tensorprod(A, B.tensors, dimA, dimB, ca, cb); + if nnz(B.scalars) > 0 + assert(sum(dimB == 1 | dimB == 3, 'all') == 1, ... + 'Cannot deduce output space unless leg 1 xor leg 3 is connected.'); + assert(sum(dimB == 2 | dimB == 4, 'all') == 1, ... + 'Cannot deduce output space unless leg 2 xor leg 4 is connected.'); + + uncA = 1:nspaces(A); uncA(dimA) = []; + uncB = 1:nspaces(B); uncB(dimB) = []; + + rA = [length(uncA) length(dimA)]; + + iA = [uncA dimA]; + iB = [flip(dimB) uncB]; + + if mod1(dimB(1) + 1, 4) ~= dimB(2) + iA(end-1:end) = flip(iA(end-1:end)); + iB(1:2) = flip(iB(1:2)); + end + + [Ia, Ja, Va] = find(reshape(tpermute(A, iA, rA), ... + [prod(size(A, uncA)) prod(size(A, dimA))])); + [Ib, Jb, Vb] = find(spmatrix(reshape(permute(B.scalars, iB), ... + [prod(size(B, dimB)) prod(size(B, uncB))]))); + sz2 = [prod(size(A, uncA)) prod(size(B, uncB))]; + + for i = 1:length(Ia) + mask = find(Ja(i) == Ib); + if isempty(mask), continue; end + subs = [repmat(Ia(i), length(mask), 1) Jb(mask)]; + idx = sub2ind_(sz2, subs); + % TODO this should probably work vectorized? + for j = 1:length(idx) + C(idx(j)) = C(idx(j)) + Va(i) .* Vb(mask(j)); + end + end + end + end + end + + function O = ctranspose(O) + O.tensors = tpermute(O.tensors', [4 1 2 3], [2 2]); + O.scalars = conj(permute(O.scalars, [1 4 3 2])); + end + + function O = transpose(O) + O.tensors = tpermute(O.tensors, [3 4 1 2], [2 2]); + O.scalars = permute(O.scalars, [3 4 1 2]); + end + end + + methods + function s = space(O, i) + if nargin == 1 + i = 1:nspaces(O); + end + s = space(O.tensors, i); + end + + function s = pspace(O) + s = domainspace(O)'; + end + + function s = domainspace(O) + s = space(O.tensors, 4); + end + + function s = codomainspace(O) + s = space(O.tensors, 2); + end + + function s = leftvspace(O, lvls) + if nargin == 1 + s = space(O.tensors, 1); + else + s = space(O.tensors(lvls, :, :, :), 1); + end + end + + function s = rightvspace(O, lvls) + if nargin == 1 + s = space(O.tensors, 3); + else + s = space(O.tensors(:, :, lvls, :), 3); + end + end + + function bool = istriu(O) + sz = size(O, 1:4); + sz1 = sz(1) * sz(2); + sz2 = sz(3) * sz(4); + bool = istriu(reshape(O.scalars, [sz1, sz2])) && ... + istriu(reshape(O.tensors, [sz1, sz2])); + end + + function bool = iseye(O) + bool = nnz(O.tensors) == 0; + if ~bool, return; end + + scal_mat = reshape(O.scalars, ... + prod(size(O.scalars, 1:2)), prod(size(O.scalars, 3:4))); + bool = isequal(spmatrix(scal_mat), speye(size(scal_mat))); + end + + function n = nnz(O) + n = nnz(O.tensors) + nnz(O.scalars); + end + + function t = contractmpo(varargin) + assert(nargin >= 3) + R = varargin{end}; + L = varargin{end-1}; + O = varargin(1:end-2); + W = length(O); + + if isa(L, 'MpsTensor') + auxlegs_l = L.alegs; + else + auxlegs_l = 0; + end + if isa(R, 'MpsTensor') + auxlegs_r = R.alegs; + else + auxlegs_r = 0; + end + + inds = arrayfun(@(x) [x -(x+1) (x+1) -(2*W+4-x)], 1:W, ... + 'UniformOutput', false); + args = [O; inds]; + t = contract(... + L, [-1, 1, -(2*W+4), -(1:auxlegs_l) - 2*W+4], ... + args{:}, ... + R, [-(W+3), W+1, -(W+2), -(1:auxlegs_r) - 2*W+4 - auxlegs_l], ... + 'Rank', [W+2 W+2] + [0, auxlegs_l + auxlegs_r]); + end + end + + methods + function t = subsref(t, s) + assert(length(s) == 1, 'mpotensor:index', ... + 'only a single level of indexing allowed.'); + switch s.type + case '.' + t = builtin('subsref', t, s); + case '()' + t.scalars = subsref(t.scalars, s); + t.tensors = subsref(t.tensors, s); + otherwise + error('mpotensor:index', 'invalid indexing expression'); + end + end + + function t = subsasgn(t, s, v) + assert(length(s) == 1, 'mpotensor:index', ... + 'only a single level of indexing allowed.'); + assert(strcmp(s.type, '()'), 'mpotensor:index', 'only () indexing allowed.'); + if isempty(t), t = MpoTensor(); end + if isnumeric(v) + t.scalars = subsasgn(t.scalars, s, v); + elseif isa(v, 'MpoTensor') + t.scalars = subsasgn(t.scalars, s, v.scalars); + t.tensors = subsasgn(t.tensors, s, v.tensors); + elseif isa(v, 'Tensor') + t.tensors = subsasgn(t.tensors, s, v); + end + end + + function i = end(t, k, n) + if n == 1 + i = prod(size(t)); + return + end + if k > ndims(t) + i = 1; + else + i = size(t, k); + end + end + + function bools = eq(a, b) + arguments + a MpoTensor + b MpoTensor + end + + bools = a.scalars == b.scalars & a.tensors == b.tensors; + end + + function I = find(O) + I = union(find(O.tensors), find(O.scalars)); + end + + function varargout = size(t, varargin) + if nargin > 1 + [varargout{1:nargout}] = size(t.scalars, varargin{:}); + else + [varargout{1:nargout}] = size(t.scalars); + end + end + + function n = ndims(t) + n = ndims(t.scalars); + end + + function disp(O) + builtin('disp', O); + end + + function jl = mat2jl(a) + jl = struct('classname', 'MpoTensor'); + jl.tensors = mat2jl(a.tensors); + jl.scalars = mat2jl(a.scalars); + end + end + + methods (Static) + function O = zeros(codomain, domain) + tensors = SparseTensor.zeros(codomain, domain); + scalars = SparseArray.zeros(size(tensors)); + O = MpoTensor(tensors, scalars); + end + + function local_operators = decompose_local_operator(H, kwargs) + % Convert a tensor into a product of local operators. + % + % Usage + % ----- + % :code:`local_operators = MpoTensor.decompose_local_operator(H, kwargs)`. + % + % Arguments + % --------- + % H : :class:`.AbstractTensor` + % tensor representing a local operator on N sites. + % + % Keyword Arguments + % ----------------- + % 'Trunc' : :class:`cell` + % optional truncation method for the decomposition. See also + % :meth:`.Tensor.tsvd` + arguments + H + kwargs.Trunc = {'TruncBelow', 1e-14} + end + + local_operators = decompose_local_operator(H, 'Trunc', kwargs.Trunc); + local_operators = cellfun(@MpoTensor, local_operators, 'UniformOutput', false); + end + end +end diff --git a/src/mps/MpsTensor.m b/src/mps/MpsTensor.m new file mode 100644 index 0000000..0c340ea --- /dev/null +++ b/src/mps/MpsTensor.m @@ -0,0 +1,712 @@ +classdef (InferiorClasses = {?Tensor, ?SparseTensor}) MpsTensor < AbstractTensor + % Generic MPS tensor objects that have a notion of virtual, physical and auxiliary legs. + % + % Properties + % ---------- + % var : :class:`.Tensor` or :class:`.SparseTensor` + % tensor data of the MPS tensor. + % + % plegs : :class:`int` + % number of physical legs. + % + % alegs : :class:`int` + % number of auxiliary legs. + % + % Todo + % ---- + % Document all methods. + + properties + var + plegs + alegs = 0 + end + + + %% Constructors + methods + function A = MpsTensor(tensor, alegs) + arguments + tensor = [] + alegs = 0 + end + + if iscell(tensor) + for i = length(tensor):-1:1 + A(i) = MpsTensor(tensor{i}, alegs); + end + return + end + + % TODO: copy constructor + + if ~isempty(tensor) + A.var = tensor; + A.plegs = nspaces(tensor) - alegs - 2; + A.alegs = alegs; + end + end + end + + methods (Static) + function A = new(varargin, kwargs) + arguments (Repeating) + varargin + end + arguments + kwargs.alegs = 0 + end + + A = MpsTensor(Tensor.new(varargin{:}), kwargs.alegs); + end + + function A = randnc(varargin, kwargs) + arguments (Repeating) + varargin + end + arguments + kwargs.alegs = 0 + end + + A = MpsTensor(Tensor.randnc(varargin{:}), kwargs.alegs); + end + end + + + %% Properties + methods +% function varargout = size(A, varargin) +% [varargout{1:nargout}] = size(A.var, varargin{:}); +% end + +% function n = numel(A) +% n = numel(A.var); +% end + +% function l = length(A) +% l = length(A.var); +% end + + function A = cat(dim, varargin) + ismpstensor = cellfun(@(x) isa(x, 'MpsTensor'), varargin); + i = find(ismpstensor, 1); + A = varargin{i}; + for j = 1:i-1 + B = varargin{j}; + if ismpstensor(j) + assert(B.alegs == A.alegs && B.plegs == A.plegs); + A.var = cat(dim, B.var, A.var); + else + A.var = cat(dim, B, A.var); + end + end + end + + function bool = isscalar(~) + bool = false; + end + + function A = horzcat(varargin) + A = cat(2, varargin{:}); + end + + function A = vertcat(varargin) + A = cat(1, varargin{:}); + end + + function n = nnz(A) + n = nnz(A.var); + end + + function A = slice(A, varargin) + s = substruct('()', varargin); + A.var = subsref(A.var, s); + end + + function tdst = insert_onespace(tsrc, varargin) + % Insert a trivial space at position :code:`i`, corresponding to an additional + % auxiliary leg. + % + % See Also + % -------- + % :meth:`.Tensor.insert_onespace` + + tdst = MpsTensor(insert_onespace(tsrc.var, varargin{:}), tsrc.alegs + 1); + end + end + + %% Spaces + methods + function s = space(A, varargin) + s = space(A.var, varargin{:}); + end + + function n = nspaces(A) + n = nspaces(A.var); + end + + function cod = codomain(A) + cod = A.var.codomain; + end + + function dom = domain(A) + dom = A.var.domain; + end + + function r = rank(A) + r = rank(A.var); + end + + function s = pspace(A) + % The physical space of an :class:`.MpsTensor`. + s = space(A, 1 + (1:A.plegs)); + end + + function s = leftvspace(A) + % The left virtual space of an :class:`.MpsTensor`. + s = space(A.var(1), 1); + end + + function s = rightvspace(A) + % The right virtual space of an :class:`.MpsTensor`. + s = space(A.var(1), nspaces(A.var(1)) - A.alegs); + end + end + + + + %% Linear Algebra + methods + + function varargout = matrixblocks(t) + [varargout{1:nargout}] = matrixblocks(t.var); + end + + function d = dot(A, B) + if isa(A, 'MpsTensor') + A = A.var; + end + if isa(B, 'MpsTensor') + B = B.var; + end + d = dot(A, B); + end + + function d = overlap(A, B) + arguments + A + B MpsTensor + end + + inds = 1:nspaces(A); + + d = contract(A, inds, B, [fliplr(inds(1:end-B.alegs)) inds(end-B.alegs+1:end)]); + end + + function t = mrdivide(t1, t2) + if isa(t1, 'MpsTensor') + t = t1; + t1 = t1.var; + else + t = t2; + end + if isa(t2, 'MpsTensor') + t2 = t2.var; + end + + t.var = t1 * inv(t2); + end + + function C = mtimes(A, B) + if isscalar(A) || isscalar(B) + C = A .* B; + return + end + + szA = size(A); + szB = size(B); + if szA(2) ~= szB(1) + error('mtimes:dimagree', ... + 'incompatible dimensions (%d) (%d)', szA(2), szB(1)); + end + + if ~(isnumeric(A) || isnumeric(B)) + error('not implemented when neither input is numeric.'); + end + + % TODO: fairly hacky for now but it works; should be revisited properly + + if isnumeric(A) + al = repmat([B(1, :).alegs], szA(1), 1); + B = reshape([B.var], szB); + else + al = repmat([A(:, 1).alegs], 1, szB(2)); + A = reshape([A.var], szA); + end + Cv = A * B; + + for i = szA(1):-1:1 + for j = szB(2):-1:1 + C(i, j) = MpsTensor(Cv(i, j), al(i, j)); + end + end + end + + function C = times(A, B) + if isscalar(A) + C = B; + for i = 1:numel(C) + C(i).var = A .* B(i).var; + end + return + end + if isscalar(B) + C = A; + for i = 1:numel(C) + C(i).var = A(i).var .* B; + end + return + end + + error('not implemented when neither input is scalar.'); + end + + function A = repartition(A, varargin) + A.var = repartition(A.var, varargin{:}); + end + + function A = plus(varargin) + for i = 1:2 + if isa(varargin{i}, 'MpsTensor') + alegs = varargin{i}.alegs; + varargin{i} = varargin{i}.var; + end + end + A = MpsTensor(plus(varargin{:}), alegs); + end + + function A = minus(varargin) + for i = 1:2 + if isa(varargin{i}, 'MpsTensor') + alegs = varargin{i}.alegs; + varargin{i} = varargin{i}.var; + end + end + A = MpsTensor(minus(varargin{:}), alegs); + end + + function A = rdivide(varargin) + for i = 1:2 + if isa(varargin{i}, 'MpsTensor') + alegs = varargin{i}.alegs; + varargin{i} = varargin{i}.var; + end + end + A = MpsTensor(rdivide(varargin{:}), alegs); + end + + function A = ldivide(varargin) + for i = 1:2 + if isa(varargin{i}, 'MpsTensor') + alegs = varargin{i}.alegs; + varargin{i} = varargin{i}.var; + end + end + A = MpsTensor(ldivide(varargin{:}), alegs); + end + + function n = norm(A, varargin) + n = norm(A.var, varargin{:}); + end + + function d = distance(A, B) + d = norm(A - B); + end + + + function [A, n] = normalize(A) + [A.var, n] = normalize(A.var); + end + + function A = conj(A) + A.var = conj(A.var); + end + + function A = twist(A, varargin) + A.var = twist(A.var, varargin{:}); + end + + function A = twistdual(A, varargin) + A.var = twistdual(A.var, varargin{:}); + end + + function t = ctranspose(t) + % Compute the adjoint of a tensor. This is defined as swapping the codomain and + % domain, while computing the adjoint of the matrix blocks. + % + % Usage + % ----- + % :code:`t = ctranspose(t)` + % + % :code:`t = t'` + % + % Arguments + % --------- + % t : :class:`.MpsTensor` + % input tensor. + % + % Returns + % ------- + % t : :class:`.MpsTensor` + % adjoint tensor. + + for i = 1:numel(t) + t(i).var = t(i).var'; + end + + t = permute(t, ndims(t):-1:1); + end + + function A = tpermute(A, varargin) + A.var = tpermute(A.var, varargin{:}); + end + + function C = tensorprod(varargin) + for i = 1:2 + if isa(varargin{i}, 'MpsTensor') + varargin{i} = varargin{i}.var; + end + end + C = tensorprod(varargin{:}); + end + + function T = transfermatrix(A, B) + arguments + A MpsTensor + B MpsTensor = A + end + + B = twist(B, [isdual(space(B, 1:B.plegs+1)) ~isdual(space(B, B.plegs+2))]); + T = FiniteMpo(B', {}, A); + end + + function C = initializeC(A) + % Initialize a set of gauge tensors for the given mpstensors. + arguments (Repeating) + A MpsTensor + end + C = cell(size(A)); + + for i = length(A):-1:1 + C{i} = A{i}.var.eye(rightvspace(A{i})', leftvspace(A{next(i, length(A))})); + end + end + + function A = expand(A, leftvspace, rightvspace, noisefactor) + % Expand a tensor to the given virtual spaces. + arguments + A + leftvspace + rightvspace + noisefactor = 1e-3 + end + + spaces = space(A); + spaces(nspaces(A) - A.alegs) = rightvspace; + spaces(1) = conj(leftvspace); + r = rank(A); + A.var = embed(A.var, ... + noisefactor * ... + normalize(A.var.randnc(spaces(1:r(1)), spaces(r(1)+1:end)'))); + end + + function type = underlyingType(A) + type = underlyingType(A.var); + end + + function disp(t) + fprintf('%s with %d plegs and %d alegs:\n', class(t), t.plegs, t.alegs); + disp(t.var); + end + end + + + %% Factorizations + methods + function [A, L] = leftorth(A, alg) + arguments + A + alg = 'polar' + end + + if A.alegs == 0 + [A.var, L] = leftorth(A.var, 1:nspaces(A)-1, nspaces(A), alg); + if isdual(space(L, 1)) == isdual(space(L, 2)) + L.codomain = conj(L.codomain); + L = twist(L, 1); + A.var.domain = conj(A.var.domain); + end + else + [A.var, L] = leftorth(A.var, [1:A.plegs+1 A.plegs+3], A.plegs+2, alg); + A.var = permute(A.var, [1:A.plegs+1 A.plegs+3 A.plegs+2], rank(A)); + end + end + + function [R, A] = rightorth(A, alg) + arguments + A + alg = 'rqpos' + end + + [R, A.var] = rightorth(A.var, 1, 2:nspaces(A), alg); + if isdual(space(R, 1)) == isdual(space(R, 2)) + R.domain = conj(R.domain); + R = twist(R, 2); + A.var.codomain = conj(A.var.codomain); + end + end + + function varargout = tsvd(t, varargin) + [varargout{1:nargout}] = tsvd(t.var, varargin{:}); + end + + function A = leftnull(A, alg) + arguments + A + alg = 'svd' + end + + if numel(A) > 1 + for i = numel(A):-1:1 + A(i) = leftnull(A(i), alg); + end + return + end + + p = 1:nspaces(A); + p1 = p(1:(end - A.alegs - 1)); + p2 = p((end - A.alegs):end); + A.var = leftnull(A.var, p1, p2, alg); + end + + function A = rightnull(A, alg) + arguments + A + alg = 'svd' + end + + if numel(A) > 1 + for i = numel(A):-1:1 + A(i) = rightnull(A(i), alg); + end + return + end + + A.var = rightnull(A.var, 1, 2:nspaces(A), alg); + end + end + + %% Contractions + methods + function v = applytransfer(L, R, v) + arguments + L MpsTensor + R MpsTensor + v = [] + end + + if isempty(v) + v = tracetransfer(L, R); + return + end + + auxlegs_v = nspaces(v) - 2; + auxlegs_l = L.alegs; + auxlegs_r = R.alegs; + newrank = rank(v); newrank(2) = newrank(2) + auxlegs_l + auxlegs_r; + + v = contract( ... + v, [1, R.plegs+2, (-(1:auxlegs_v) - 2 - auxlegs_l)], ... + L, [-1, (1:L.plegs)+1, 1 (-(1:auxlegs_l) - (1+L.plegs))], ... + R, [R.plegs+2, (R.plegs:-1:1)+1, -2, (-(1:auxlegs_r) - (2+R.plegs) - auxlegs_l - auxlegs_v)], ... + 'Rank', newrank); + end + + function v = contracttransfer(L, R) + arguments + L MpsTensor + R MpsTensor + end + + auxlegs_l = L.alegs; + auxlegs_r = R.alegs; + assert(R.plegs == L.plegs); + plegs = L.plegs; %#ok + + v = contract(... + L, [-1, 1:plegs, -4, (-(1:auxlegs_l) - 4)], ... + R, [-3, flip(1:plegs), -2 (-(1:auxlegs_r) - 4 - auxlegs_l)], ... + 'Rank', [2, 2] + [0, auxlegs_l + auxlegs_r]); %#ok + end + + function v = tracetransfer(L, R) + arguments + L MpsTensor + R MpsTensor + end + + auxlegs_l = L.alegs; + auxlegs_r = R.alegs; + assert(R.plegs == L.plegs); + plegs = L.plegs; %#ok + newrank = [1 1 + auxlegs_l + auxlegs_r]; + + v = contract(... + L, [-1 1:(plegs + 1) (-(1:auxlegs_l) - 2)], ... + R, [flip(1:(plegs + 1)) -2 (-(1:auxlegs_r) - 2 - auxlegs_l)], ... + 'Rank', newrank); %#ok + end + + function A = multiplyleft(A, C) + % Multiply a gauge matrix from the left. + assert(nspaces(C) == 2, 'mps:tba', 'not implemented yet'); + A.var = contract(C, [-1 1], ... + A.var, [1 -((1:A.plegs)+1) -((1:A.alegs+1)+A.plegs+1)], 'Rank', rank(A)); + end + + function A = multiplyright(A, C) + % Multiply a gauge matrix from the right. + r = rank(A); npA = A.plegs; naA = A.alegs; + nC = nspaces(C); naC = nC - 2; + r(2) = r(2) + naC; + A.var = contract(A.var, [-(1:npA + 1) 1 -((1:naA) + npA + 2)], ... + C, [1 -(2 + npA + (1:(nC - 1)))], 'Rank', r); + A.alegs = A.alegs + naC; + end + end + + %% + methods + function bool = isisometry(t, varargin) + bool = isisometry(t.var, varargin{:}); + end + end + + + %% Converters + methods + function t = Tensor(A) + t = full(A.var); + end + + function t = SparseTensor(A) + t = reshape([A.var], size(A)); + t = sparse(t); + end + end + + + %% Solvers + methods + function v = vectorize(t, type) + % Collect all parameters in a vector, weighted to reproduce the correct + % inproduct. + % + % Arguments + % --------- + % t : :class:`.MpsTensor` + % input tensor. + % + % type : 'real' or 'complex' + % optionally specify if complex entries should be seen as 1 or 2 parameters. + % Defaults to 'complex', with complex parameters. + % + % Returns + % ------- + % v : :class:`numeric` + % real or complex vector containing the parameters of the tensor. + + arguments + t + type = 'complex' + end + + v = vectorize(t.var, type); + end + + function t = devectorize(v, t, type) + % Collect all parameters from a vector, and insert into a tensor. + % + % Arguments + % --------- + % v : :class:`numeric` + % real or complex vector containing the parameters of the tensor. + % + % t : :class:`.MpsTensor` + % input tensor. + % + % type : :class:`char`, 'real' or 'complex' + % optionally specify if complex entries should be seen as 1 or 2 parameters. + % Defaults to 'complex', with complex parameters. + % + % Returns + % ------- + % t : :class:`.MpsTensor` + % output tensor, filled with the parameters. + + arguments + v + t + type = 'complex' + end + + t.var = devectorize(v, t.var, type); + end + end + + + %% + methods (Static) + function local_tensors = decompose_local_state(psi, kwargs) + % convert a tensor into a product of local operators. + % + % Usage + % ----- + % :code:`local_operators = MpoTensor.decompose_local_operator(H, kwargs)`. + % + % Arguments + % --------- + % psi : :class:`.AbstractTensor` + % tensor representing a local state on N sites. + % + % Keyword Arguments + % ----------------- + % 'Trunc' : :class:`cell` + % optional truncation method for the decomposition. See also + % :meth:`.Tensor.tsvd` + arguments + psi + kwargs.Trunc = {'TruncBelow', 1e-14} + end + + L = nspaces(psi); + assert(L >= 3, 'argerror', ... + sprintf('state must have at least 3 legs. (%d)', L)); + + local_tensors = cell(1, L - 2); + for i = 1:length(local_tensors)-1 + [u, s, psi] = tsvd(psi, [1 2], [3:nspaces(psi)], kwargs.Trunc{:}); + local_tensors{i} = multiplyright(MpsTensor(u), s); + end + local_tensors{end} = MpsTensor(repartition(psi, [2 1])); + end + end + + methods (Hidden) + function tdst = zerosLike(t, varargin) + tdst = repmat(0 * t, varargin{:}); + end + end + +end + diff --git a/src/mps/PepsSandwich.m b/src/mps/PepsSandwich.m new file mode 100644 index 0000000..015670b --- /dev/null +++ b/src/mps/PepsSandwich.m @@ -0,0 +1,201 @@ +classdef (InferiorClasses = {?Tensor, ?MpsTensor, ?SparseTensor}) PepsSandwich + % Data structure representing a pair of PEPS tensors in an overlap, which behave as an + % MPO tensor. + % + % Properties + % ---------- + % top : :class:`.PepsTensor` + % top-layer PEPS tensor, usually interpreted as the 'ket' in the overlap. + % + % bot : :class:`.PepsTensor` + % bottom-layer PEPS tensor, usually interpreted as the 'bra' in the overlap. + % + % Todo + % ---- + % Document. + + properties + top PepsTensor + bot PepsTensor + end + + + methods + function T = PepsSandwich(top, bot) + arguments + top + bot = conj(top) + end + + T.top = top; + T.bot = bot; + end + + function T = rot90(T) + T.top = tpermute(T.top, [1, 3, 4, 5, 2], rank(T.top)); + T.bot = tpermute(T.bot, [1, 3, 4, 5, 2], rank(T.bot)); + end + + function T = rot270(T) + T.top = tpermute(T.top, [1, 5, 2, 3, 4], rank(T.top)); + T.bot = tpermute(T.bot, [1, 5, 2, 3, 4], rank(T.bot)); + end + + function T = transpose(T) + top = T.top; + bot = T.bot; + T.top = tpermute(bot, [1, 4, 5, 2, 3], rank(bot)); + T.bot = tpermute(top, [1, 4, 5, 2, 3], rank(top)); + end + + function T = ctranspose(T) + T.top = tpermute(conj(T.top), [1, 2, 5, 4, 3], rank(T.top)); + T.bot = tpermute(conj(T.bot), [1, 2, 5, 4, 3], rank(T.bot)); + end + + function s = pspace(T) + s = domainspace(T)'; + end + + function s = domainspace(T) + % flipped for consistency + s = [northvspace(T.bot), northvspace(T.top)]; + end + + function s = codomainspace(T) + s = [southvspace(T.top), southvspace(T.bot)]; + end + + function s = rightvspace(T) + % flipped for consistency + s = [eastvspace(T.bot), eastvspace(T.top)]; + end + + function s = leftvspace(T) + s = [westvspace(T.top), westvspace(T.bot)]; + end + + function v = applychannel(T, L, R, v) + arguments + T PepsSandwich + L MpsTensor + R MpsTensor + v MpsTensor + end + auxlegs_v = nspaces(v) - 4; + auxlegs_l = nspaces(L) - 4; + auxlegs_r = nspaces(R) - 4; + newrank = rank(v); newrank(2) = newrank(2) + auxlegs_l + auxlegs_r; + + v = contract(... + v, [1, 4, 3, 7, (-(1:auxlegs_v) - 4 - auxlegs_l)], ... + L, [-1, 2, 5, 1, (-(1:auxlegs_l) - 4)], ... + T.top.var, [6, 5, -2, 8, 4], ... + T.bot.var, [6, 2, -3, 9, 3], ... + R, [7, 8, 9, -4, (-(1:auxlegs_r) - 4 - auxlegs_l - auxlegs_v)], ... + 'Rank', newrank); + v = MpsTensor(v, auxlegs_v + auxlegs_l + auxlegs_r); + end + + function v = applympo(varargin) + assert(nargin >= 3) + v = varargin{end}; + R = varargin{end-1}; + L = varargin{end-2}; + O = varargin(1:end-3); + W = length(O); + + auxlegs_v = nspaces(v) - (2*W+2); + if isa(L, 'MpsTensor') + auxlegs_l = L.alegs; + else + auxlegs_l = 0; + end + if isa(R, 'MpsTensor') + auxlegs_r = R.alegs; + else + auxlegs_r = 0; + end + auxlegs_extra = auxlegs_l + auxlegs_r; + + inds_top = arrayfun(@(x) [ 5 + 5*(x-1), ... + 2 + 5*(x-1), ... + -(2 + 2*(x-1)), ... + 2 + 5*x, ... + 3 + 5*(x-1)], ... + 1:W, 'UniformOutput', false); + inds_bot = arrayfun(@(x) [ 5 + 5*(x-1), ... + 4 + 5*(x-1), ... + -(3 + 2*(x-1)), ... + 4 + 5*x, ... + 6 + 5*(x-1)], ... + 1:W, 'UniformOutput', false); + tops = cellfun(@(x) x.top.var, O, 'UniformOutput', false); + bots = cellfun(@(x) x.bot.var, O, 'UniformOutput', false); + Oargs = [tops; inds_top; bots; inds_bot]; + v = contract(v, [1, reshape([3, 6]' + 5*(0:W-1), 1, []), 3 + 5*W, (-(1:auxlegs_v) - (2*W+2) - auxlegs_l)], ... + L, [-1, 4, 2, 1, (-(1:auxlegs_l) - (2*W+2))], ... + Oargs{:}, ... + R, [3 + 5*W, 2 + 5*W, 4 + 5*W, -(2*W + 2), (-(1:auxlegs_r) - (2*W+2) - auxlegs_l - auxlegs_v)], ... + 'Rank', rank(v) + [0 auxlegs_extra]); + end + + function t = contractmpo(varargin) + assert(nargin >= 3) + R = varargin{end}; + L = varargin{end-1}; + O = varargin(1:end-2); + W = length(O); + + if isa(L, 'MpsTensor') + auxlegs_l = L.alegs; + else + auxlegs_l = 0; + end + if isa(R, 'MpsTensor') + auxlegs_r = R.alegs; + else + auxlegs_r = 0; + end + + inds_top = arrayfun(@(x) [ 3 + 3*(x-1), ... + 1 + 3*(x-1), ... + -(2 + 2*(x-1)), ... + 1 + 3*x, ... + -(4*W + 5 - 2*x)], ... + 1:W, 'UniformOutput', false); + inds_bot = arrayfun(@(x) [ 3 + 3*(x-1), ... + 2 + 3*(x-1), ... + -(3 + 2*(x-1)), ... + 2 + 3*x, ... + -(4*W + 4 - 2*x)], ... + 1:W, 'UniformOutput', false); + tops = cellfun(@(x) x.top.var, O, 'UniformOutput', false); + bots = cellfun(@(x) x.bot.var, O, 'UniformOutput', false); + args = [tops; inds_top; bots; inds_bot]; + t = contract(... + L, [-1, 2, 1, -(4*W + 4), -(1:auxlegs_l) - (4*W + 4)], ... + args{:}, ... + R, [-(2*W + 3), 1 + 3*W, 2 + 3*W, -(2 + 2*W), -(1:auxlegs_r) - (4*W + 4) - auxlegs_r], ... + 'Rank', [2*W+2, 2*W+2] + [0, auxlegs_l + auxlegs_r]); + end + + function t = MpoTensor(T) + fuse_east = Tensor.eye(prod(rightvspace(T)), rightvspace(T)'); + fuse_south = Tensor.eye(prod(codomainspace(T)), codomainspace(T)); + fuse_west = Tensor.eye(prod(leftvspace(T)), leftvspace(T)'); + fuse_north = Tensor.eye(prod(domainspace(T))', domainspace(T)); + + t = MpoTensor(contract(... + T.top.var, [1, 2, 4, 6, 8], ... + T.bot.var, [1, 3, 5, 7, 9], ... + fuse_east, [-1, 3, 2], ... + fuse_south, [-2, 5, 4], ... + fuse_west, [-3, 6, 7], ... + fuse_north, [-4, 8, 9], ... + 'Rank', [2, 2])); + % TODO: test properly + end + end +end + diff --git a/src/mps/PepsTensor.m b/src/mps/PepsTensor.m new file mode 100644 index 0000000..9e24d60 --- /dev/null +++ b/src/mps/PepsTensor.m @@ -0,0 +1,219 @@ +classdef PepsTensor + % Generic PEPS tensor object that has a notion of virtual and physical legs. + % + % This object represents the PEPS tensor at a single site as a rank (1, 4) tensor, + % where the physical index lies in the codomain and the virtual indices lie in the + % domain. + % + % .. code-block:: + % + % 5 1 + % | / + % v ^ + % |/ + % 2 ->-- O --<- 4 + % | + % ^ + % | + % 3 + % + % Properties + % ---------- + % var : :class:`.Tensor` + % PEPS tensor data. + % + % Todo + % ---- + % Document. + properties + var + end + + + %% Constructors + methods + function A = PepsTensor(tensor) + arguments + tensor = [] + end + + if ~isempty(tensor) + A.var = tensor; + end + end + end + + + %% Properties + methods + function n = nspaces(A) + n = 5; + end + + function s = space(A, varargin) + s = space(A.var, varargin{:}); + end + + function s = pspace(A) + s = space(A, 1); + end + + function s = westvspace(A) + s = space(A, 2); + end + + function s = southvspace(A) + s = space(A, 3); + end + + function s = eastvspace(A) + s = space(A, 4); + end + + function s = northvspace(A) + s = space(A, 5); + end + + function s = vspace(A) + s = space(A, 2:5); + end + + function cod = codomain(A) + cod = A.var.codomain; + end + + function dom = domain(A) + dom = A.var.domain; + end + + function r = rank(A) + r = rank(A.var); + end + end + + + %% Linear Algebra + methods + function A = repartition(A, varargin) + A.var = repartition(A.var, varargin{:}); + end + + function A = tpermute(A, varargin) + A.var = tpermute(A.var, varargin{:}); + end + + function A = plus(A, B) + arguments + A PepsTensor + B PepsTensor + end + A.var = plus(A.var, B.var); + end + + function A = minus(A, B) + arguments + A PepsTensor + B PepsTensor + end + A.var = minus(A.var, B.var); + end + + function n = norm(A) + n = norm(A.var); + end + + function A = conj(A) + A.var = conj(A.var); + end + + function A = twist(A, varargin) + A.var = twist(A.var, varargin{:}); + end + + function t = ctranspose(t) + t.var = t.var'; + t = permute(t, ndims(t):-1:1); + end + + function t = dagger(t) + leftflip = Tensor.eye(westvspace(t), eastvspace(t)); + rightflip = twist(leftflip', 2); + pflip = Tensor.eye(pspace(t), pspace(t)'); + t.var = tpermute(conj(t.var), [1,2,5,4,3]); + t.var = contract(t.var, [1,2,-3,3,-5], pflip, [1,-1], leftflip, [-2,2], rightflip, [3,-4]); + end + + function t = rot90(t) + t = tpermute(t, [1, 3, 4, 5, 2], rank(t)); + end + + function t = rot270(t) + t = tpermute(t, [1, 5, 2, 3, 4], rank(t)); + end + + function C = tensorprod(varargin) + for i = 1:2 + if isa(varargin{i}, 'PepsTensor') + varargin{i} = varargin{i}.var; + end + end + C = tensorprod(varargin{:}); + end + + function type = underlyingType(A) + type = underlyingType(A.var); + end + end + + + %% Converters + methods + function t = Tensor(A) + t = full(A.var); + end + + function t = SparseTensor(A) + t = reshape([A.var], size(A)); + t = sparse(t); + end + end + + + %% Static constructors + methods (Static) + function t = new(fun, pspace, westvspace, southvspace, eastvspace, northvspace) + arguments + fun + pspace + westvspace + southvspace + eastvspace = westvspace' + northvspace = southvspace' + end + + t = PepsTensor(Tensor.new(fun, pspace, [westvspace, southvspace, eastvspace, northvspace]')); + end + + function t = rand(varargin) + t = PepsTensor.new(@rand, varargin{:}); + end + + function t = randn(varargin) + t = PepsTensor.new(@randn, varargin{:}); + end + + function t = randc(varargin) + t = PepsTensor.new(@randc, varargin{:}); + end + + function t = randnc(varargin) + t = PepsTensor.new(@randnc, varargin{:}); + end + + function t = zeros(varargin) + t = PepsTensor.new(@zeros, varargin{:}); + end + + end +end + diff --git a/src/mps/UniformMps.m b/src/mps/UniformMps.m new file mode 100644 index 0000000..7ab6db4 --- /dev/null +++ b/src/mps/UniformMps.m @@ -0,0 +1,1091 @@ +classdef UniformMps + % Implementation of infinite translation invariant MPS + % + % The center gauge is defined to have: + % + % .. math:: + % AL_w \cdot C_w = AC_w = C_{w-1} \cdot AR_w + % + % Properties + % ---------- + % AL : :class:`.MpsTensor` + % left-gauged mps tensors. + % + % AR : :class:`.MpsTensor` + % right-gauged mps tensors. + % + % C : :class:`.Tensor` + % center gauge transform. + % + % AC : :class:`.MpsTensor` + % center-gauged mps tensors. + % + % Todo + % ---- + % Document all methods. + + properties + AL (1,:) cell + AR (1,:) cell + C (1,:) cell + AC (1,:) cell + end + + + %% Constructors + methods + function mps = UniformMps(varargin) + % Usage + % ----- + % :code:`mps = UniformMps(A)` + % + % :code:`mps = UniformMps(AL, AR, C [, AC])` + % + % Arguments + % --------- + % A : :class:`cell` of :class:`.MpsTensor` + % set of tensors per site that define an MPS to be gauged. + % + % AL, AR, AC : :class:`cell` of :class:`.MpsTensor` + % set of gauged MpsTensors. + % + % C : :class:`cell` of :class:`.Tensor` + % gauge tensor. + % + % Returns + % ------- + % mps : :class:`.UniformMps` + % gauged uniform MPS. + + narginchk(0, 4); + + if nargin == 0, return; end % default empty constructor + + if nargin == 1 + if isa(varargin{1}, 'UniformMps') % copy constructor + for i = numel(varargin{1}):-1:1 + mps(i).AL = varargin{1}(i).AL; + mps(i).AR = varargin{1}(i).AR; + mps(i).C = varargin{1}(i).C; + mps(i).AC = varargin{1}(i).AC; + end + mps = reshape(mps, size(mps)); + + elseif isa(varargin{1}, 'Tensor') + for i = height(varargin{1}):-1:1 + mps.AR = MpsTensor(varargin{1}); + mps.AL = MpsTensor(varargin{1}); + end + mps = canonicalize(mps); + + elseif isa(varargin{1}, 'MpsTensor') + % by default we take the input as AR, as canonicalize right + % orthogonalizes before left orthogonalization + for i = height(varargin{1}):-1:1 + mps.AR = varargin{1}; + mps.AL = varargin{1}; + end + mps = canonicalize(mps); + + elseif iscell(varargin{1}) + for j = height(varargin{1}):-1:1 + mps(j).AR = varargin{1}(j, :); + mps(j).AL = varargin{1}(j, :); + end + mps = canonicalize(mps); + else + error('Invalid constructor for UniformMps.') + end + + else + if iscell(varargin{1}), mps.AL = varargin{1}; else, mps.AL = varargin(1); end + if iscell(varargin{2}), mps.AR = varargin{2}; else, mps.AR = varargin(2); end + if iscell(varargin{3}), mps.C = varargin{3}; else, mps.C = varargin(3); end + if nargin == 4 + if iscell(varargin{4}) + mps.AC = varargin{4}; + else + mps.AC = varargin(4); + end + end + end + + + end + end + + methods (Static) + function mps = new(fun, pspaces, vspaces) + % Create a uniform matrix product state with data using a function handle. + % + % Usage + % ----- + % :code:`UniformMps.new(fun, pspaces, vspaces)` + % + % Arguments + % --------- + % fun : :class:`function_handle` + % function to initialize the tensor. + % + % Repeating Arguments + % ------------------- + % pspaces : :class:`.AbstractSpace` + % physical spaces for each site. + % + % vspaces : :class:`.AbstractSpace` + % virtual spaces between each site. (entry `i` corresponds to left of site + % `i`.) + + arguments + fun = [] + end + arguments (Repeating) + pspaces + vspaces + end + + if isempty(fun), fun = @randnc; end + L = length(pspaces); + + for w = length(pspaces):-1:1 + rankdeficient = vspaces{w} * prod(pspaces{w}) < vspaces{next(w, L)} || ... + vspaces{w} > prod(pspaces{w})' * vspaces{next(w, L)}; + if rankdeficient + error('mps:rank', ... + 'Cannot create a full rank mps with given spaces.'); + end + + A{w} = MpsTensor(Tensor.new(fun, [vspaces{w} pspaces{w}], ... + vspaces{next(w, L)}), 0); + end + + mps = UniformMps(A); + end + + function mps = randnc(pspaces, vspaces) + % Create a uniform matrix product state with random entries. + % + % See Also + % -------- + % :meth:`.UniformMps.new` + + arguments (Repeating) + pspaces + vspaces + end + args = [pspaces; vspaces]; + mps = UniformMps.new(@randnc, args{:}); + end + end + + + %% Properties + methods + function p = period(mps) + % Period over which the mps is translation invariant. + p = length(mps(1).AL); + end + + function d = depth(mps) + % Number of lines in a multi-line mps. + d = size(mps, 1); + end + + function mps = horzcat(mps, mps2, varargin) + mps.AL = horzcat(mps.AL, mps2.AL); + mps.AR = horzcat(mps.AR, mps2.AR); + mps.C = horzcat(mps.C, mps2.C); + mps.AC = horzcat(mps.AC, mps2.AC); + if nargin > 2 + mps = horzcat(mps, varargin{:}); + end + end + + function mps = vertcat(varargin) + for i = 2:length(varargin) + assert(period(varargin{1}) == period(varargin{i}), ... + 'Can only stack UniformMps with matching periods.') + end + mps = builtin('vertcat', varargin{:}); + end + + function s = leftvspace(mps, w) + % Return the virtual space to the left of site `w`. + if nargin == 1 || isempty(w), w = 1:period(mps); end + s = arrayfun(@leftvspace, mps.AL{w}); + end + + function s = pspace(mps, w) + % Return the physical space at site `w`. + if nargin == 1 || isempty(w), w = 1:period(mps); end + s = pspace(mps.AL{w}); + end + + function s = rightvspace(mps, w) + % Return the virtual space to the right of site `w`. + if nargin == 1 || isempty(w), w = 1:period(mps); end + s = arrayfun(@rightvspace, mps.AL{w}); + end + + function type = underlyingType(mps) + type = underlyingType(mps(1).AR{1}); + end + end + + + %% Methods + methods + function [mps, lambda] = canonicalize(mps, kwargs) + % Compute the center-gauged form of an mps. + % + % Usage + % ----- + % :code:`[mps, lambda] = canonicalize(mps, kwargs)` + % + % Arguments + % --------- + % mps : :class:`.UniformMps` + % input mps, from which AL or AR is used as the state, and optionally C as an + % initial guess for the gauge. + % + % Keyword Arguments + % ----------------- + % Tol : :class:`numeric` + % tolerance for the algorithm. + % + % MaxIter : :class:`integer` + % maximum number of iterations. + % + % Method : :class:`char` + % algorithm used for decomposition. Must be 'polar', 'qr' or 'qrpos'. + % + % Verbosity : :class:`.Verbosity` + % level of output. + % + % DiagC : :class:`logical` + % flag to indicate if `C` needs to be diagonalized. + % + % ComputeAC : :class:`logical` + % flag to indicate if `AC` needs to be computed. + % + % Order : :class:`char`, 'lr' or 'rl' + % order of gauge fixing: + % + % - 'lr' uses AL as input tensors, first :code:`leftorth`, then :class:`rightorth`. + % - 'rl' uses AR as input tensors, first :class:`rightorth`, then :code:`leftorth`. + + arguments + mps + kwargs.Tol = eps(underlyingType(mps))^(3/4) + kwargs.MaxIter = 1e3 + kwargs.Method {mustBeMember(kwargs.Method, {'polar', 'qr', 'qrpos'})} ... + = 'polar' + kwargs.Verbosity = Verbosity.warn + kwargs.DiagC = false + kwargs.ComputeAC = true + kwargs.Order {mustBeMember(kwargs.Order, {'lr', 'rl'})} = 'lr' + end + + for i = 1:depth(mps) + if strcmp(kwargs.Order, 'rl') + [mps(i).AR, ~, ~, eta1] = uniform_rightorth(... + mps(i), [], ... + 'Tol', kwargs.Tol, 'MaxIter', kwargs.MaxIter, ... + 'Method', kwargs.Method, 'Verbosity', kwargs.Verbosity); + mps(i).AL = mps(i).AR; + [mps(i).AL, mps(i).C, lambda, eta2] = uniform_leftorth(... + mps(i), mps(i).C, ... + 'Tol', kwargs.Tol, 'MaxIter', kwargs.MaxIter, ... + 'Method', kwargs.Method, 'Verbosity', kwargs.Verbosity); + else + [mps(i).AL, ~, ~, eta1] = uniform_leftorth(... + mps(i), {}, ... + 'Tol', kwargs.Tol, 'MaxIter', kwargs.MaxIter, ... + 'Method', kwargs.Method, 'Verbosity', kwargs.Verbosity); + mps(i).AR = mps(i).AL; + [mps(i).AR, mps(i).C, lambda, eta2] = uniform_rightorth(... + mps(i), mps(i).C, ... + 'Tol', kwargs.Tol, 'MaxIter', kwargs.MaxIter, ... + 'Method', kwargs.Method, 'Verbosity', kwargs.Verbosity); + end + end + + if kwargs.DiagC + mps = diagonalizeC(mps); + end + + if kwargs.ComputeAC + for d = 1:depth(mps) + for w = 1:period(mps) + mps(d).AC{w} = computeAC(mps, d, w); + end + end + end + end + + function AC = computeAC(mps, d, w, type) + arguments + mps + d + w + type = 'L' + end + if strcmp(type, 'L') + AC = multiplyright(mps(d).AL{w}, mps(d).C{w}); + elseif strcmp(type, 'R') + AC = multiplyleft(mps(d).AR{w}, mps(d).C{prev(w, period(mps))}); + else + error('mps:argerror', 'invalid type %s', type) + end + end + + function AC2 = computeAC2(mps, d, w, type) + arguments + mps + d + w + type = 'R' + end + + ww = next(w, period(mps)); + + if strcmp(type, 'R') + assert(mps(d).AC{w}.alegs == 0, 'tba'); + AC2 = MpsTensor(contract(... + mps(d).AC{w}, ... + [-(1:mps(d).AC{w}.plegs+1), 1], ... + mps(d).AR{ww}, ... + [1, -(1:mps(d).AR{ww}.plegs+1) - 1 - mps(d).AR{ww}.plegs], ... + 'Rank', [1 + mps(d).AC{w}.plegs, 1 + mps(d).AR{ww}.plegs])); + elseif strcmp(type, 'L') + assert(mps(d).AC{ww}.alegs == 0, 'tba'); + AC2 = MpsTensor(contract(... + mps(d).AL{w}, ... + [-(1:mps(d).AL{w}.plegs+1), 1], ... + mps(d).AC{ww}, ... + [1, -(1:mps(d).AR{ww}.plegs+1) - 1 - mps(d).AC{ww}.plegs], ... + 'Rank', [1 + mps(d).AL{w}.plegs, 1 + mps(d).AR{ww}.plegs])); + else + error('mps:argerror', 'invalid type %s', type) + end + end + + function mps = diagonalizeC(mps) + % Gauge transform an mps such that C is diagonal. + + for i = 1:depth(mps) + for w = 1:period(mps(i)) + C_iw = mps(i).C{w}; + [U, S, V] = tsvd(C_iw, 1, 2); + + if isdual(C_iw.codomain) ~= isdual(S.codomain) + U.domain = conj(U.domain); + S.codomain = conj(S.codomain); + S = twist(S, 1); + end + + if isdual(C_iw.domain) ~= isdual(S.domain) + V.codomain = conj(V.codomain); + S.domain = conj(S.domain); + S = twist(S, 2); + end + + ww = next(w, period(mps(i))); + mps(i).C{w} = S; + + mps(i).AL{ww} = multiplyleft(mps(i).AL{ww}, U'); + mps(i).AL{w} = multiplyright(mps(i).AL{w}, U); + + mps(i).AR{ww} = multiplyleft(mps(i).AR{ww}, V); + mps(i).AR{w} = multiplyright(mps(i).AR{w}, V'); + end + end + end + + function mps = normalize(mps) + % Normalize an mps state. + + mps.C = arrayfun(@normalize, mps.C); + mps.AC = arrayfun(@normalize, mps.AC); + end + + function mps = circshift(mps, k) + if length(k) > 1 + error('tba'); + end + mps.AR = circshift(mps.AR, k); + mps.AL = circshift(mps.AL, k); + mps.C = circshift(mps.C, k); + mps.AC = circshift(mps.AC, k); + end + + function T = transfermatrix(mps1, mps2, sites, kwargs) + % A finite matrix product operator that represents the transfer matrix of an + % mps. + % + % Usage + % ----- + % :code:`T = transfermatrix(mps1, mps2, sites, kwargs)` + % + % Arguments + % --------- + % mps1 : :class:`.UniformMps` + % input mps for top layer. + % + % mps2 : :class:`.UniformMps` + % input mps for bottom layer, by default equal to the top. + % + % sites : :class:`int` + % optionally slice the unit cell of the mps and only define the transfer + % matrix for this slice. + % + % Keyword Arguments + % ----------------- + % Type : :class:`char` + % 'LL', 'LR', 'RL', 'RR' to determine if the top or bottom respectively are AL + % or AR. + % + % Returns + % ------- + % T : :class:`.FiniteMpo` + % transfer matrix of an mps, acting to the left. + + arguments + mps1 + mps2 = mps1 + sites = 1:period(mps1) + kwargs.Type {mustBeMember(kwargs.Type, {'LL' 'LR' 'RL' 'RR'})} = 'RR' + end + + assert(all(diff(sites) == 1), 'sites must be neighbouring and increasing.'); + + if kwargs.Type(1) == 'L' + A1 = mps1.AL(sites); + else + A1 = mps1.AR(sites); + end + if kwargs.Type(2) == 'L' + A2 = mps2.AL(sites); + else + A2 = mps2.AR(sites); + end + + T = cellfun(@transfermatrix, A1, A2); + end + + function rho = fixedpoint(mps, type, w) + % compute the fixed point of the transfer matrix of an mps. + % + % Usage + % ----- + % :code:`rho = fixedpoint(mps, type, w)` + % + % Arguments + % --------- + % mps : :class:`.UniformMps` + % input state. + % + % type : :class:`char` + % specification of the type of transfer matrix: + % general format: :code:`sprintf(%c_%c%c, side, top, bot)` where side is 'l' or 'r' to + % determine which fixedpoint, and top and bot are 'L' or 'R' to specify + % whether to use AL or AR in the transfer matrix. + % + % w : :class:`int` + % position within the mps unitcell of the fixed point. + + arguments + mps + type {mustBeMember(type, ... + {'l_LL' 'l_LR' 'l_RL' 'l_RR' 'r_LL' 'r_LR' 'r_RL' 'r_RR'})} + w = strcmp(type(1), 'l') * 1 + strcmp(type(1), 'r') * period(mps) + end + + ww = prev(w, period(mps)); + switch type + case 'l_RR' + rho = contract(mps.C{ww}', [-1 1], mps.C{ww}, [1 -2], 'Rank', [1 1]); +% if isdual(space(rho, 1)), rho = twist(rho, 1); end + case 'l_RL' + rho = mps.C{ww}; +% if isdual(space(rho, 1)), rho = twist(rho, 1); end + case 'l_LR' + rho = mps.C{ww}'; +% if isdual(space(rho, 1)), rho = twist(rho, 1); end + case 'l_LL' + rho = mps.C{w}.eye(leftvspace(mps, w), leftvspace(mps, w)); + if isdual(space(rho, 1)), rho = twist(rho, 1); end + + case 'r_RR' + rho = mps.C{w}.eye(rightvspace(mps, w)', rightvspace(mps, w)'); + if isdual(space(rho, 2)), rho = twist(rho, 2); end + case 'r_RL' + rho = twist(mps.C{w}', 2); +% if isdual(space(rho, 1)), rho = twist(rho, 2); end + case 'r_LR' + rho = twist(mps.C{w}, 2); +% if ~isdual(space(rho, 2)), rho = twist(rho, 2); end + case 'r_LL' + rho = contract(mps.C{w}, [-1 1], mps.C{w}', [1 -2], 'Rank', [1 1]); + rho = twist(rho, 2); + end + end + + function [V, D] = transfereigs(mps1, mps2, howmany, which, eigopts, kwargs) + % Compute the eigenvalues of the transfer matrix of an mps. + % + % Usage + % ----- + % :code:`[V, D] = transfereigs(mps1, mps2, howmany, which, eigopts, kwargs)` + % + % Arguments + % --------- + % mps1 : :class:`.UniformMps` + % input mps for top layer. + % + % mps2 : :class:`.UniformMps` + % input mps for bottom layer. Default value equal to `mps1`. + % + % howmany : :class:`int` + % number of eigenvectors and eigenvalues to compute. + % + % which : :class:`char` + % type of eigenvectors to target. + % + % Keyword Arguments + % ----------------- + % eigopts + % see keyword arguments for :func:`.eigsolve`. + % + % Verbosity : :class:`int` + % detail level for output. + % + % Type : :class:`char` + % type of transfer matrix to construct, see + % :meth:`.UniformMps.transfermatrix`. + % + % Charge : :class:`.AbstractCharge` + % charge of eigenvectors to target. + + arguments + mps1 + mps2 = mps1 + howmany = min(20, dim(leftvspace(mps1, 1) * leftvspace(mps2, 1)')) + which = 'largestabs' + eigopts.Algorithm = 'Arnoldi' + eigopts.KrylovDim = 100 + eigopts.MaxIter = 1000 + eigopts.ReOrth = 2 + eigopts.Tol = eps(underlyingType(mps1))^(3/4) + kwargs.Verbosity = 0 + kwargs.Type {mustBeMember(kwargs.Type, ... + {'l_LL' 'l_LR' 'l_RL' 'l_RR' 'r_LL' 'r_LR' 'r_RL' 'r_RR'})} = 'r_RR' + kwargs.Charge = [] + end + + T = transfermatrix(mps1, mps2, 'Type', kwargs.Type(3:4)); + if kwargs.Type(1) == 'r', T = T'; end + + if ~isempty(kwargs.Charge) + Tdomain = domain(T); + auxspace = Tdomain.new(... + struct('charges', kwargs.Charge, 'degeneracies', 1), ... + false); + v0 = Tensor.randnc([Tdomain auxspace], []); + else + v0 = []; + end + + eigopts.Verbosity = kwargs.Verbosity; + eigkwargs = namedargs2cell(eigopts); + [V, D] = eigsolve(T, v0, howmany, which, eigkwargs{:}); + + if kwargs.Type(1) == 'r', V = V'; end + if nargout < 2, V = diag(D); end + end + + function f = fidelity(mps1, mps2, kwargs) + % Compute the fidelity between two uniform MPSs. + + arguments + mps1 + mps2 + kwargs.KrylovDim = 30 + kwargs.MaxIter = 500 + kwargs.ReOrth = 2 + kwargs.Tol = eps(underlyingType(mps1))^(3/4) + kwargs.Verbosity = 0 + end + + eigkwargs = [fieldnames(kwargs).'; struct2cell(kwargs).']; + f = transfereigs(mps1, mps2, 1, 'largestabs', eigkwargs{:}); + end + + function E = expectation_value(mps1, O, mps2) + % Compute the expectation value of an operator. + + arguments + mps1 + O + mps2 = mps1 + end + + if isa(O, 'InfJMpo') + [GL, GR] = environments(O, mps1, mps2); + Hs = AC_hamiltonian(O, mps1, GL, GR, period(mps1)); + H = Hs{1}; + N = size(H.R.var, 2); + H.R.var = H.R.var(1, N, 1); + H.O{1} = H.O{1}(:, :, N, :); + AC_ = apply(H, mps1.AC{end}); + E = dot(AC_, mps2.AC{end}); + + elseif isa(O, 'InfMpo') + [GL, GR] = environments(O, mps1, mps2); % should be normalized + Hs = AC_hamiltonian(O, mps1, GL, GR); + E = zeros(size(Hs)); + for i = 1:length(Hs) + AC_ = apply(Hs{i}, mps1.AC{i}); + E(i) = dot(AC_, mps2.AC{i}); + end + E = prod(E); + + elseif isa(O, 'AbstractTensor') + E = local_expectation_value(mps1, O); + else + error('Unknown operator type (%s)', class(O)); + end + end + + function E = local_expectation_value(mps, O, offset) + % Compute the expectation value of a local operator. + + arguments + mps + O + offset = 0 % site offset + end + + local_ops = MpoTensor.decompose_local_operator(O); + N = length(local_ops); + + A = [mps.AC(1 + offset) mps.AR(2:(N - 1) + offset)]; + + T = FiniteMpo.mps_channel_operator(A, local_ops, A); + rhoL = insert_onespace(fixedpoint(mps, 'l_LL', offset+ 1), 2, ... + ~isdual(space(T(1).O{1}, 4))); + rhoR = insert_onespace(fixedpoint(mps, 'r_RR', offset + N), 2, ... + ~isdual(space(T(1).O{1}, 2))); + E1 = apply(T, rhoL); + E = contract(E1, 1:3, rhoR, flip(1:3)); + end + + function [svals, charges] = schmidt_values(mps, w) + % Compute the Schmidt values and corresponding charges for an entanglement cut + % to the right of site :code:`w`. + % + % Usage + % ----- + % :code:`[svals, charges] = schmidt_values(mps, w)` + + arguments + mps + w = 1 + end + + [svals, charges] = matrixblocks(tsvd(mps.C{w})); + svals = cellfun(@diag, svals, 'UniformOutput', false); + end + + function plot_entanglementspectrum(mps, d, w, ax, kwargs) + % Plot the entanglement spectrum of a uniform MPS. + + arguments + mps + d = 1:depth(mps) + w = 1:period(mps) + ax = [] + kwargs.SymmetrySort = true + kwargs.ExpandQdim = false + end + if isempty(ax) + figure; + ax = gobjects(depth(mps), width(mps)); + for dd = 1:length(d) + for ww = 1:length(w) + ax(dd, ww) = subplot(length(d), length(w), ww + (dd-1)*length(w)); + end + end + end + for dd = 1:length(d) + for ww = 1:length(w) + hold(ax(dd,ww), 'off'); + [svals, charges] = schmidt_values(mps(dd), w(ww)); + if kwargs.ExpandQdim + for i = 1:length(svals) + svals{i} = reshape(repmat(svals{i}, 1, qdim(charges(i))), [], 1); + end + end + ctr = 0; + labels = arrayfun(@string, charges, 'UniformOutput', false); + lengths = cellfun(@length, svals); + ticks = cumsum(lengths); + if kwargs.SymmetrySort + for i = 1:length(svals) + semilogy(ax(dd, ww), ctr+(1:lengths(i)), svals{i}, '.', 'MarkerSize', 10, 'Color', colors(i)); + if i == 1, hold(ax(dd,ww), 'on'); end + ctr = ctr + lengths(i); + end + set(ax(dd, ww), 'Xtick', ticks, 'fontsize', 10, ... + 'XtickLabelRotation', 60, 'Xgrid', 'on'); + else + [~, p] = sort(vertcat(svals{:}), 'descend'); + p = invperm(p); + for i = 1:length(svals) + semilogy(ax(dd, ww), p(ctr+(1:lengths(i))), svals{i}, '.', 'MarkerSize', 10, 'Color', colors(i)); + if i == 1, hold(ax(dd,ww), 'on'); end + ctr = ctr + lengths(i); + end + + end + legend(ax(dd, ww), labels) + set(ax(dd, ww), 'TickLabelInterpreter', 'latex'); + xlim(ax(dd, ww), [1 - 1e-8 ticks(end) + 1e-8]); + end + end + hold off + linkaxes(ax, 'y'); + end + + function mps = desymmetrize(mps) + if numel(mps) > 1 + mps = arrayfun(@desymmetrize, mps); + return + end + mps.AL = desymmetrize(mps.AL); + mps.AR = desymmetrize(mps.AR); + mps.C = desymmetrize(mps.C); + mps.AC = desymmetrize(mps.AC); + end + + function [xi, theta] = correlation_length(mps, charge) + % Compute the correlation length of an MPS in a given charge sector. + % + % Usage + % ----- + % :code:`[xi, theta] = correlation_length(mps, charge)` + % + % Arguments + % --------- + % mps : :class:`.UniformMps` + % input mps. + % + % charge : :class:`.AbstractCharge` + % charge sector for correlation length to target. + % + % Returns + % ------- + % xi : :class:`numeric` + % correlation length in the given charge sector. + % + % theta : :class:`numeric` + % angle of the corresponding oscillation period. + + arguments + mps + charge = [] + end + + if isempty(charge) || charge == one(charge) + f = transfereigs(mps, mps, 5, 'KrylovDim', 30); + if abs(f(1) - 1) > 1e-12 + warning('mps:noninjective', ... + 'mps might be non-injective:\n%s', num2str(f)); + end + epsilon = -log(abs(f(2))); + theta = angle(f(2)); + else + f = transfereigs(mps, mps, 1, 'KrylovDim', 20, 'Charge', charge); + epsilon = -log(abs(f)); + theta = angle(f); + end + + xi = 1 / epsilon; + end + + function [epsilon, delta, spectrum] = marek_gap(mps, charge, kwargs) + % Compute the Marek gap of an MPS in a given charge sector. + % + % Usage + % ----- + % :code:`[epsilon, delta, spectrum] = marek_gap(mps, charge, kwargs)` + % + % Arguments + % --------- + % mps : :class:`.UniformMps` + % input mps. + % + % charge : :class:`.AbstractCharge` + % charge sector for correlation length to target. + % + % Keyword Arguments + % ----------------- + % HowMany : :class:`int` + % amount of transfer matrix eigenvalues to compute. + % + % Angle : :class:`numeric` + % angle in radians around which the gap should be computed. + % + % AngleTol : :class:`numeric` + % tolerance in radians for angles to be considered equal. + % + % Returns + % ------- + % epsilon : :class:`numeric` + % inverse correlation length in the given charge sector. + % + % delta : :class:`numeric` + % refinement parameter. + % + % spectrum : :class:`numeric` + % computed partial transfer matrix spectrum. + arguments + mps + charge = [] + kwargs.Angle + kwargs.AngleTol = 1e-1 + kwargs.HowMany = 5 + end + + spectrum = transfereigs(mps, mps, kwargs.HowMany, 'largestabs', 'Charge', charge); + [d, p] = sort(abs(spectrum), 'descend'); + inds = d > 1 - 1e-12; + + if ((isempty(charge) || charge == one(charge)) && sum(inds) > 1) || ... + sum(inds) > 0 + warning('mps:noninjective', ... + 'mps might be non-injective:\n%s', num2str(spectrum)); + end + + d(inds) = []; + p(inds) = []; + + if abs(diff(unwrap(angle(spectrum(p(1:2)))))) > 1e-1 + warning('comparing values with different angles:\n%s', num2str(spectrum)); + end + if isfield('Angle', kwargs) + error('tba'); + end + + epsilon = -log(d(1)); + delta = log(d(1) / d(2)); + end + + function S = entanglement_entropy(mps, w) + % Compute the entanglement entropy of a uniform MPS for a cut to the right of + % site :code:`w` + arguments + mps + w = 1 + end + + [svals, charges] = schmidt_values(mps, w); + + S = 0; + for i = 1:length(svals) + S = S - qdim(charges(i)) * sum(svals{i}.^2 .* log(svals{i}.^2)); + end + end + + function S = renyi_entropy(mps, n, w) + % Compute the :code:`n`-th Renyi entropy of a uniform MPS for a cut to the right + % of site :code:`w` + arguments + mps + n + w = 1 + end + + [svals, charges] = schmidt_values(mps, w); + + S = 0; + for i = 1:length(svals) + S = S + qdim(charges(i)) * sum(svals{i}.^(2 * n)); + end + S = 1 / (1 - n) * log(S); + end + + function out = truncate(mps, trunc) + % Truncate a uniform MPS according to the options specified in :code:`trunc` + % (see :meth:`.Tensor.tsvd` for details on the truncation options). + arguments + mps + trunc.TruncDim + trunc.TruncTotalDim + trunc.TruncBelow + trunc.TruncSpace + end + + trunc = [fieldnames(trunc),struct2cell(trunc)]'; + out = repmat(UniformMps, size(mps, 1), 1); + for d = 1:depth(mps) + for w = 1:period(mps(d)) + [~, ~, V] = tsvd(mps(d).C(w), 1, 2, trunc{:}); + ww = next(w, period(mps(d))); + mps(d).AR(ww) = multiplyleft(mps(d).AR(ww), V); + mps(d).AR(w) = multiplyright(mps(d).AR(w), V'); + end + % bring truncated mps to canonical form + out(d) = UniformMps(mps(d).AR); + end + end + + end + + %% Subroutines + methods (Access = protected) + function [AL, CL, lambda, eta] = uniform_leftorth(mps, CL, kwargs) + % Bring a uniform MPS into left canonical form. + % + % Usage + % ----- + % :code:`[AL, CL, lambda, eta] = uniform_leftorth(mps, CL, kwargs)` + arguments + mps + CL = {} + kwargs.Tol = eps(underlyingType(mps))^(3/4) + kwargs.MaxIter = 500 + kwargs.Method = 'polar' + kwargs.Verbosity = Verbosity.off + kwargs.Normalize = true + kwargs.EigsInit = 3 + kwargs.EigsFrequence = 2 + end + + A = mps.AL; +% + % constants + EIG_TOLFACTOR = 1/50; + EIG_MAXTOL = 1e-4; + MINKRYLOVDIM = 8; + MAXKRYLOVDIM = 30; + + + + % initialization + N = period(mps); + if isempty(CL) + CL = initializeC(A{:}); + end + + if kwargs.Normalize, CL{1} = normalize(CL{1}); end + + for i = 1:numel(A) + A{i} = repartition(A{i}, [nspaces(A{i}) - 1, 1]); + end + AL = A; + + eta_best = Inf; + ctr_best = 0; + AL_best = AL; + C_best = CL; + lambda_best = 0; + + for ctr = 1:kwargs.MaxIter + if ctr > kwargs.EigsInit && mod(ctr, kwargs.EigsFrequence) == 0 + C_ = repartition(CL{end}, [2 0]); + T = cellfun(@transfermatrix, A, AL); + + eigalg = Arnoldi('Tol', min(eta * EIG_TOLFACTOR, EIG_MAXTOL), ... + 'KrylovDim', between(MINKRYLOVDIM, ... + MAXKRYLOVDIM - ctr / 2 + 4, ... + MAXKRYLOVDIM), ... + 'NoBuild', 4, ... + 'Verbosity', kwargs.Verbosity - 1); + + [C_, ~] = eigsolve(eigalg, @(x) T.apply(x), C_, 1, 'largestabs'); + [~, CL{end}] = leftorth(C_, 1, 2, kwargs.Method); + if isdual(space(CL{end}, 2)) == isdual(space(CL{end}, 1)) + CL{end}.codomain = conj(CL{end}.codomain); + CL{end} = twist(CL{end}, 1); + end + end + + C_ = CL{end}; + lambdas = ones(1, N); + for w = 1:N + ww = prev(w, N); + CA = multiplyleft(A{w}, CL{ww}); + [AL{w}, CL{w}] = leftorth(CA, kwargs.Method); + lambdas(w) = norm(CL{w}); + if kwargs.Normalize, CL{w} = CL{w} ./ lambdas(w); end + end + lambda = prod(lambdas); + eta = norm(C_ - CL{end}, Inf); + if eta < kwargs.Tol + if kwargs.Verbosity >= Verbosity.conv + fprintf('Conv %2d:\terror = %0.4e\n', ctr, eta); + end + break; + elseif eta < eta_best + eta_best = eta; + ctr_best = ctr; + AL_best = AL; + C_best = CL; + lambda_best = lambda; + elseif ctr > 40 && ctr - ctr_best > 5 + warning('uniform_orthright:stagnate', 'Algorithm stagnated'); + eta = eta_best; + AL = AL_best; + CL = C_best; + lambda = lambda_best; + break; + end + + if kwargs.Verbosity >= Verbosity.iter + fprintf('Iter %2d:\terror = %0.4e\n', ctr, eta); + end + end + + if kwargs.Verbosity >= Verbosity.warn && eta > kwargs.Tol + fprintf('Not converged %2d:\terror = %0.4e\n', ctr, eta); + end + end + + function [AR, CR, lambda, eta] = uniform_rightorth(mps, CR, kwargs) + % Bring a uniform MPS into right canonical form. + % + % Usage + % ----- + % :code:`[AR, CR, lambda, eta] = uniform_rightorth(mps, CR, kwargs)` + arguments + mps + CR = mps.C + kwargs.Tol = eps(underlyingType(mps))^(3/4) + kwargs.MaxIter = 500 + kwargs.Method = 'polar' + kwargs.Verbosity = 0 + kwargs.Normalize = true + kwargs.EigsInit = 3 + kwargs.EigsFrequence = 2 + end + + opts = namedargs2cell(kwargs); + mps_d = mps; + mps_d.AL = flip(cellfun(@ctranspose, mps.AR, 'UniformOutput', false)); + + if isempty(CR) + Cd = {}; + else + Cd = circshift(flip(cellfun(@ctranspose, CR, 'UniformOutput', false)), -1); + end + + [ARd, CRd, lambda, eta] = uniform_leftorth(mps_d, Cd, opts{:}); + + AR = flip(cellfun(@ctranspose, ARd, 'UniformOutput', false)); + CR = circshift(flip(cellfun(@ctranspose, CRd, 'UniformOutput', false)), -1); + lambda = conj(lambda); + end + end +end + diff --git a/src/mps/UniformPeps.m b/src/mps/UniformPeps.m new file mode 100644 index 0000000..dd881bb --- /dev/null +++ b/src/mps/UniformPeps.m @@ -0,0 +1,163 @@ +classdef UniformPeps + % Implementation of infinite translation invariant PEPS + % + % Properties + % ---------- + % A : :class:`cell` of :class:`.PepsTensor` + % cell array of PEPS tensors in 2D unit cell. + % + % Todo + % ---- + % Document. + + properties + A cell + end + + + %% Constructors + methods + function peps = UniformPeps(varargin) + % Usage + % ----- + % :code:`peps = UniformPeps(A)` + % + % Arguments + % --------- + % A : :class:`cell` of :class:`.PepsTensor` + % cell array of PEPS tensors in 2D unit cell. + % + % Returns + % ------- + % peps : :class:`.UniformPeps` + % infinite translation-invariant PEPS. + + if nargin == 0, return; end % default empty constructor + + if nargin == 1 + if isa(varargin{1}, 'UniformPeps') % copy constructor + peps.A = varargin{1}.A; + + elseif isa(varargin{1}, 'Tensor') + peps.A{1} = PepsTensor(varargin{1}); + + elseif isa(varargin{1}, 'PepsTensor') + peps.A{1} = varargin{1}; + + elseif iscell(varargin{1}) + assert(isa(varargin{1}{1},'PepsTensor')) + for i = height(varargin{1}):-1:1 + for j = width(varargin{1}):-1:1 + peps.A{i,j} = varargin{1}{i,j}; + end + end + else + error('Invalid constructor for UniformPeps.') + end + + else + error('Invalid constructor for UniformPeps.') + end + end + end + + %% Properties + methods + function h = height(peps) + % vertical period over which the peps is translation invariant. + h = height(peps.A); + end + + function w = width(peps) + % horizontal period over which the peps is translation invariant. + w = width(peps.A); + end + + function s = westvspace(peps, h, w) + % return the virtual space to the left of site (h,w). + if nargin == 1, h = 1:height(peps); w = 1:width(peps); end + s = cellfun(@westvspace, peps.A(h,w)); + end + + function s = southvspace(peps, h, w) + % return the virtual space to the bottom of site (h,w). + if nargin == 1, h = 1:height(peps); w = 1:width(peps); end + s = cellfun(@southvspace, peps.A(h,w)); + end + + function s = eastvspace(peps, h, w) + % return the virtual space to the right of site (h,w). + if nargin == 1, h = 1:height(peps); w = 1:width(peps); end + s = cellfun(@eastvspace, peps.A(h,w)); + end + + function s = northvspace(peps, h, w) + % return the virtual space to the top of site (h,w). + if nargin == 1, h = 1:height(peps); w = 1:width(peps); end + s = cellfun(@northvspace, peps.A(h,w)); + end + + function s = pspace(peps, h, w) + % return the physical space at site (h,w). + if nargin == 1, h = 1:height(peps); w = 1:width(peps); end + s = cellfun(@pspace, peps.A(h,w)); + end + + function peps = rot90(peps) + peps.A = cellfun(@(x)rot90(x),peps.A.','UniformOutput',false); + end + + function peps = rot270(peps) + peps.A = cellfun(@(x)rot270(x),peps.A.','UniformOutput',false); + end + + function type = underlyingType(peps) + type = underlyingType(peps.A{1,1}); + end + end + + + %% Methods + methods + %build horizontalTransferMatrix + + %build verticalTransferMatrix + + %CtmrgEnvironment + + function ctmrgenv = CtmrgEnvironment(peps_top, peps_bot, varargin) + + h = height(peps_top); + w = width(peps_top); + C = cell(4, h, w); + T = cell(4, h, w); + + if nargin == 2 + vspace = repmat(one(space(peps_top.A{1})), h, w); + elseif all(size(varargin{1})==[1,1]) + vspace = repmat(varargin{1}, h, w); + else + vspace = varargin{1}; + end + + for i = 1:h + for j = 1:w + C{1,i,j} = Tensor.randnc(vspace(i,j), vspace(i,j)); + C{2,i,j} = Tensor.randnc(vspace(i,prev(j,w)), vspace(i,prev(j,w))); + C{3,i,j} = Tensor.randnc(vspace(prev(i,h),prev(j,w)), vspace(prev(i,h),prev(j,w))); + C{4,i,j} = Tensor.randnc(vspace(prev(i,h),j), vspace(prev(i,h),j)); + + T{1,i,j} = Tensor.randnc([vspace(i,prev(j,w)), northvspace(peps_top,next(i,h),j)', northvspace(peps_bot,next(i,h),j)], vspace(i,j)); + T{2,i,j} = Tensor.randnc([vspace(prev(i,h),prev(j,w)), eastvspace(peps_top,i,prev(j,w))', eastvspace(peps_bot,i,prev(j,w))], vspace(i,prev(j,w))); + T{3,i,j} = Tensor.randnc([vspace(prev(i,h),j), southvspace(peps_top,prev(i,h),j)', southvspace(peps_bot,prev(i,h),j)], vspace(prev(i,h),prev(j,w))); + T{4,i,j} = Tensor.randnc([vspace(i,j), westvspace(peps_top,i,next(j,w))', westvspace(peps_bot,i,next(j,w))], vspace(prev(i,h),j)); + end + end + + ctmrgenv = CtmrgEnvironment(C, T); + + end + + end +end + diff --git a/src/sparse/SparseTensor.m b/src/sparse/SparseTensor.m new file mode 100644 index 0000000..b9b41b4 --- /dev/null +++ b/src/sparse/SparseTensor.m @@ -0,0 +1,1050 @@ +classdef (InferiorClasses = {?Tensor}) SparseTensor < AbstractTensor + % Class for multi-dimensional sparse objects. + % + % Todo + % ---- + % Document properties, behavior and methods. + + %#ok<*PROPLC> + + properties + codomain + domain + end + + properties (Access = private) + ind = [] + sz = [] + var (:, 1) Tensor = Tensor.empty(0, 1); + end + + %% Constructors + methods + function t = SparseTensor(varargin) + if nargin == 0 || (nargin == 1 && isempty(varargin{1})) + return; + + elseif nargin == 1 % cast from existing object + source = varargin{1}; + + if isa(source, 'SparseTensor') + t.ind = source.ind; + t.sz = source.sz; + t.var = source.var; + + t.codomain = source.codomain; + t.domain = source.domain; + + elseif isa(source, 'Tensor') + t.sz = ones(1, nspaces(source(1))); + t.sz(1:ndims(source)) = size(source); + + t.ind = ind2sub_(t.sz, 1:numel(source)); + t.var = source(:); + + [t.codomain, t.domain] = deduce_spaces(t); + else + error('sparse:ArgError', 'Unknown syntax.'); + end + + elseif nargin == 2 % indices and values + ind = varargin{1}; + var = reshape(varargin{2}, [], 1); + if isscalar(var), var = repmat(var, size(ind, 1), 1); end + assert(size(ind, 1) == size(var, 1), 'sparse:argerror', ... + 'indices and values must be the same size.'); + t.ind = ind; + t.var = var; + t.sz = max(ind, [], 1); + [t.codomain, t.domain] = deduce_spaces(t); + + elseif nargin == 3 % indices, values and size + ind = varargin{1}; + if ~isempty(ind) && ~isempty(varargin{2}) + var = reshape(varargin{2}, [], 1); + if isscalar(var), var = repmat(var, size(ind, 1), 1); end + assert(size(ind, 1) == size(var, 1), 'sparse:argerror', ... + 'indices and values must be the same size.'); + sz = reshape(varargin{3}, 1, []); + assert(isempty(ind) || size(ind, 2) == length(sz), 'sparse:argerror', ... + 'number of indices does not match size vector.'); + assert(isempty(ind) || all(max(ind, [], 1) <= sz), 'sparse:argerror', ... + 'indices must not exceed size vector.'); + t.var = var; + else + sz = reshape(varargin{3}, 1, []); + end + t.ind = ind; + t.sz = sz; + [t.codomain, t.domain] = deduce_spaces(t); + + elseif nargin == 5 % indices, values, size, codomain, domain + ind = varargin{1}; + if ~isempty(ind) && ~isempty(varargin{2}) + var = reshape(varargin{2}, [], 1); + if isscalar(var), var = repmat(var, size(ind, 1), 1); end + assert(size(ind, 1) == size(var, 1), 'sparse:argerror', ... + 'indices and values must be the same size.'); + sz = reshape(varargin{3}, 1, []); + assert(isempty(ind) || size(ind, 2) == length(sz), 'sparse:argerror', ... + 'number of indices does not match size vector.'); + assert(isempty(ind) || all(max(ind, [], 1) <= sz), 'sparse:argerror', ... + 'indices must not exceed size vector.'); + t.var = var; + else + sz = reshape(varargin{3}, 1, []); + ind = double.empty(0, size(sz, 2)); + end + t.ind = ind; + t.sz = sz; + t.codomain = varargin{4}; + t.domain = varargin{5}; + end + end + end + + methods (Static) + function t = new(f, codomain, domain, kwargs) + arguments + f + codomain SumSpace + domain SumSpace + kwargs.Density = 1 + end + + sz = [nsubspaces(codomain) flip(nsubspaces(domain))]; + + inds = sort(randperm(prod(sz), round(prod(sz) * kwargs.Density))); + subs = ind2sub_(sz, inds); + vars = Tensor.empty(0, 1); + for i = length(inds):-1:1 + for j = length(codomain):-1:1 + subcodomain(j) = subspaces(codomain(j), subs(i, j)); + end + for j = length(domain):-1:1 + subdomain(j) = subspaces(domain(j), subs(i, end + 1 - j)); + end + vars(i) = Tensor.new(f, subcodomain, subdomain); + end + t = SparseTensor(subs, vars, sz, codomain, domain); + end + + function t = rand(varargin) + t = SparseTensor.new(@rand, varargin{:}); + end + + function t = randc(varargin) + t = SparseTensor.new(@randc, varargin{:}); + end + + function t = randnc(varargin) + t = SparseTensor.new(@randnc, varargin{:}); + end + + function t = zeros(codomain, domain, kwargs) + arguments + codomain + domain + kwargs.Density = 0 + end + t = SparseTensor.new(@zeros, codomain, domain, 'Density', kwargs.Density); + end + + function t = eye(codomain, domain) + t = SparseTensor.zeros(codomain, domain); + + sz1 = size(t, 1:length(codomain)); + sz2 = size(t, length(codomain) + (1:length(domain))); + n = prod(sz1); + assert(n == prod(sz2)); + + inds = sub2ind_([n n], [1:n; 1:n].'); + for i = flip(1:n) + t.ind(i,:) = ind2sub_(t.sz, inds(i)); + [cod, dom] = slice(codomain, domain, t.ind(i,:)); + t.var(i) = Tensor.eye(cod, dom); + end + end + end + + + %% Utility + methods + function [codomain, domain] = deduce_spaces(t) + spaces = cell(1, ndims(t)); + for i = 1:length(spaces) + todo = false(1, size(t, i)); + for j = flip(1:size(t, i)) + idx = find(t.ind(:, i) == j, 1); + if isempty(idx) + warning('sparse:argerror', ... + 'Cannot deduce %dth space at index %d.', i, j); + todo(j) = true; + else + spaces{i}(j) = space(t.var(idx), i); + end + end + + if any(todo) + I = find(~todo, 1); + if isempty(I) + error('unable to deduce spaces automatically'); + end + E = one(spaces{i}(I)); + if isdual(spaces{i}(I)) + E = conj(E); + end + for J = find(todo) + spaces{i}(J) = E; + end + end + end + Nout = indout(t.var(1)); + codomain = SumSpace(spaces{1:Nout}); + domain = SumSpace(spaces{(Nout+1):end})'; + end + + function B = full(A) + inds = ind2sub_(A.sz, 1:prod(A.sz)); + [lia, locb] = ismember(inds, A.ind, 'rows'); + B(lia) = A.var(locb(lia)); + if ~all(lia) + s = arrayfun(@(i) subspaces(space(A, i)), 1:ndims(A), 'UniformOutput', false); + r = rank(A); + for i = find(~lia).' + allspace = arrayfun(@(j) s{j}(inds(i, j)), 1:length(s)); + B(i) = Tensor.zeros(allspace(1:r(1)), allspace(r(1)+1:end)'); + end + end + + B = reshape(B, A.sz); + end + + function A = sparse(A) + end + + function sp = space(t, inds) + sp = [t.codomain t.domain']; + if nargin > 1 + sp = sp(inds); + end + end + + function n = nspaces(t) + n = length(t.domain) + length(t.codomain); + end + + function n = ndims(A) + n = length(A.sz); + end + + function r = rank(t, i) + r = [length(t.codomain) length(t.domain)]; + if nargin > 1 + r = r(i); + end + end + + function varargout = size(a, i) + if nargin == 1 + sz = a.sz; + else + sz = ones(1, max(i)); + sz(1:length(a.sz)) = a.sz; + sz = sz(i); + end + + if nargout <= 1 + varargout = {sz}; + else + varargout = num2cell(sz); + end + end + + function n = numel(t) + n = prod(t.sz); + end + + function disp(t) + r = t.rank; + nz = nnz(t); + if nz == 0 + fprintf('all-zero rank (%d, %d) %s:\n', r(1), r(2), class(t)); + else + fprintf('rank (%d, %d) %s with %d nonzeros:\n', r(1), r(2), class(t), nz); + end + s = space(t); + for i = 1:length(s) + fprintf('\t%d.\t', i); + disp(s(i)); + fprintf('\b'); + end + fprintf('\n'); + + if nz == 0 + fprintf('all-zero %s of size %s\n', class(t), ... + dim2str(t.sz)); + return + end + + fprintf('%s of size %s with %d nonzeros:\n', class(t), ... + dim2str(t.sz), nz); + + spc = floor(log10(max(double(t.ind), [], 1))) + 1; + fmt_subs = sprintf("%%%du,", spc(1:end-1)); + fmt_subs = sprintf("%s%%%du", fmt_subs, spc(end)); + fmt = sprintf("\t(%s)", fmt_subs); + for i = 1:nz + fprintf('%s\t\t', compose(fmt, t.ind(i,:))); + disp(t.var(i)); + end + end + + function type = underlyingType(a) + if isempty(a.var) + type = 'double'; + else + type = underlyingType(a.var); + end + end + + function bool = issparse(~) + bool = true; + end + + function bool = isscalar(t) + bool = prod(t.sz) == 1; + end + + function n = nnz(t) + n = length(t.var); + end + + function bools = eq(a, b) + arguments + a SparseTensor + b SparseTensor + end + + if isscalar(a) && isscalar(b) + bools = (isempty(a.var) && isempty(b.var)) || ... + (~isempty(a.var) && ~isempty(b.var) && a.var == b.var); + return + end + + if isscalar(a) + if nnz(a) == 0 + bools = true(size(b)); + if nnz(b) ~= 0 + bools(sub2ind_(b.sz, b.ind)) = false; + end + else + bools = false(size(b)); + bools(sub2ind_(b.sz, b.ind)) = a.var == b.var; + end + return + end + + if isscalar(b) + bools = b == a; + return + end + + assert(isequal(size(a), size(b)), 'sparse:dimerror', ... + 'input sizes incompatible'); + bools = true(size(a.inds)); + [inds, ia, ib] = intersect(a.ind, b.ind, 'rows'); + + bools(sub2ind_(a.sz, a.ind)) = false; + bools(sub2ind_(b.sz, b.ind)) = false; + bools(sub2ind_(a.sz, inds)) = a.var(ia) == b.var(ib); + end + + function jl = mat2jl(a) + jl = struct('classname', 'SparseTensor'); + jl.codomain = mat2jl(a.codomain); + jl.domain = mat2jl(a.domain); + jl.ind = mat2jl(a.ind); + jl.sz = mat2jl(a.sz); + jl.var = mat2jl(a.var); + end + end + + %% Linear Algebra + methods + function a = conj(a) + if ~isempty(a.var) + a.var = conj(a.var); + end + a.codomain = conj(a.codomain); + a.domain = conj(a.domain); + end + + function a = ctranspose(a) + for i = 1:nnz(a) + a.var(i) = a.var(i)'; + end + a.ind = fliplr(a.ind); + a.sz = fliplr(a.sz); + [a.codomain, a.domain] = swapvars(a.codomain, a.domain); + end + + function d = dot(a, b) + [~, ia, ib] = intersect(a.ind, b.ind, 'rows'); + if isempty(ia), d = 0; return; end + d = dot(a.var(ia), b.var(ib)); + end + + function a = minus(a, b) + a = a + (-b); + end + + function c = mtimes(a, b) + if isscalar(a) || isscalar(b) + c = a .* b; + return + end + + szA = size(a); + szB = size(b); + assert(length(szA) == 2 && length(szB) == 2, 'sparse:argerror', ... + 'mtimes only defined for matrices.'); + assert(szA(2) == szB(1), 'sparse:dimerror', ... + 'incompatible sizes for mtimes.'); + + if isnumeric(a) + c = SparseTensor.zeros(szA(1), szB(2)); + + for i = 1:szA(1) + for j = 1:szB(2) + c = subsasgn(c, substruct('()', {i, j}), ... + sum(a(i, :).' .* ... + subsref(b, substruct('()', {':', j})))); + end + end + return + end + + if isnumeric(b) + c = SparseTensor.zeros(szA(1), szB(2)); + + for i = 1:szA(1) + for j = 1:szB(2) + c = subsasgn(c, substruct('()', {i, j}), ... + sum(subsref(a, substruct('()', {i, ':'})) .* ... + b(:, j).')); + end + end + return + end + + cvar = []; + cind = double.empty(0, 2); + + for k = 1:size(a, 2) + rowlinds = a.ind(:, 2) == k; + if ~any(rowlinds), continue; end + + collinds = b.ind(:, 1) == k; + if ~any(collinds), continue; end + + rowinds = find(rowlinds); + colinds = find(collinds); + + for i = rowinds.' + av = a.var(i); + ai = a.ind(i, 1); + for j = colinds.' + bv = b.var(j); + bj = b.ind(j, 2); + + + mask = all([ai bj] == cind, 2); + if any(mask) + cvar(mask) = cvar(mask) + av * bv; + else + cvar(end+1) = av * bv; + cind = [cind; ai bj]; + end + end + end + end + c = SparseTensor(cind, cvar, [szA(1) szB(2)]); + end + + function n = norm(t, p) + arguments + t + p = 'fro' + end + + if isempty(t.var), n = 0; return; end + n = norm(t.var); + end + + function t = normalize(t) + if isempty(t.var) + warning('sparse:empty', 'cannot normalize an empty tensor.'); + end + t = t .* (1 / norm(t)); + end + + function a = plus(a, b) + n = max(ndims(a), ndims(b)); + assert(isequal(size(a, 1:n), size(b, 1:n)), ... + 'sparse:dimerror', 'input dimensions incompatible.'); + + if ~issparse(a) + if nnz(b) > 0 + idx = sub2ind_(b.sz, b.ind); + a(idx) = reshape(a(idx), [], 1) + b.var; + end + return + end + + if ~issparse(b) + a = b + a; + return + end + + if isempty(b.ind), return; end + if isempty(a.ind), a = b; return; end + + [lia, locb] = ismember(b.ind, a.ind, 'rows'); + a.var(locb(lia)) = a.var(locb(lia)) + b.var(lia); + a.var = [a.var; b.var(~lia)]; + a.ind = [a.ind; b.ind(~lia, :)]; + end + + function t = rdivide(t, a) + assert(isnumeric(a), 'method not implemented.'); + if nnz(t) > 0 + t.var = rdivide(t.var, a); + end + end + + function B = sum(A, dim) + arguments + A + dim = [] + end + + if isscalar(A) + B = A; + return + end + + if isempty(dim), dim = find(size(A) ~= 1, 1); end + + if strcmp(dim, 'all') + if nnz(A) == 0 + for i = flip(1:length(A.codomain)) + cod(i) = SumSpace(subspaces(A.codomain(i), 1)); + end + for i = flip(1:length(A.domain)) + dom(i) = SumSpace(subspaces(A.domain(i), 1)); + end + B = SparseTensor.zeros(cod, dom); + return + end + + B = sum(A.var, 'all'); + return + end + + if isvector(A) + if nnz(A) == 0 + B = SparseTensor.zeros(1, 1); + else + B = sum(A.var, 'all'); + end + return + end + + if ismatrix(A) + if dim == 1 + B = SparseTensor.zeros(1, size(A, 2)); + n = nnz(A); + for i = 1:size(A, 2) + if n == 0, break; end + idx = A.ind(:, 2) == i; + if ~any(idx), continue; end + B(1, i) = sum(A.var(idx)); + n = n - sum(idx); + end + return + end + + if dim == 2 + B = SparseTensor.zeros(size(A, 1), 1); + n = nnz(A); + for i = 1:size(A, 2) + if n == 0, break; end + idx = A.ind(:, 1) == i; + if ~any(idx), continue; end + B(i, 1) = sum(A.var(idx)); + n = n - sum(idx); + end + return + end + end + error('TBA'); + end + + function C = tensorprod(A, B, dimA, dimB, ca, cb, options) + arguments + A SparseTensor + B SparseTensor + dimA + dimB + ca = false + cb = false + options.NumDimensionsA = ndims(A) + end + + szA = size(A, 1:options.NumDimensionsA); + szB = size(B, 1:max(ndims(B), max(dimB))); + + assert(length(dimA) == length(dimB) && all(szA(dimA) == szB(dimB)), ... + 'sparse:dimerror', 'incompatible contracted dimensions.'); + + uncA = 1:length(szA); uncA(dimA) = []; + uncB = 1:length(szB); uncB(dimB) = []; + + if isempty(uncA) + if isempty(uncB) + szC = [1 1]; + elseif length(uncB) == 1 + szC = [1 szB(uncB)]; + else + szC = szB(uncB); + end + elseif isempty(uncB) + if length(uncA) == 1 + szC = [szA(uncA) 1]; + else + szC = szA(uncA); + end + else + szC = [szA(uncA) szB(uncB)]; + end + + A = reshape(permute(A, [uncA dimA]), [prod(szA(uncA)), prod(szA(dimA))]); + B = reshape(permute(B, [flip(dimB) uncB]), [prod(szB(dimB)), prod(szB(uncB))]); + + if isempty(uncA) && isempty(uncB) + C = 0; + if nnz(A) > 0 && nnz(B) > 0 + for i = 1:size(A, 1) + for j = 1:size(B, 2) + for k = 1:size(A, 2) + Aind = all(A.ind == [i k], 2); + if ~any(Aind), continue; end + Bind = all(B.ind == [k j], 2); + if ~any(Bind), continue; end + + C = C + ... + tensorprod(A.var(Aind), B.var(Bind), dimA, dimB, ... + 'NumDimensionsA', options.NumDimensionsA); + end + end + end + end + else + Cvar = A.var.empty(0, 1); + Cind = double.empty(0, length(uncA) + length(uncB)); + + if nnz(A) > 0 && nnz(B) > 0 + for i = 1:size(A, 1) + for j = 1:size(B, 2) + for k = 1:size(A, 2) + Aind = all(A.ind == [i k], 2); + if ~any(Aind), continue; end + Bind = all(B.ind == [k j], 2); + if ~any(Bind), continue; end + if ~isempty(Cind) && all(Cind(end,:) == [i j], 2) + Cvar(end) = Cvar(end) + ... + tensorprod(A.var(Aind), B.var(Bind), dimA, dimB, ... + 'NumDimensionsA', options.NumDimensionsA); + else + Cvar(end+1) = ... + tensorprod(A.var(Aind), B.var(Bind), dimA, dimB, ... + 'NumDimensionsA', options.NumDimensionsA); + Cind = [Cind; [i j]]; + end + end + end + end + end + + Ccod = space(A, uncA); + Cdom = space(B, uncB)'; + + C = SparseTensor(Cind, Cvar, [size(A, 1) size(B, 2)], Ccod, Cdom); + C = reshape(C, szC); + if size(Cind, 1) == prod(szC), C = full(C); end + end + end + + function t = times(t1, t2) + if isscalar(t1) && ~isscalar(t2) + t1 = repmat(t1, size(t2)); + elseif isscalar(t2) && ~isscalar(t1) + t2 = repmat(t2, size(t1)); + end + + nd = max(ndims(t1), ndims(t2)); + assert(isequal(size(t1, 1:nd), size(t2, 1:nd))); + + if isnumeric(t1) + t = t2; + idx = sub2ind_(t.sz, t.ind); + t1 = t1(idx); + idx2 = find(t1); + if ~isempty(idx2) + t.var = t.var(idx2) .* reshape(full(t1(idx2)), [],1); + t.ind = t.ind(idx2, :); + else + t.var = t.var(idx2); + t.ind = t.ind(idx2, :); + end + return + end + + if isnumeric(t2) + t = t2 .* t1; + return + end + + if ~issparse(t1) + idx = sub2ind_(t2.sz, t2.ind); + t = t2; + t.var = t.var .* t1(idx); + return + end + + if ~issparse(t2) + t = t2 .* t1; + return + end + + t = t1; + [t.ind, ia, ib] = intersect(t1.ind, t2.ind, 'rows'); + t.var = t1.var(ia) .* t2.var(ib); + end + + function t = tpermute(t, p, r) + arguments + t + p = [] + r = [] + end + + if isempty(p), p = 1:nspaces(t); end + if isempty(r), r = rank(t); end + + for i = 1:numel(t.var) + t.var(i) = tpermute(t.var(i), p, r); + end + t = permute(t, p); + sp = space(t, p); + t.codomain = sp(1:r(1)); + t.domain = sp(r(1) + (1:r(2)))'; + end + + function t = repartition(t, r) + arguments + t + r (1,2) = [nspaces(t) 0] + end + + if nnz(t) > 0 + t.var = arrayfun(@(x) repartition(x, r), t.var); + end + + sp = space(t); + t.codomain = sp(1:r(1)); + t.domain = sp(r(1) + (1:r(2)))'; + end + + function t = twist(t, i, inv) + arguments + t + i + inv = false + end + if nnz(t) > 0 + t.var = twist(t.var, i, inv); + end + end + + function t = twistdual(t, i, inv) + arguments + t + i + inv = false + end + if nnz(t) > 0 + t.var = twistdual(t.var, i, inv); + end + end + + function a = uminus(a) + if ~isempty(a.var), a.var = -a.var; end + end + + function a = uplus(a) + end + end + + methods + function bool = ismatrix(a) + bool = ndims(a) == 2; + end + + function bool = istriu(a) + assert(ismatrix(a), 'sparse:matrix', 'istriu is only defined for matrices'); + inds = a.ind; vars = a.var; + for i = 1:size(inds, 1) + if inds(i, 1) > inds(i, 2) && norm(vars(i)) > 1e-10 + bool = false; + return + end + end + bool = true; + end + end + + + %% Indexing + methods + function t = cat(dim, t, varargin) + for i = 1:length(varargin) + t2 = sparse(varargin{i}); + N = max(ndims(t), ndims(t2)); + dimcheck = 1:N; + dimcheck(dim) = []; + assert(isequal(size(t, dimcheck), size(t2, dimcheck)), ... + 'sparse:dimagree', 'incompatible sizes for concatenation.'); + newinds = t2.ind; + newinds(:, dim) = newinds(:, dim) + size(t, dim); + t.var = vertcat(t.var, t2.var); + t.ind = vertcat(t.ind, newinds); + t.sz(dim) = t.sz(dim) + size(t2, dim); + end + end + + function i = end(t, k, n) + if n == 1 + i = prod(t.sz); + return + end + + assert(n == length(t.sz), 'sparse:index', 'invalid amount of indices.') + i = t.sz(k); + end + + function [I, J, V] = find(t, k, which) + arguments + t + k = [] + which = 'first' + end + + if isempty(t.ind) + I = []; + J = []; + V = []; + return + end + + [inds, p] = sortrows(t.ind, width(t.ind):-1:1); + + if ~isempty(k) + if strcmp(which, 'first') + inds = inds(1:k, :); + p = p(1:k); + else + inds = inds(end:-1:end-k+1, :); + p = p(end:-1:end-k+1); + end + end + + if nargout < 2 + I = sub2ind_(t.sz, inds); + return + end + + subs = sub2sub([t.sz(1) prod(t.sz(2:end))], t.sz, inds); + I = subs(:,1); + J = subs(:,2); + + if nargout > 2 + V = t.var(p); + end + end + + function t = horzcat(varargin) + t = cat(2, varargin{:}); + end + + function t = permute(t, p) + if ~isempty(t.ind) + t.ind = t.ind(:, p); + end + t.sz = t.sz(p); + end + + function t = reshape(t, varargin) + if nargin == 1 + sz = varargin{1}; + else + hasempty = find(cellfun(@isempty, varargin)); + if isempty(hasempty) + sz = [varargin{:}]; + elseif isscalar(hasempty) + varargin{hasempty} = 1; + sz = [varargin{:}]; + sz(hasempty) = round(numel(t) / prod(sz)); + else + error('Can only accept a single empty size index.'); + end + end + assert(prod(sz) == prod(t.sz), ... + 'sparse:argerror', 'To reshape the number of elements must not change.'); + idx = sub2ind_(t.sz, t.ind); + t.ind = ind2sub_(sz, idx); + t.sz = sz; + end + + function t = sortinds(t) + % Sort the non-zero entries by their index. + % + % Arguments + % --------- + % t : :class:`SparseTensor` + % + % Returns + % ------- + % t : :class:`SparseTensor` + % tensor with sorted elements. + + if isempty(t), return; end + [t.ind, p] = sortrows(t.ind, width(t.ind):-1:1); + t.var = t.var(p); + end + + function varargout = subsref(t, s) + switch s(1).type + case '()' + n = size(s(1).subs, 2); + if n == 1 % linear indexing + I = ind2sub_(t.sz, s(1).subs{1}); + s(1).subs = arrayfun(@(x) I(:,x), 1:width(I), 'UniformOutput',false); + else + assert(n == size(t.sz, 2), 'sparse:index', ... + 'number of indexing indices must match tensor size.'); + end + f = true(size(t.ind, 1), 1); + newsz = zeros(1, size(s(1).subs, 2)); + + for i = 1:size(s(1).subs, 2) + A = s(1).subs{i}; + if strcmp(A, ':') + newsz(i) = t.size(i); + continue; + end + nA = length(A); + if nA ~= length(unique(A)) + error("Repeated index in position %i",i); + end + if i > length(t.codomain) + t.domain(end-(i-length(t.codomain))+1) = ... + SumSpace(subspaces(t.domain(end-(i-length(t.codomain))+1), A)); + else + t.codomain(i) = SumSpace(subspaces(t.codomain(i), A)); + end + if ~isempty(t.ind) + B = t.ind(:, i); + P = false(max(max(A), max(B)) + 1, 1); + P(A + 1) = true; + f = and(f, P(B + 1)); + [~, ~, temp] = unique([A(:); t.ind(f, i)], 'stable'); + t.ind(f, i) = temp(nA+1:end); + end + newsz(i) = nA; + end + t.sz = newsz; + if ~isempty(t.ind) + t.ind = t.ind(f, :); + t.var = t.var(f); + end + if length(s) > 1 + assert(isscalar(t)) + t = subsref(t.var, s(2:end)); + end + varargout{1} = t; + case '.' + [varargout{1:nargout}] = builtin('subsref', t, s); + otherwise + error('sparse:index', '{} indexing not defined'); + end + end + + function n = numArgumentsFromSubscript(~, ~, ~) + n = 1; + end + + function t = subsasgn(t, s, v) + assert(strcmp(s(1).type, '()'), 'sparse:index', 'only () indexing allowed'); + + % Todo: check spaces when assigning + + if length(s(1).subs) == 1 + I = ind2sub_(t.sz, s(1).subs{1}); + s(1).subs = arrayfun(@(x) I(:,x), 1:width(I), 'UniformOutput',false); + end + + assert(length(s(1).subs) == size(t.sz, 2), 'sparse:index', ... + 'number of indexing indices must match tensor size.'); + assert(all(t.sz >= cellfun(@max, s(1).subs)), 'sparse:bounds', ... + 'out of bounds assignment disallowed'); + + subsize = zeros(1, size(s(1).subs, 2)); + for i = 1:length(subsize) + if strcmp(s(1).subs{i}, ':') + s(1).subs{i} = 1:t.sz(i); + elseif islogical(s(1).subs{i}) + s(1).subs{i} = find(s(1).subs{i}).'; + end + subsize(i) = length(s(1).subs{i}); + end + if nnz(v) == 0, return; end + if isscalar(v) && prod(subsize) > 1, v = repmat(v, subsize); end + subs = combvec(s(1).subs{:}).'; + + if isempty(t.ind) + t.ind = subs; + t.var = v(:); + else + for i = 1:size(subs, 1) + idx = find(all(t.ind == subs(i, :), 2)); + if isempty(idx) + t.ind = [t.ind; subs]; + t.var = [t.var; full(v(i))]; + else + t.var(idx) = full(v(i)); + end + end + end + t.sz = max(t.sz, cellfun(@max, s(1).subs)); + end + + function t = vertcat(varargin) + t = cat(1, varargin{:}); + end + end + + + +end diff --git a/src/sparse/ind2sub_.m b/src/sparse/ind2sub_.m new file mode 100644 index 0000000..40e2fdd --- /dev/null +++ b/src/sparse/ind2sub_.m @@ -0,0 +1,36 @@ +function I = ind2sub_(sz, ind, perm) +% Faster implementation of builtin ind2sub + +if isempty(ind) + I = []; + return; +elseif size(sz, 2) == 1 + I = ind; + return; +end + +if nargin < 3 + perm = 1:numel(sz); +else + perm(perm) = 1:numel(sz); +end + +nout = numel(sz); +I = zeros(numel(ind), numel(sz)); + +if nout > 2 + k = cumprod(sz); + for i = nout:-1:3 + I(:, perm(i)) = floor((ind-1) / k(i-1)) + 1; + ind = rem(ind-1, k(i-1)) + 1; + end +end + +if nout >= 2 + I(:, perm(2)) = floor((ind-1)/sz(1)) + 1; + I(:, perm(1)) = rem(ind-1, sz(1)) + 1; +else + I(:, perm(1)) = ind; +end + +end \ No newline at end of file diff --git a/src/sparse/spblkdiag.m b/src/sparse/spblkdiag.m new file mode 100644 index 0000000..05cebb4 --- /dev/null +++ b/src/sparse/spblkdiag.m @@ -0,0 +1,81 @@ +function y = spblkdiag(varargin) +%SPBLKDIAG Sparse block diagonal concatenation of matrix input arguments. +% +% |A 0 .. 0| +% Y = SPBLKDIAG(A,B,...) produces |0 B .. 0| +% |0 0 .. | + +if nargin == 0 + y = []; +else + checkAllAreMatrices(varargin); % Inputs must be all 2-dimensional + X = varargin{1}; + clsX = class(X); + if isobject(X) || ~isnumeric(X) || ~hasSameClass(varargin) + y = X; + if ~strcmp(clsX, 'logical') %#ok + clsX = 'double'; + end + for k = 2:nargin + x = varargin{k}; + [p1,m1] = size(y); + [p2,m2] = size(x); + y = [y zeros(p1,m2,clsX); zeros(p2,m1,clsX) x]; %#ok + end + else + if hasSparse(varargin) + y = matlab.internal.math.blkdiag(varargin{:}); % for sparse double + else + nz = sum(cellfun(@numel, varargin)); + [p, q] = getIndexVectors(varargin); + y = spalloc(p(end), q(end), nz); %Preallocate + for k = 1:nargin + y(p(k)+1:p(k+1),q(k)+1:q(k+1)) = varargin{k}; + end + end + end +end + +% Helper functions + +% check if cellX contains all matrices. +function checkAllAreMatrices(cellX) +for i = 1:numel(cellX) + if ~ismatrix(cellX{i}) + throwAsCaller(MException(message('MATLAB:blkdiag:inputMustBe2D'))); + end +end + +% check if cellX contains all matrices of same class classA. +function tf = hasSameClass(cellX) +clsA = class(cellX{1}); +for i = 2:numel(cellX) + if ~strcmp(clsA, class(cellX{i})) + tf = false; + return + end +end +tf = true; + +% check if cellX contains all sparse matrices. +function tf = hasSparse(cellX) +for i = 1:numel(cellX) + if issparse(cellX{i}) + tf = true; + return + end +end +tf = false; + +% compute the index vector from each matrix. +function [p, q] = getIndexVectors(cellX) +numEl = numel(cellX); +p = zeros(1, numEl+1); +q = zeros(1, numEl+1); +for i = 1:numEl + x = cellX{i}; + p(i+1) = size(x,1); + q(i+1) = size(x,2); +end +p = cumsum(p); +q = cumsum(q); diff --git a/src/sparse/sub2ind_.m b/src/sparse/sub2ind_.m new file mode 100644 index 0000000..eb66711 --- /dev/null +++ b/src/sparse/sub2ind_.m @@ -0,0 +1,28 @@ +function ind = sub2ind_(sz, I) +%sub2ind_ faster implementation of builtin sub2ind. + +if isempty(I) + ind = []; + return; +elseif size(I,2) == 1 + ind = I; + return; +end + +numOfIndInput = size(I,2); + +ind = I(:,1); +if numOfIndInput >= 2 + %Compute linear indices + ind = ind + (I(:,2) - 1).*sz(1); +end + +if numOfIndInput > 2 + %Compute linear indices + k = cumprod(sz); + for i = 3:numOfIndInput + ind = ind + (I(:,i)-1)*k(i-1); + end +end + +end \ No newline at end of file diff --git a/src/sparse/sub2sub.m b/src/sparse/sub2sub.m new file mode 100644 index 0000000..a1d0a2d --- /dev/null +++ b/src/sparse/sub2sub.m @@ -0,0 +1,34 @@ +function sub2 = sub2sub(sz2, sz1, sub1) +%SUB2SUB Convert subscripts to subscripts corresponding to different array +%dimensions. + +if isempty(sub1) || isequal(sz1, sz2) + sub2 = sub1; + return; +end +sub2 = zeros(size(sub1, 1), numel(sz2)); % preallocate new subs +nto1 = sum(cumsum(flip(sz1-1), 2) == 0, 2); % extract number of trailing ones in both sizes +nto2 = sum(cumsum(flip(sz2-1), 2) == 0, 2); +pos1_prev = 0; pos2_prev = 0; +flag = true; +while flag + [pos1, pos2] = find(cumprod(sz1(pos1_prev+1:end)).' == cumprod(sz2(pos2_prev+1:end)), 1); + pos1 = pos1 + pos1_prev; + pos2 = pos2 + pos2_prev; + if prod(sz1(pos1_prev+1:pos1)) > 2^48-1 + error('Cannot map subscripts to new size as intermediate index exceeds MAXSIZE') + end + sub2(:, pos2_prev+1:pos2) = ind2sub_(sz2(pos2_prev+1:pos2), sub2ind_(sz1(pos1_prev+1:pos1), sub1(:, pos1_prev+1:pos1))); + if (isempty(pos2) && numel(sz2) - nto2 == 0) || ... + (isempty(pos1) && numel(sz1) - nto1 == 0) || ... + pos2 == numel(sz2) - nto2 || ... + pos1 == numel(sz1) - nto1 + flag = false; + else + pos1_prev = pos1; + pos2_prev = pos2; + end +end +sub2(:, end-nto2+1:end) = 1; + +end diff --git a/src/tensors/AbstractTensor.m b/src/tensors/AbstractTensor.m new file mode 100644 index 0000000..8929afc --- /dev/null +++ b/src/tensors/AbstractTensor.m @@ -0,0 +1,540 @@ +classdef AbstractTensor + % Abstract base class for representing tensors. + % + % See Also + % -------- + % :class:`.Tensor`, :class:`.SparseTensor`, :class:`.MpsTensor` and + % :class:`.MpoTensor` + + methods + function varargout = linsolve(A, b, x0, M1, M2, options) + % Find a solution for a linear system :code:`A(x) = b` or :code:`A * x = b`. + % + % Arguments + % --------- + % A : :class:`.AbstractTensor` or :class:`function_handle` + % either a function handle implementing or an object that supports + % right multiplication. + % + % b : :class:`.AbstractTensor` + % right-hand side of the equation, interpreted as vector. + % + % x0 : :class:`.AbstractTensor` + % optional initial guess for the solution. + % + % M1, M2 : :class:`.AbstractTensor` or :class:`function_handle` + % preconditioner :code:`M = M1` or :code:`M = M1 * M2` to effectively solve + % the system :code:`A * inv(M) * y = b` with :code:`y = M * x`. + % :code:`M` is either a function handle implementing or an object that + % supports left division. + % + % Keyword Arguments + % ----------------- + % Tol : :class:`numeric` + % specifies the tolerance of the method, by default this is the square root of + % eps. + % + % Algorithm : :class:`char` + % specifies the algorithm used. Can be either one of the following: + % + % - 'bicgstab' + % - 'bicgstabl' + % - 'gmres' + % - 'pcg' + % + % MaxIter : :class:`int` + % Maximum number of iterations. + % + % Restart : :class:`int` + % For 'gmres', amount of iterations after which to restart. + % + % Verbosity : :class:`int` + % Level of output information, by default nothing is printed if `flag` is + % returned, otherwise only warnings are given. + % + % - 0 : no information + % - 1 : information at failure + % - 2 : information at convergence + % + % Returns + % ------- + % x : :class:`.AbstractTensor` + % solution vector. + % + % flag : :class:`int` + % a convergence flag: + % + % - 0 : linsolve converged to the desired tolerance. + % - 1 : linsolve reached the maximum iterations without convergence. + % - 2 : linsolve preconditioner was ill-conditioned. + % - 3 : linsolve stagnated. + % - 4 : one of the scalar quantities calculated became too large or too small. + % + % relres : :class:`numeric` + % relative residual, norm(b - A * x) / norm(b). + % + % iter : :class:`int` + % iteration number at which x was computed. + % + % resvec : :class:`numeric` + % vector of estimated residual norms at each part of the iteration. + + arguments + A + b + x0 = [] + M1 = [] + M2 = [] + + options.Tol = eps(underlyingType(b)) ^ (3/4) + options.Algorithm {mustBeMember(options.Algorithm, ... + {'pcg', 'gmres', 'bicgstab', 'bicgstabl'})} = 'gmres' + options.MaxIter = 400 + options.Restart = 30 + options.Verbosity = 0 + end + + if issparse(b), b = full(b); end + + % Convert input objects to vectors + b_vec = vectorize(b); + b_sz = size(b_vec); + + if ~isempty(x0) && ~issparse(x0) + x0_vec = vectorize(x0); + else + x0_vec = zeros(b_sz); + end + + % Convert input operators to handle vectors + if isa(A, 'function_handle') + A_fun = @(x) vectorize(A(devectorize(x, b))); + else + A_fun = @(x) vectorize(A * devectorize(x, b)); + end + + if isempty(M1) + M1_fun = []; + elseif isa(M1, 'function_handle') + M1_fun = @(x) vectorize(M1(devectorize(x, b))); + else + M1_fun = @(x) vectorize(M1 \ devectorize(x, b)); + end + + if isempty(M2) + M2_fun = []; + elseif isa(M2, 'function_handle') + M2_fun = @(x) vectorize(M2(devectorize(x, b))); + else + M2_fun = @(x) vectorize(M2 \ devectorize(x, b)); + end + + % Sanity check on parameters + options.Restart = min(options.Restart, b_sz(1)); + if options.Tol < eps(underlyingType(b))^0.9 + warning('Requested tolerance might be too strict.'); + end + + % Apply MATLAB implementation + switch options.Algorithm + case 'bicgstab' + [varargout{1:nargout}] = bicgstab(A_fun, b_vec, ... + options.Tol, options.MaxIter, M1_fun, M2_fun, x0_vec); + case 'bicgstabl' + [varargout{1:nargout}] = bicgstabl(A_fun, b_vec, ... + options.Tol, options.MaxIter, M1_fun, M2_fun, x0_vec); + case 'gmres' + options.MaxIter = min(b_sz(1), options.MaxIter); + [varargout{1:nargout}] = gmres(A_fun, b_vec, ... + options.Restart, options.Tol, options.MaxIter, ... + M1_fun, M2_fun, x0_vec); + case 'pcg' + [varargout{1:nargout}] = pcg(A_fun, b_vec, ... + options.Tol, options.MaxIter, M1_fun, M2_fun, x0_vec); + end + + + % Convert output + varargout{1} = devectorize(varargout{1}, b); + end + + function varargout = eigsolve(A, x0, howmany, sigma, options) + % Find a few eigenvalues and eigenvectors of an operator. + % + % Usage + % ----- + % :code:`[V, D, flag] = eigsolve(A, x0, howmany, sigma, kwargs)` + % + % :code:`D = eigsolve(A, x0, ...)` + % + % Arguments + % --------- + % A : :class:`.AbstractTensor` or :class:`function_handle` + % A square tensormap interpreted as matrix. + % A function handle which implements one of the following, depending on sigma: + % + % - :code:`A \ x`, if `sigma` is 0 or 'smallestabs' + % - :code:`(A - sigma * I) \ x`, if sigma is a nonzero scalar + % - :code:`A * x`, for all other cases + % + % x0 : :class:`.AbstractTensor` + % initial guess for the eigenvector. If A is a :class:`.Tensor`, this defaults + % to a random complex :class:`.Tensor`, for function handles this is a required + % argument. + % + % howmany : :class:`int` + % amount of eigenvalues and eigenvectors that should be computed. By default + % this is 1, and this should not be larger than the total dimension of A. + % + % sigma : :class:`char` or :class:`numeric` + % selector for the eigenvalues, should be either one of the following: + % + % - 'largestabs', 'lm': default, eigenvalues of largest magnitude + % - 'largestreal', 'lr': eigenvalues with largest real part + % - 'largestimag', 'li': eigenvalues with largest imaginary part. + % - 'smallestabs', 'sm': default, eigenvalues of smallest magnitude + % - 'smallestreal', 'sr': eigenvalues with smallest real part + % - 'smallestimag', 'si': eigenvalues with smallest imaginary part. + % - 'bothendsreal', 'be': both ends, with howmany/2 values with largest and + % smallest real part respectively. + % - 'bothendsimag', 'li': both ends, with howmany/2 values with largest and + % smallest imaginary part respectively. + % - numeric : eigenvalues closest to sigma. + % + % Keyword Arguments + % ----------------- + % Tol : :class:`numeric` + % tolerance of the algorithm. + % + % Algorithm : :class:`char` + % choice of eigensolver algorithm. Currently there is a choice between the use + % of Matlab's buitin `eigs` specified by the identifiers 'eigs' or + % 'KrylovSchur', or the use of a custom Arnoldi algorithm specified by + % the identifier 'Arnoldi'. + % + % MaxIter : :class:`int` + % maximum number of iterations, 100 by default. + % + % KrylovDim : :class:`int` + % number of vectors kept in the Krylov subspace. + % + % IsSymmetric : :class:`logical` + % flag to speed up the algorithm if the operator is symmetric, false by + % default. + % + % Verbosity : :class:`int` + % Level of output information, by default nothing is printed if :code:`flag` + % is returned, otherwise only warnings are given. + % + % - 0 : no information + % - 1 : information at failure + % - 2 : information at convergence + % - 3 : information at every iteration + % + % Returns + % ------- + % V : (1, :code:`howmany`) :class:`.AbstractTensor` + % vector of eigenvectors. + % + % D : :class:`numeric` + % vector of eigenvalues if only a single output argument is asked, diagonal + % matrix of eigenvalues otherwise. + % + % flag : :class:`int` + % if :code:`flag = 0` then all eigenvalues are converged, otherwise not. + + arguments + A + x0 = A.randnc(A.domain, []) + howmany = 1 + sigma = 'lm' + + options.Algorithm {mustBeMember(options.Algorithm, ... + {'eigs', 'KrylovSchur', 'Arnoldi'})} = 'Arnoldi' + + options.Tol = eps(underlyingType(x0))^(3/4) + options.MaxIter = 100 + options.KrylovDim = 20 + options.ReOrth = 2 + options.NoBuild = 3 + options.Verbosity = Verbosity.warn + options.IsSymmetric logical = false + end + + switch options.Algorithm + + case {'Arnoldi'} + alg_opts = rmfield(options, {'Algorithm', 'IsSymmetric'}); + kwargs = namedargs2cell(alg_opts); + alg = Arnoldi(kwargs{:}); + [varargout{1:nargout}] = eigsolve(alg, A, x0, howmany, sigma); + + case {'eigs', 'KrylovSchur'} + alg_opts = rmfield(options, ... + {'Algorithm', 'DeflateDim', 'ReOrth', 'NoBuild', 'IsSymmetric'}); + kwargs = namedargs2cell(alg_opts); + alg = KrylovSchur(kwargs{:}); + [varargout{1:nargout}] = eigsolve(alg, A, x0, howmany, sigma, ... + 'IsSymmetric', options.IsSymmetric); + + end + end + + function C = contract(tensors, indices, kwargs) + arguments (Repeating) + tensors + indices (1, :) {mustBeInteger} + end + + arguments + kwargs.Conj (1, :) logical = false(size(tensors)) + kwargs.Rank = [] + kwargs.Debug = false + kwargs.CheckOptimal = false + end + + assert(length(kwargs.Conj) == length(tensors)); + + if kwargs.CheckOptimal + legcosts = zeros(2, 0); + for i = 1:length(indices) + legcosts = [legcosts [indices{i}; dims(tensors{i})]]; + end + legcosts = unique(legcosts.', 'rows'); + + currentcost = contractcost(indices, legcosts); + [sequence, cost] = netcon(indices, 0, 1, currentcost, 1, legcosts); + + if cost < currentcost + warning('suboptimal contraction order.\n current (%d): %s\n optimal(%d): %s', ... + currentcost, num2str(1:max(legcosts(:,1))), ... + cost, num2str(sequence)); + end + end + + for i = 1:length(tensors) + [i1, i2] = traceinds(indices{i}); + tensors{i} = tensortrace(tensors{i}, i1, i2); + indices{i}([i1 i2]) = []; + end + + debug = kwargs.Debug; + + % Special case for single input tensor + if nargin == 2 + C = tensors{1}; + + if isempty(indices{1}) + assert(isnumeric(C)); + if kwargs.Conj + C = C'; + end + return + end + + [~, order] = sort(indices{1}, 'descend'); + + if kwargs.Conj + C = tpermute(C', order(length(order):-1:1), kwargs.Rank); + else + C = tpermute(C, order, kwargs.Rank); + end + return + end + + % Generate trees + contractindices = cellfun(@(x) x(x > 0), indices, 'UniformOutput', false); + partialtrees = num2cell(1:length(tensors)); + tree = generatetree(partialtrees, contractindices); + + % contract all subtrees + [A, ia, ca] = contracttree(tensors, indices, kwargs.Conj, tree{1}, debug); + [B, ib, cb] = contracttree(tensors, indices, kwargs.Conj, tree{2}, debug); + + % contract last pair + [dimA, dimB] = contractinds(ia, ib); + + if debug, contractcheck(A, ia, ca, B, ib, cb); end + + C = tensorprod(A, B, dimA, dimB, ca, cb, 'NumDimensionsA', length(ia)); + ia(dimA) = []; ib(dimB) = []; + ic = [ia ib]; + + % permute last tensor + if ~isempty(ic) && length(ic) > 1 + [~, order] = sort(ic, 'descend'); + if isempty(kwargs.Rank) + kwargs.Rank = [length(order) 0]; + end + C = tpermute(C, order, kwargs.Rank); + end + end + + function disp(t, details) + if nargin == 1 || isempty(details), details = false; end + if isscalar(t) + r = t.rank; + fprintf('Rank (%d, %d) %s:\n', r(1), r(2), class(t)); + s = space(t); + for i = 1:length(s) + fprintf('\t%d.\t', i); + disp(s(i)); + fprintf('\b'); + end + fprintf('\n'); + if details + [blocks, charges] = matrixblocks(t); + for i = 1:length(blocks) + if ~isempty(blocks) + fprintf('charge %s:\n', string(charges(i))); + end + disp(blocks{i}); + end + end + else + fprintf('%s of size %s:\n', class(t), ... + regexprep(mat2str(size(t)), {'\[', '\]', '\s+'}, {'', '', 'x'})); + subs = ind2sub_(size(t), 1:numel(t)); + spc = floor(log10(max(double(subs), [], 1))) + 1; + if numel(spc) == 1 + fmt = strcat("\t(%", num2str(spc(1)), "u)"); + else + fmt = strcat("\t(%", num2str(spc(1)), "u,"); + for i = 2:numel(spc) - 1 + fmt = strcat(fmt, "%", num2str(spc(i)), "u,"); + end + fmt = strcat(fmt, "%", num2str(spc(end)), "u)"); + end + for i = 1:numel(t) + fprintf('%s\t\t', compose(fmt, subs(i, :))); + disp(t(i), details); + end + end + end + + function d = distance(A, B) + % Compute the Euclidean distance between two tensors. + % + % Arguments + % --------- + % A, B : :class:`.AbstractTensor` + % + % Returns + % ------- + % d : :class:`numeric` + % Euclidean distance, defined as the norm of the distance. + + n = max(ndims(A), ndims(B)); + assert(isequal(size(A, 1:n), size(B, 1:n)) || isscalar(A) || isscalar(B), ... + 'tensors:SizeError', 'Incompatible sizes for vectorized function.'); + + % make everything a vector + A = repartition(A); + B = repartition(B); + + d = norm(A - B); + end + + function local_operators = decompose_local_operator(H, kwargs) + % convert a tensor into a product of local operators. + % + % Usage + % ----- + % :code:`local_operators = decompose_local_operator(H, kwargs)`. + % + % Arguments + % --------- + % H : :class:`.AbstractTensor` + % tensor representing a local operator on N sites. + % + % Keyword Arguments + % ----------------- + % 'Trunc' : :class:`cell` + % optional truncation method for the decomposition. See also + % :meth:`.Tensor.tsvd` + arguments + H + kwargs.Trunc = {'TruncBelow', 1e-12} + end + + assert(mod(nspaces(H), 2) == 0, ... + 'InfJMpo:Argerror', 'local operator must have an even amount of legs.'); + H = repartition(H, nspaces(H) ./ [2 2]); + assert(isequal(H.domain, H.codomain), ... + 'InfJMpo:ArgError', 'local operator must be square.'); + + N = indin(H); + local_operators = cell(1, N); + if N == 1 + local_operators{1} = insert_onespace(insert_onespace(H, 1), 3, true); + else + [u, s, v] = tsvd(H, [1 2*N], 2:(2*N-1), kwargs.Trunc{:}); + local_operators{1} = insert_onespace(tpermute(u * s, [1 3 2], [1 2]), 1); + + for i = 2:N-1 + [u, s, v] = tsvd(v, [1 2 nspaces(v)], 3:(nspaces(v) - 1), ... + kwargs.Trunc{:}); + local_operators{i} = tpermute(u * s, [1 2 4 3], [2 2]); + end + + local_operators{N} = insert_onespace(repartition(v, [2 1]), 3, true); + end + end + + function B = tensortrace(A, i1, i2) + if isempty(i1) && isempty(i2), B = A; return; end + assert(length(i1) == length(i2), 'invalid indices'); + + firstspaces = conj(space(A, i1)); + secondspaces = space(A, i2); + assert(isequal(firstspaces, secondspaces), 'Tensor:SpaceMismatch', ... + 'Cannot trace spaces %s and %s', string(firstspaces), string(secondspaces)); + + E = A.eye(firstspaces, secondspaces); + E = twistdual(E, 1:length(firstspaces)); + + iA = [i1 i2]; + iE = [1:length(i1) length(i1) + (length(i2):-1:1)]; + B = tensorprod(A, E, iA, iE); + end + + function sz = dims(t, inds) + sz = dims(space(t)); + if nargin > 1 + sz = sz(inds); + end + end + + function o = overlap(t1, t2) + o = contract(t1, 1:nspaces(t1), t2, flip(1:nspaces(t1))); + end + end + + + %% Contractions + methods + function v = applytransfer(L, R, v) + arguments + L + R + v = [] + end + + if isempty(v) + v = tracetransfer(L, R); + return + end + + auxlegs_v = nspaces(v) - 2; + auxlegs_l = 0; + auxlegs_r = 0; + newrank = rank(v); newrank(2) = newrank(2) + auxlegs_l + auxlegs_r; + + v = contract(v, [1 3 (-(1:auxlegs_v) - 2 - auxlegs_l)], ... + L, [-1 2 1 (-(1:auxlegs_l) - 2)], ... + R, [3 2 -2 (-(1:auxlegs_r) - 3 - auxlegs_l - auxlegs_v)], ... + 'Rank', newrank); + end + + end +end diff --git a/src/tensors/FusionTree.m b/src/tensors/FusionTree.m index ba9fb75..6a2943b 100644 --- a/src/tensors/FusionTree.m +++ b/src/tensors/FusionTree.m @@ -3,16 +3,16 @@ % % Properties % ---------- - % charges : :class:`AbstractCharge` + % charges : (:, :) :class:`.AbstractCharge` % labels for the edges of the fusion trees % - % vertices : int + % vertices : (:, :) :class:`int` % labels for the vertices of the fusion trees % - % isdual : logical + % isdual : (1, :) :class:`logical` % indicator of duality transform on the external edges. % - % rank : int + % rank : (1, 2) :class:`int` % amount of splitting tree and fusion tree legs. properties @@ -37,18 +37,18 @@ % % Arguments % --------- - % charges : :class:`AbstractCharge` + % charges : (:, :) :class:`.AbstractCharge` % array of charges, where each row represents an allowed fusion channel. % - % vertices : int = [] + % vertices (:, :) : :class:`.int` % Array of vertex labels. Must be empty or have the same amount of % rows as `charges`. This is optional if the charges have a fusion style % which is multiplicity-free. % - % isdual : logical + % isdual : (1, :) :class:`logical` % mask that shows the presence of duality transformations. % - % rank : int + % rank : (1, 2) :class:`int` % number of splitting and fusing legs. if nargin == 0 @@ -77,8 +77,8 @@ f.vertices = vertices; f.isdual = isdual; f.rank = rank; - -% assert(all(isallowed(f))); % comment out for debugging + + % assert(all(isallowed(f))); % comment out for debugging end end @@ -88,15 +88,15 @@ % % Arguments % --------- - % rank : int + % rank : (1, 2) :class:`int` % number of legs for the splitting and fusion tree. % % Repeating Arguments % ------------------- - % charges : :class:`AbstractCharge` + % charges : (1, :) :class:`.AbstractCharge` % set of charges for a given leg % - % isdual : logical + % isdual : :class:`logical` % presence of a duality transform arguments @@ -238,23 +238,23 @@ % % Arguments % --------- - % f : :class:`FusionTree` - % tree to swap. + % f : :class:`.FusionTree` + % tree to swap % - % i : int - % `i` and `i+1` are the braided strands. + % i : :class:`int` + % :code:`i` and :code:`i+1` are the braided strands % - % inv : logical = false - % flag to indicate whether to perform an overcrossing (false) or an - % undercrossing (true). + % inv : :class:`logical` = :code:`false` + % flag to indicate whether to perform an overcrossing (:code:`false`) or an + % undercrossing (:code:`true`) % % Returns % ------- - % c : sparse double - % matrix of coefficients that transform input to output trees. - % f(i) --> c(i,j) * f(j) + % c : (:, :) :class:`sparse` + % matrix of coefficients that transform input to output trees: + % :code:`f(i) --> c(i,j) * f(j)` % - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % braided trees in canonical form. if nargin < 3, inv = false; end @@ -301,6 +301,7 @@ for j = 1:length(ia2) blocks{j} = Rsymbol(abc(j, 1), abc(j, 2), abc(j, 3), inv); end + f.charges = f.charges(order, [2 1 3:end]); f.vertices = f.vertices(order, :); f.isdual(1:2) = f.isdual([2 1]); @@ -343,7 +344,7 @@ f.charges = vertcat(newcharges{:}); f.isdual(i:i + 1) = f.isdual([i + 1 i]); - c = sparse(blkdiag(blocks{ic2})); + c = spblkdiag(blocks{ic2}); % TODO: make sure this doesn't slow down [f, p] = sort(f); c = c(invperm(order), p); return @@ -409,16 +410,16 @@ % % Arguments % --------- - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % tree to bend. % % Returns % ------- - % c : sparse double + % c : (:, :) :class:`sparse` % matrix of coefficients that transform input to output trees. - % f(i) --> c(i,j) * f(j) + % :code:`f(i) --> c(i,j) * f(j)` % - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % bent trees in canonical form. f = flip(f); @@ -433,16 +434,16 @@ % % Arguments % --------- - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % tree to bend. % % Returns % ------- - % c : sparse double + % c : (:, :) :class:`sparse` % matrix of coefficients that transform input to output trees. - % `f(i) --> c(i,j) * f(j)` + % :code:`f(i) --> c(i,j) * f(j)` % - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % bent trees in canonical form. if ~hasmultiplicity(fusionstyle(f)) @@ -543,30 +544,31 @@ function [c, f] = braid(f, p, lvl, rank) % Compute the coefficients that bring a braided tree into canonical form. - % This is done by reducing the braid into a composition of elementary swaps - % on neighbouring strands. + % + % This is done by reducing the braid into a composition of elementary swaps + % on neighbouring strands. % % Arguments % --------- - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % tree to braid. % - % p : int + % p : (:, :) :class:`int` % permutation indices. % - % lvl : int + % lvl : (:, :) :class:`int` % height of the strands, indicating over- and undercrossings. % - % rank : int + % rank : :class:`int` (1, 2) % final number of splitting and fusing legs. % % Returns % ------- - % c : sparse double + % c : (:, :) :class:`sparse` % matrix of coefficients that transform input to output trees. - % f(i) --> c(i,j) * f(j) + % :code:`f(i) --> c(i,j) * f(j)` % - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % braided trees in canonical form. % % Todo @@ -587,20 +589,39 @@ assert(N == f.legs, 'tensors:trees:ArgError', ... 'p has the wrong number of elements.'); + global cache + if isempty(cache), cache = LRU; end + + if Options.CacheEnabled() + key = GetMD5({f, p, lvl, rank}, 'Array', 'hex'); + med = get(cache, key); + + if ~isempty(med) + c = med.c; + f = med.f; + return + end + end + if all(p == 1:f.legs) [c, f] = repartition(f, rank); - return + else + swaps = perm2swap(p); + [c, f] = repartition(f, [f.legs 0]); + for s = swaps + [c_, f] = artinbraid(f, s, lvl(s) > lvl(s + 1)); + c = c * c_; + lvl(s:s + 1) = lvl([s + 1 s]); + end + [c_, f] = repartition(f, rank); + c = c * c_; end - swaps = perm2swap(p); - [c, f] = repartition(f, [f.legs 0]); - for s = swaps - [c_, f] = artinbraid(f, s, lvl(s) > lvl(s + 1)); - c = c * c_; - lvl(s:s + 1) = lvl([s + 1 s]); + if Options.CacheEnabled() + med.c = c; + med.f = f; + cache = set(cache, key, med); end - [c_, f] = repartition(f, rank); - c = c * c_; end function [c, f] = elementary_trace(f, i) @@ -697,19 +718,19 @@ % f : :class:`FusionTree` % tree to permute. % - % p : int + % p : (1, :) :class:`int` % permutation indices. % - % r : int + % r : (1, 2) :class:`int` % final number of splitting and fusing legs. % % Returns % ------- - % c : sparse double + % c : (:, :) :class:`sparse` % matrix of coefficients that transform input to output trees. - % `f(i) --> c(i,j) * f(j)` + % :code:`f(i) --> c(i,j) * f(j)` % - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % permuted trees in canonical form. arguments @@ -729,19 +750,19 @@ % % Arguments % --------- - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % tree to repartition. % - % newrank : int + % newrank : (1, 2) :class:`int` % new rank of the fusion tree. % % Returns % ------- - % c : sparse double + % c : (:, :) :class:`sparse` % matrix of coefficients that transform input to output trees. - % `f(i) --> c(i,j) * f(j)` + % :code:`f(i) --> c(i,j) * f(j)` % - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % repartitioned trees in canonical form. arguments @@ -773,6 +794,47 @@ [f, p] = sort(f); c = c(:, p); end + + function [c, f] = twist(f, i, inv) + % Compute the coefficients that twist legs. + % + % Arguments + % --------- + % f : :class:`.FusionTree` + % tree to repartition. + % + % i : :class:`int` or :class:`logical` + % indices of legs to twist. + % + % inv : :class:`logical` + % flag to determine inverse twisting. + % + % Returns + % ------- + % c : (:, :) :class:`sparse` + % matrix of coefficients that transform input to output trees. + % :code:`f(i) --> c(i,j) * f(j)` + % + % f : :class:`.FusionTree` + % twisted trees in canonical form. + + arguments + f + i + inv = false + end + + if isempty(f), c = []; return; end + + if istwistless(braidingstyle(f)) || isempty(i) || ~any(i) + c = speye(length(f)); + return + end + + theta = prod(twist(f.uncoupled(:, i)), 2); + if inv, theta = conj(theta); end + c = spdiags(theta, 0, length(f), length(f)); + end end @@ -781,6 +843,7 @@ function style = braidingstyle(f) style = f.charges.braidingstyle; end + function bool = isallowed(f) if ~hasmultiplicity(fusionstyle(f)) if f.rank(1) == 0 @@ -841,13 +904,16 @@ end function style = fusionstyle(f) - try style = fusionstyle(f.charges); - catch - bla - end end + function [lia, locb] = ismember(f1, f2) + if hasmultiplicity(fusionstyle(f1)) + error('TBA'); + end + + [lia, locb] = ismember(f1.charges, f2.charges, 'rows'); + end function [f, p] = sort(f) % sort - Sort the fusion trees into canonical order. @@ -865,8 +931,8 @@ 'Col', cols); else cols = [treeindsequence(f.rank(1)) + 1 ... % center charge - (1:treeindsequence(f.rank(2))) + treeindsequence(f.rank(1)) + 1 ... % fuse charges - fliplr(1:treeindsequence(f.rank(1)))]; % split charges + (1:treeindsequence(f.rank(2))) + treeindsequence(f.rank(1)) + 1 ... % fuse charges + fliplr(1:treeindsequence(f.rank(1)))]; % split charges if nargout > 1 [f.charges, p] = sortrows(f.charges, cols); else @@ -874,6 +940,20 @@ end end end + + function bool = issorted(f) + if ~isempty(f.vertices) && hasmultiplicity(fusionstyle(f)) + [~, p] = sort(f); + bool = isequal(p, (1:length(f)).'); + else + bool = issorted(f.coupled); + if ~bool, return; end + cols = [treeindsequence(f.rank(1)) + 1 ... % center charge + (1:treeindsequence(f.rank(2))) + treeindsequence(f.rank(1)) + 1 ... % fuse charges + fliplr(1:treeindsequence(f.rank(1)))]; + bool = issortedrows(f.charges(:, cols)); + end + end end %% Setters and Getters @@ -929,7 +1009,8 @@ function varargout = size(f, varargin) if nargin == 1 if nargout < 2 - varargout{1} = [1, size(f.charges, 1)]; + c = f.charges; + varargout{1} = [1, size(c, 1)]; elseif nargout == 2 varargout = {1, size(f.charges, 1)}; else @@ -1058,11 +1139,11 @@ function displayNonScalarObject(f) % % Arguments % --------- - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % % Returns % ------- - % A : double + % A : :class:`double` % array representation of a fusion tree. assert(issymmetric(braidingstyle(f)), ... @@ -1096,13 +1177,13 @@ function displayNonScalarObject(f) % % Arguments % --------- - % f : :class:`FusionTree` + % f : :class:`.FusionTree` % an array of fusion trees to convert. % % Returns % ------- - % C : cell - % a cell array containing the array representations of f. + % C : :class:`cell` + % a cell array containing the array representations of :code:`f`. if fusionstyle(f) == FusionStyle.Unique C = num2cell(ones(size(f))); @@ -1201,5 +1282,9 @@ function displayNonScalarObject(f) end C = {C}; end + + function s = GetMD5_helper(f) + s = {f.charges, f.vertices, f.isdual, f.rank}; + end end end diff --git a/src/tensors/Tensor.m b/src/tensors/Tensor.m index 038909f..f1a9e5f 100644 --- a/src/tensors/Tensor.m +++ b/src/tensors/Tensor.m @@ -1,6 +1,20 @@ -classdef Tensor - %TENSOR Summary of this class goes here - % Detailed explanation goes here +classdef Tensor < AbstractTensor + % Base implementation of a dense tensor array with optional symmetries. + % + % Properties + % ---------- + % codomain + % codomain vector space represented as tensor product space. + % + % domain + % codomain vector space represented as tensor product space. + % + % var + % block sparse representation of tensor data. + % + % Todo + % ---- + % Document all methods. properties codomain @@ -24,10 +38,10 @@ % % Arguments % --------- - % array : numeric + % array : :class:`numeric` % numeric input array to convert to a :class:`Tensor` % - % codomain, domain : :class:`AbstractSpace` + % codomain, domain : (1, :) :class:`.AbstractSpace` % spaces that define the structure of the output tensor. % % Returns @@ -39,9 +53,17 @@ % t = Tensor(tensor) if nargin == 1 && isa(varargin{1}, 'Tensor') - t.codomain = varargin{1}.codomain; - t.domain = varargin{1}.domain; - t.var = varargin{1}.var; + for i = numel(varargin{1}):-1:1 + t(i).codomain = varargin{1}(i).codomain; + t(i).domain = varargin{1}(i).domain; + t(i).var = varargin{1}(i).var; + end + t = reshape(t, size(varargin{1})); + return + end + + if nargin == 1 && isa(varargin{1}, 'SparseTensor') + t = full(varargin{1}); return end @@ -53,7 +75,7 @@ varargin); return; end - + % t = Tensor(codomain, domain) if nargin == 2 codomain = varargin{1}; @@ -61,10 +83,30 @@ assert(~isempty(codomain) || ~isempty(domain), ... 'Cannot create (0,0) tensors.'); - t.domain = domain; - t.codomain = codomain; - t.var = AbstractBlock.new(codomain, domain); + if isa(codomain, 'SumSpace') || isa(domain, 'SumSpace') + if isempty(codomain) + sz = flip(nsubspaces(domain)); + elseif isempty(domain) + sz = nsubspaces(codomain); + else + if ~isa(codomain, 'SumSpace'), codomain = SumSpace(codomain); end + if ~isa(domain, 'SumSpace'), domain = SumSpace(domain); end + sz = [nsubspaces(codomain) flip(nsubspaces(domain))]; + end + subs = ind2sub_(sz, 1:prod(sz)); + for i = size(subs, 1):-1:1 + [cod, dom] = slice(codomain, domain, subs(i,:)); + t(i).codomain = cod; + t(i).domain = dom; + t(i).var = AbstractBlock.new(cod, dom); + end + else + t.domain = domain; + t.codomain = codomain; + t.var = AbstractBlock.new(codomain, domain); + end + return end @@ -77,26 +119,26 @@ % Usage % ----- % :code:`t = fill_matrix(t, matrices, charges)` - % + % % :code:`t = fill_matrix(t, fun, charges)` % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor to fill into. % - % matrices : cell or numeric + % matrices : :class:`cell` or :class:`numeric` % list of matrices or single matrix to fill with. % % fun : :class:`function_handle` % function of signature :code:`fun(dims, charge)` to fill with. - % - % charges : :class:`AbstractCharge` + % + % charges : :class:`.AbstractCharge` % optional list of charges to identify the matrix blocks. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % filled tensor. arguments @@ -108,52 +150,60 @@ if isnumeric(data), data = {data}; end if iscell(data) - t.var = fill_matrix_data(t.var, data, charges); + if isempty(charges) + t.var = fill_matrix_data(t.var, data); + else + t.var = fill_matrix_data(t.var, data, charges); + end else - t.var = fill_matrix_fun(t.var, data, charges); + if isempty(charges) + t.var = fill_matrix_fun(t.var, data); + else + t.var = fill_matrix_fun(t.var, data, charges); + end end end - function t = fill_tensor(t, data, trees) + function t = fill_tensor(t, data) % Fill the tensor blocks of a tensor. % % Usage % ----- - % :code:`t = fill_tensor(t, tensors, trees)` - % - % :code:`t = fill_tensor(t, fun, trees)` + % :code:`t = fill_tensor(t, tensors)` + % + % :code:`t = fill_tensor(t, fun)` % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor to fill into. % - % tensors : cell or numeric + % tensors : :class:`cell` or :class:`numeric` % list of tensors or single tensor to fill with. % % fun : :class:`function_handle` % function of signature :code:`fun(dims, trees)` to fill with. - % - % trees : :class:`FusionTree` - % optional list of fusion trees to identify the tensor blocks. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % filled tensor. arguments t data - trees = [] end if isnumeric(data), data = {data}; end if iscell(data) - t.var = fill_tensor_data(t.var, data, trees); + t.var = fill_tensor_data(t.var, data); else - t.var = fill_tensor_fun(t.var, data, trees); + [tmp, trees] = tensorblocks(t); + for i = 1:length(tmp) + tmp{i} = data(size(tmp{i}), trees(i)); + end + t.var = fill_tensor_data(t.var, tmp); end end @@ -173,7 +223,7 @@ % % Repeating Arguments % ------------------- - % tensors : :class:`Tensor` + % tensors : :class:`.Tensor` % input tensors used to copy legs. % % indices : int @@ -181,27 +231,27 @@ % % Keyword Arguments % ----------------- - % Rank : (1, 2) int + % Rank : (1, 2) :class:`int´ % rank of the output tensor, by default this is :code:`[nspaces(t) 0]`. % - % Conj : logical + % Conj : :class:`logical` % flag to indicate whether the space should be equal to the input space, or % fit onto the input space. This can be either an array of size(tensors), or a % scalar, in which case it applies to all tensors. % - % Mode : 'tensor' or 'matrix' + % Mode : :class:`char`, 'tensor' or 'matrix' % method of filling the tensor data. By default this is matrix, where the % function should be of signature :code:`fun(dims, charge)`, for 'tensor' this % should be of signature :code:`fun(dims, tree)`. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % output tensor. % % Examples % -------- - % :code:`t = similar([], mpsbar, 1, mpo, 4, mps, 1, 'Conj', true)` creates a + % :code:`t = similar([], mpsbar, 1, mpo, 4, mps, 1, 'Conj', true)` creates a % left mps environment tensor. arguments @@ -265,39 +315,39 @@ % function of signature :code:`fun(dims, id)` where id is determined by Mode. % If this is left empty, the tensor data will be uninitialized. % - % dims : int + % dims : :class:`int` % list of dimensions for non-symmetric tensors. % - % arrows : logical + % arrows : :class:`logical` % optional list of arrows for tensor legs. % - % tensor : :class:`Tensor` + % tensor : :class:`.Tensor` % input tensor to copy structure. % % Repeating Arguments % ------------------- - % charges : cell + % charges : :class:`cell` % list of charges for each tensor index. % - % degeneracies : cell + % degeneracies : :class:`cell` % list of degeneracies for each tensor index. % - % arrow : logical + % arrow : :class:`logical` % arrow for each tensor index. % % Keyword Arguments % ----------------- - % Rank : int (1, 2) - % rank of the constructed tensor. By default this is [nspaces(t) 0]. + % Rank : (1, 2) :class:`int` + % rank of the constructed tensor. By default this is :code:`[nspaces(t) 0]`. % - % Mode : 'matrix' or 'tensor' + % Mode : :class:`char`, 'matrix' or 'tensor' % method of filling the resulting tensor. When this is 'matrix' (default), % the function signature is :code:`fun(dims, charge)`, while for 'tensor' the % signature should be :code:`fun(dims, tree)`. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % output tensor. arguments @@ -310,7 +360,8 @@ arguments kwargs.Rank - kwargs.Mode = 'matrix' + kwargs.Mode {mustBeMember(kwargs.Mode, {'matrix', 'tensor'})} ... + = 'matrix' end % Parse special constructors @@ -319,7 +370,8 @@ spaces = CartesianSpace.new(varargin{1}); elseif isnumeric(varargin{2}) || islogical(varargin{2}) assert(length(varargin{1}) == length(varargin{2})) - spaces = ComplexSpace.new(varargin{1}, varargin{2}); + args = [num2cell(varargin{1}); num2cell(varargin{2})]; + spaces = ComplexSpace.new(args{:}); end if ~isfield(kwargs, 'Rank') @@ -355,38 +407,47 @@ % Fill tensor if ~isempty(fun) - if strcmp(kwargs.Mode, 'matrix') - t = fill_matrix(t, fun); - elseif strcmp(kwargs.Mode, 'tensor') - t = fill_tensor(t, fun); - else - error('tensors:ArgumentError', 'Unknown options for Mode'); + switch kwargs.Mode + case 'matrix' + t = fill_matrix(t, fun); + case 'tensor' + t = fill_tensor(t, fun); end end end - + function t = zeros(varargin) - t = Tensor.new(@(dims, charge) zeros(dims), varargin{:}); + t = Tensor.new(@zeros, varargin{:}); end function t = ones(varargin) - t = Tensor.new(@(dims, charge) ones(dims), varargin{:}); + t = Tensor.new(@ones, varargin{:}); end function t = eye(varargin) - t = Tensor.new(@(dims, charge) eye(dims), varargin{:}); + t = Tensor.new(@eye, varargin{:}); end function t = rand(varargin) - t = Tensor.new(@(dims, charge) rand(dims), varargin{:}); + t = Tensor.new(@rand, varargin{:}); + end + + function t = randn(varargin) + t = Tensor.new(@randn, varargin{:}); end function t = randc(varargin) - t = Tensor.new(@(dims, charge) randc(dims), varargin{:}); + t = Tensor.new(@randc, varargin{:}); end function t = randnc(varargin) - t = Tensor.new(@(dims, charge) randnc(dims), varargin{:}); + t = Tensor.new(@randnc, varargin{:}); + end + end + + methods (Hidden) + function tdst = zerosLike(t, varargin) + tdst = repmat(0 * t, varargin{:}); end end @@ -394,33 +455,31 @@ %% Structure methods function n = indin(t) - n = length(t.domain); + n = length(t(1).domain); end function n = indout(t) - n = length(t.codomain); + n = length(t(1).codomain); end function n = nspaces(t) - n = length(t.domain) + length(t.codomain); + n = length(t(1).domain) + length(t(1).codomain); end function r = rank(t, i) - r = [length(t.codomain) length(t.domain)]; + r = [length(t(1).codomain) length(t(1).domain)]; if nargin > 1 r = r(i); end end - function sz = dims(t, inds) - sz = dims([t.codomain, t.domain']); - if nargin > 1 - sz = sz(inds); - end - end - function sp = space(t, inds) - sp = [t.codomain t.domain']; + if isscalar(t) + sp = [t.codomain t.domain']; + else + [cod, dom] = deduce_spaces(t); + sp = [cod, dom']; + end if nargin > 1 sp = sp(inds); end @@ -430,41 +489,87 @@ f = fusiontrees(t.codomain, t.domain); end - function b = tensorblocks(t) + function [b, f] = tensorblocks(t) b = tensorblocks(t.var); + if nargout > 1, f = fusiontrees(t); end end function varargout = matrixblocks(t) [varargout{1:nargout}] = matrixblocks(t.var); end - end - - - %% Comparison - methods - function d = distance(A, B) - % Compute the Euclidean distance between two tensors. + + function style = braidingstyle(t) + style = braidingstyle(t.codomain, t.domain); + end + + function style = fusionstyle(t) + style = fusionstyle(t.codomain, t.domain); + end + + function t = full(t) + end + + function tdst = insert_onespace(tsrc, i, dual) + % Insert a trivial space at position :code:`i`. % % Arguments % --------- - % A, B : :class:`Tensor` + % tsrc : :class:`.Tensor` + % input tensor. % - % Returns - % ------- - % d : numeric - % Euclidean distance, defined as the norm of the distance. + % i : :class:`int` + % position at which to insert trivial space, defaults to the last index. + % + % dual : :class:`logical` + % indicate whether or not to dualize the trivial space, defaults to + % :code:`false`. + arguments + tsrc + i = nspaces(tsrc) + 1 + dual = false + end + + spaces = insertone(space(tsrc), i, dual); + data = matrixblocks(tsrc); + + r = rank(tsrc); + if i <= r(1) + r(1) = r(1) + 1; + else + r(2) = r(2) + 1; + end + tdst = Tensor(spaces(1:r(1)), spaces(r(1)+1:end)'); + tdst.var = fill_matrix_data(tdst.var, data); + end + + function tdst = embed(tsrc, tdst) + % Embed a tensor in a different tensor. + + assert(isequal(rank(tsrc), rank(tdst)), 'tensors:argerror', ... + 'tensors must have the same rank'); - assert(isequal(size(A), size(B)) || isscalar(A) || isscalar(B), ... - 'tensors:SizeError', 'Incompatible sizes for vectorized function.'); + [bsrc, fsrc] = tensorblocks(tsrc); + [bdst, fdst] = tensorblocks(tdst); - % make everything a vector - A = arrayfun(@repartition, A); - B = arrayfun(@repartition, B); + [lia, locb] = ismember(fsrc, fdst); + nsp = nspaces(tdst); - d = norm(A - B); + for i = find(lia).' + sz = min(size(bsrc{i}, 1:nsp), size(bdst{locb(i)}, 1:nsp)); + inds = arrayfun(@(x) 1:x, sz, 'UniformOutput', false); + bdst{locb(i)}(inds{:}) = bsrc{i}(inds{:}); + end + + tdst.var = fill_tensor_data(tdst.var, bdst); end end + + + %% Comparison + methods + end + %% Linear algebra methods @@ -475,15 +580,17 @@ % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % conjugate tensor. - t = permute(t', nspaces(t):-1:1, rank(t)); + for i = 1:numel(t) + t(i) = tpermute(t(i)', nspaces(t(i)):-1:1, rank(t(i))); + end end function t = ctranspose(t) @@ -493,47 +600,57 @@ % Usage % ----- % :code:`t = ctranspose(t)` + % % :code:`t = t'` % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % adjoint tensor. - [t.codomain, t.domain] = swapvars(t.codomain, t.domain); - t.var = t.var'; + for i = 1:numel(t) + [t(i).codomain, t(i).domain] = swapvars(t(i).codomain, t(i).domain); + t(i).var = t(i).var'; + end + + t = permute(t, ndims(t):-1:1); end function d = dot(t1, t2) - % Compute the scalar dot product of two tensors. This is defined as the overlap + % Compute the scalar dot product of two tensors. This is defined as the overlap % of the two tensors, which therefore must have equal domain and codomain. This % function is sesquilinear in its arguments. % % Arguments % --------- - % t1, t2 : :class:`Tensor` + % t1, t2 : :class:`.Tensor` % tensors of equal structure. % % Returns % ------- - % d : double + % d : :class:`double` % scalar dot product of the two tensors. - assert(isequal(t1.domain, t2.domain) && isequal(t1.codomain, t2.codomain), ... - 'tensors:SpaceMismatch', ... - 'Dot product only defined for tensors of equal structure.'); - + assert(isequal(size(t1), size(t2)), 'tensors:dimerror', ... + 'input tensors must have the same size.'); + d = 0; - [mblocks1, mcharges] = matrixblocks(t1.var); - mblocks2 = matrixblocks(t2.var); - qdims = qdim(mcharges); - for i = 1:length(t1.var) - d = d + qdims(i) * sum(conj(mblocks1{i}) .* mblocks2{i}, 'all'); + for i = 1:numel(t1) + assert(isequal(t1(i).domain, t2(i).domain) && ... + isequal(t1(i).codomain, t2(i).codomain), ... + 'tensors:SpaceMismatch', ... + 'dot product only defined for tensors of equal structure.'); + [mblocks1, mcharges] = matrixblocks(t1(i).var); + mblocks2 = matrixblocks(t2(i).var); + qdims = qdim(mcharges); + for j = 1:length(mblocks1) + d = d + qdims(j) * sum(conj(mblocks1{j}) .* mblocks2{j}, 'all'); + end end end @@ -542,12 +659,12 @@ % % Arguments % --------- - % t1, t2 : :class:`Tensor` or numeric + % t1, t2 : :class:`.Tensor` or :class:`numeric` % input tensors, scalars are interpreted as scalar * eye. % % Returns % ------- - % t1 : :class:`Tensor` + % t1 : :class:`.Tensor` % output tensor if isnumeric(t1), t1 = t1 + (-t2); return; end @@ -559,6 +676,10 @@ 'Subtraction with scalars only defined for square tensors.'); I = t1(i).eye(t1(i).codomain, t1(i).domain); t1(i).var = t1(i).var - I.var .* t2(i); + elseif iszero(t2(i)) + continue; + elseif iszero(t1(i)) + t1(i) = -t2(i); else assert(isequal(t1(i).domain, t2(i).domain) && ... isequal(t1(i).codomain, t2(i).codomain), 'tensors:SpaceMismatch', ... @@ -579,17 +700,16 @@ % % Arguments % --------- - % t1, t2 : :class:`Tensor` or numeric + % t1, t2 : :class:`.Tensor` or :class:`numeric` % input tensor or scalar. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % output tensor. t = inv(t1) * t2; end - function t = mrdivide(t1, t2) % Right division of tensors. @@ -602,12 +722,12 @@ % % Arguments % --------- - % t1, t2 : :class:`Tensor` or numeric + % t1, t2 : :class:`.Tensor` or :class:`numeric` % input tensor or scalar. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % output tensor. t = t1 * inv(t2); @@ -624,26 +744,61 @@ % % Arguments % --------- - % A, B : :class:`Tensor` - % input tensors, satisfying A.domain = B.codomain. + % A, B : :class:`.Tensor` + % input tensors, satisfying :code:`A.domain == B.codomain`. % % Returns % ------- - % C : :class:`Tensor` + % C : :class:`.Tensor` % output tensor. + if isscalar(A) || isscalar(B) + C = A .* B; + return + end + szA = size(A); szB = size(B); - assert(szA(2) == szB(1)); - - % Scalar multiplications - if isnumeric(A) || isnumeric(B) - for i = szA(1):-1:1 - for j = szB(2):-1:1 - C(i,j) = A(i, 1) .* B(1, j); - for k = 2:szA(2) - C(i,j) = C(i,j) + A(i, k) .* B(k, j); - end + if szA(2) ~= szB(1) + error('mtimes:dimagree', ... + 'incompatible dimensions (%d) (%d)', szA(2), szB(1)); + end + + if isnumeric(A) + if issparse(A) && ~all(any(A, 2)) + [cod, dom] = deduce_spaces(B); + cod2 = cell(size(cod)); + for i = flip(1:length(cod)) + tmp = subspaces(cod(i)); + cod2{i} = tmp(1); + end + C = SparseTensor.zeros(SumSpace(cod2{:}), dom); + else + C = Tensor.empty(szA(1), szB(2), 0); + end + + for i = 1:szA(1) + for j = 1:szB(2) + C(i, j) = sum(A(i, :).' .* B(:, j), 'all'); + end + end + return + end + + if isnumeric(B) + if issparse(B) && ~all(any(B, 1)) + [cod, dom] = deduce_spaces(A); + dom2 = cell(size(dom)); + for i = 1:length(dom) + tmp = subspaces(dom(i)); + dom2{i} = tmp(1); + end + C = SparseTensor.zeros(cod, SumSpace(dom2{:})); + end + C(szA(1), szB(2)) = Tensor(); + for i = flip(1:szA(1)) + for j = flip(1:szB(2)) + C(i, j) = sum(A(i, :) .* B(:, j).', 'all'); end end return @@ -652,27 +807,24 @@ % Tensor multiplications for i = szA(1):-1:1 for j = szB(2):-1:1 - C(i,j) = tensorprod(A(i, 1), B(1, j), ... - rank(A(i, 1), 1) + (1:rank(A(i, 1), 2)), ... - rank(B(1, j), 1):-1:1); - for k = 2:szA(2) - C(i,j) = C(i,j) + tensorprod(A(i, k), B(k, j), ... - rank(A(i, k), 1) + (1:rank(A(i, k), 2)), ... - rank(B(k, j), 1):-1:1); - end + C(i, j) = sum(reshape(A(i, :), [], 1) .* B(:, j)); end end end + function n = nnz(A) + n = numel(A); + end + function n = norm(t, p) % Compute the matrix p-norm of a tensor. % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor, considered as a matrix from domain to codomain. % - % p : 1, 2, inf or 'fro' + % p : 1, 2, 'inf' or 'fro' % type of norm to compute % % Returns @@ -689,6 +841,7 @@ case {1, 2, Inf} n = 0; for i = 1:numel(t) + if iszero(t(i)), continue; end mblocks = matrixblocks(t(i).var); for j = 1:length(mblocks) n = max(norm(mblocks{j}, p), n); @@ -698,6 +851,7 @@ case 'fro' n = 0; for i = 1:numel(t) + if iszero(t(i)), continue; end [mblocks, mcharges] = matrixblocks(t(i).var); qdims = qdim(mcharges); for j = 1:length(mblocks) @@ -711,15 +865,21 @@ end end - function t = normalize(t) + function [t, n] = normalize(t) + n = zeros(size(t)); for i = 1:numel(t) - t(i) = t(i) .* (1 / norm(t(i))); + n(i) = norm(t(i)); + t(i) = t(i) .* (1 / n(i)); end end function t1 = plus(t1, t2) if isnumeric(t1), t1 = t2 + t1; return; end - assert(isequal(size(t1), size(t2)), 'Incompatible sizes for vectorized plus.'); + if ~isequal(size(t1), size(t2)) + error('plus:dimagree', ... + 'Incompatible sizes for vectorized plus. (%s) (%s)', ... + dim2str(size(t1)), dim2str(size(t2))); + end for i = 1:numel(t1) if isnumeric(t2(i)) @@ -727,38 +887,56 @@ 'Addition with scalars only defined for square tensors.'); I = t1(i).eye(t1(i).codomain, t1(i).domain); t1(i).var = t1(i).var + I.var .* t2(i); + elseif iszero(t2(i)) + continue; + elseif iszero(t1(i)) + t1(i) = t2(i); else - assert(isequal(t1(i).domain, t2(i).domain) && ... - isequal(t1(i).codomain, t2(i).codomain), 'tensors:SpaceMismatch', ... - 'Cannot add tensors of different structures.'); + if ~isequal(t1(i).domain, t2(i).domain) || ... + ~isequal(t1(i).codomain, t2(i).codomain) + t1_str = sprintf('%s\t<-\t%s', join(string(t1(i).codomain)), ... + join(string(t1(i).domain))); + t2_str = sprintf('%s\t<-\t%s', join(string(t2(i).codomain)), ... + join(string(t2(i).domain))); + error('tensors:spacemismatch', ... + 'Added spaces incompatible.\n%s\n%s', t1_str, t2_str); + end t1(i).var = t1(i).var + t2(i).var; end end end - function t = permute(t, p, r) + function t = tpermute(t, p, r) % Permute the spaces of a tensor. % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % - % p : (1, :) int + % p : (1, :) :class:`int` % permutation vector, by default a trivial permutation. % - % r : (1, 2) int + % r : (1, 2) :class:`int` % rank of the output tensor, by default equal to the rank of the input tensor. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % permuted tensor with desired rank. arguments t - p = 1:nspaces(t) - r = rank(t) + p = [] + r = [] + end + + if ~isscalar(t) + for i = 1:numel(t) + t(i) = tpermute(t(i), p, r); + end + t = permute(t, p); + return end if isempty(p), p = 1:nspaces(t); end @@ -769,11 +947,11 @@ if (all(p == 1:nspaces(t)) && all(rank(t) == r)), return; end - persistent cache + global cache if isempty(cache), cache = LRU; end if Options.CacheEnabled() - key = GetMD5({GetMD5_helper(t.codomain), GetMD5_helper(t.domain), p, r}, ... + key = GetMD5({t.codomain, t.domain, p, r}, ... 'Array', 'hex'); med = get(cache, key); if isempty(med) @@ -799,10 +977,11 @@ % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % - % r : (1, 2) int + % r : (1, 2) :class:`int` + % rank of the output tensor, by default equal to :code:`[nspaces(t) 0]`. % % Returns % ------- @@ -814,8 +993,11 @@ r (1,2) = [nspaces(t) 0] end - assert(sum(r) == sum(rank(t)), 'tensors:ValueError', 'Invalid new rank.'); - t = permute(t, 1:nspaces(t), r); + for i = 1:numel(t) + t(i) = tpermute(t(i), 1:nspaces(t(i)), r); + assert(sum(r) == sum(rank(t(i))), ... + 'tensors:ValueError', 'Invalid new rank.'); + end end function t = rdivide(t, a) @@ -829,10 +1011,10 @@ % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. - % uminus - % a : numeric + % + % a : :class:`numeric` % input scalar. % % Returns @@ -840,7 +1022,53 @@ % t : :class:`Tensor` % output tensor. - t.var = rdivide(t.var, a); + if isscalar(a) + for i = 1:numel(t) + t(i).var = rdivide(t(i).var, a); + end + return + end + + error('undefined'); + end + + function C = sum(A, dim) + arguments + A + dim = [] + end + + if isscalar(A), C = A; return; end + + if isempty(dim), dim = find(size(A) ~= 1, 1); end + + if strcmp(dim, 'all') + C = A(1); + for i = 2:numel(A) + C = C + A(i); + end + return + end + + if ismatrix(A) + if dim == 1 + C = A(1, :); + for i = 2:size(A, 1) + C = C + A(i, :); + end + return + end + + if dim == 2 + C = A(:, 1); + for i = 2:size(A, 2) + C = C + A(:, i); + end + return + end + end + + error('TBA'); end function C = tensorprod(A, B, dimA, dimB, ca, cb, options) @@ -848,20 +1076,20 @@ % % Arguments % --------- - % A, B : :class:`Tensor` - % input tensors, must satisfy space(A, dimA) = conj(space(B, dimB)). + % A, B : :class:`.Tensor` + % input tensors, must satisfy :code:`space(A, dimA) == conj(space(B, dimB))`. % - % dimA, dimB : (1, :) int + % dimA, dimB : (1, :) :class:`int` % selected indices to contract. % % Keyword Arguments % ----------------- - % NumDimensionsA : int + % NumDimensionsA : :class:`int` % number of spaces of A, to satisfy builtin tensorprod syntax. % % Returns % ------- - % C : :class:`Tensor` or numeric + % C : :class:`.Tensor` or :class:`numeric` % output tensor, with the uncontracted spaces of A as codomain, and the % uncontracted spaces of B as domain, or output scalar, if no uncontracted % spaces remain. @@ -873,9 +1101,56 @@ dimB ca = false cb = false - options.NumDimensionsA + options.NumDimensionsA = ndims(A) end + if ~isscalar(A) || ~isscalar(B) + szA = size(A, 1:options.NumDimensionsA); + szB = size(B, 1:max(ndims(B), max(dimB))); + + assert(all(szA(dimA) == szB(dimB)), 'tensors:SizeMismatch', ... + 'Invalid contraction sizes.'); + + uncA = 1:length(szA); uncA(dimA) = []; + uncB = 1:length(szB); uncB(dimB) = []; + + if isempty(uncA) + if isempty(uncB) + szC = [1 1]; + elseif length(uncB) == 1 + szC = [1 szB(uncB)]; + else + szC = szB(uncB); + end + elseif isempty(uncB) + if length(uncA) == 1 + szC = [szA(uncA) 1]; + else + szC = szA(uncA); + end + else + szC = [szA(uncA) szB(uncB)]; + end + + A = reshape(permute(A, [uncA dimA]), prod(szA(uncA)), prod(szA(dimA))); + B = reshape(permute(B, [dimB, uncB]), prod(szB(dimB)), prod(szB(uncB))); + + for i = prod(szA(uncA)):-1:1 + for j = prod(szB(uncB)):-1:1 + C(i,j) = tensorprod(A(i,1), B(1,j), dimA, dimB, ca, cb, ... + 'NumDimensionsA', options.NumDimensionsA); + for k = 2:prod(szA(dimA)) + C(i,j) = C(i,j) + ... + tensorprod(A(i,k), B(k,j), dimA, dimB, ca, cb, ... + 'NumDimensionsA', options.NumDimensionsA); + end + end + end + C = reshape(C, szC); + return + end + + uncA = 1:nspaces(A); uncA(dimA) = []; iA = [uncA dimA]; rA = [length(uncA) length(dimA)]; @@ -894,28 +1169,39 @@ iB = idx(iB); end - persistent cache + global cache if isempty(cache), cache = LRU; end if Options.CacheEnabled() - key = GetMD5({GetMD5_helper(A.codomain), GetMD5_helper(A.domain), ... - GetMD5_helper(B.codomain), GetMD5_helper(B.domain), ... + key = GetMD5({A.codomain, A.domain, ... + B.codomain, B.domain, ... dimA, dimB, ca, cb}, 'Array', 'hex'); med = get(cache, key); if isempty(med) med = struct; + A_ = similar(@(x,charge) uninit(x), A, iA, 'Rank', rA); med.varA = A_.var; - med.mapA = permute(fusiontrees(A), iA, rA); + [med.mapA, f] = permute(fusiontrees(A), iA, rA); + for i = rA(1) + (1:rA(2)) + if ~isdual(space(A_, i)) + [c2, f] = twist(f, i); + med.mapA = med.mapA * c2; + end + end B_ = similar(@(x,charge) uninit(x), B, iB, 'Rank', rB); med.varB = B_.var; med.mapB = permute(fusiontrees(B), iB, rB); - assert(isequal(A_.domain, B_.codomain), 'tensors:SpaceMismatch', ... - 'Contracted spaces incompatible.'); - if ~isempty(A_.codomain) || ~isempty(B_.domain) - med.C = Tensor(A_.codomain, B_.domain); + if ~isequal(A_.domain, B_.codomain) + error('tensors:SpaceMismatch', ... + 'Contracted spaces incompatible.\n%s\n%s', ... + join(string(A_.domain), ' '), ... + join(string(B_.codomain), ' ')); + end + if ~isempty(A_.codomain) || ~isempty(B_.domain) + med.C = Tensor.zeros(A_.codomain, B_.domain); else med.C = []; end @@ -928,15 +1214,19 @@ if isempty(C) Ablocks = matrixblocks(varA); Bblocks = matrixblocks(varB); - C = horzcat(Ablocks{:}) * vertcat(Bblocks{:}); + C = sum(cellfun(@mtimes, Ablocks, Bblocks), 'all'); % horzcat(Ablocks{:}) * vertcat(Bblocks{:}); else C.var = mul(C.var, varA, varB); end return end - A = permute(A, iA, rA); - B = permute(B, iB, rB); + A = tpermute(A, iA, rA); + for i = rA(1) + (1:rA(2)) + if ~isdual(space(A, i)), A = twist(A, i); end + end + % A = twist(A, [false(1, length(A.codomain)) ~isdual(A.domain')]); + B = tpermute(B, iB, rB); assert(isequal(A.domain, B.codomain), 'tensors:SpaceMismatch', ... 'Contracted spaces incompatible.'); @@ -954,30 +1244,73 @@ C.var = mul(C.var, A.var, B.var); end - function t = times(t, a) + function C = times(A, B) % Scalar product of a tensor and a scalar. % % Usage % ----- % :code:`t = times(t, a)` - % :code:`t uminus= t .* a` + % % :code:`t = a .* t` % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. - % - % a : numeric + % + % a : :class:`numeric` % input scalar. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % output tensor. - if isnumeric(t), [t, a] = swapvars(t, a); end - t.var = times(t.var, a); + if isscalar(A) && ~isscalar(B) + A = repmat(A, size(B)); + elseif isscalar(B) && ~isscalar(A) + B = repmat(B, size(A)); + end + + nd = max(ndims(A), ndims(B)); + assert(isequal(size(A, 1:nd), size(B, 1:nd)), ... + 'times:dimagree', 'incompatible dimensions.'); + + if isnumeric(A) + if issparse(A) + [cod, dom] = deduce_spaces(B); + C = SparseTensor.zeros(cod, dom); + I = find(A); + if isempty(I), return; end + C(I) = full(A(I)) .* B(I); + return + end + + C = B; + for i = 1:numel(C) + C(i).var = C(i).var .* A(i); + end + return + end + + if isnumeric(B) + C = B .* A; + return + end + + for i = numel(A):-1:1 + assert(isequal(A(i).domain, B(i).codomain), 'tensors:SpaceMismatch', ... + 'Multiplied spaces incompatible.'); + if ~isempty(A(i).codomain) || ~isempty(B(i).domain) + C(i) = Tensor.zeros(A(i).codomain, B(i).domain); + C(i).var = mul(C(i).var, A(i).var, B(i).var); + else + Ablocks = matrixblocks(A(i).var); + Bblocks = matrixblocks(B(i).var); + C(i) = horzcat(Ablocks{:}) * vertcat(Bblocks{:}); + end + end + C = reshape(C, size(A)); end function tr = trace(t) @@ -985,12 +1318,12 @@ % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor, considered as a matrix from domain to codomain. % % Returns % ------- - % tr : double + % tr : :class:`double` % matrix trace of the tensor. tr = 0; @@ -1006,30 +1339,123 @@ function t = transpose(t, p, r) % Compute the transpose of a tensor. This is defined as rotating the domain to % the codomain and vice versa, while cyclicly permuting the tensor blocks. - % Currently not implemented. % % Usage % ----- % :code:`t = transpose(t, p, rank)` + % % :code:`t = t.'` % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`:Tensor` % input tensor. % - % p : (1, :) int + % p : (1, :) :class:`int` % permutation vector, which must be cyclic. By default this is no permutation. % - % r : (1, 2) int + % r : (1, 2) :class:`int` % rank of the output tensor, by default equal to the rank of the input tensor. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % transposed output tensor. + if nargin < 2 + p = circshift(1:nspaces(t), length(t.domain)); + else + assert(iscircperm(p)); + end + + if nargin < 3 + r = flip(rank(t)); + end + + t = tpermute(t, p, r); + end + + function t = twist(t, i, inv) + % Twist the spaces of a tensor. + % + % Arguments + % --------- + % t : :class:`.Tensor` + % input tensor. + % + % i : (1, :) :class:`int` or :class:`logical` + % indices to twist. + % + % inv : :class:`logical` + % flag to indicate inverse twisting. + % + % Returns + % ------- + % t : :class:`.Tensor` + % twisted tensor with desired rank. + + arguments + t + i + inv = false + end + + if isempty(i) || ~any(i) || istwistless(braidingstyle(t(1))) + return + end + + if numel(t) > 1 + for i = 1:numel(t) + t(i) = twist(t(i), i, inv); + end + return + end + + global cache + if isempty(cache), cache = LRU; end + + if Options.CacheEnabled() + key = GetMD5({t.codomain, t.domain, i, inv}, ... + 'Array', 'hex'); + med = get(cache, key); + if isempty(med) + med = twist(fusiontrees(t), i, inv); + cache = set(cache, key, med); + end + else + med = twist(fusiontrees(t), i, inv); + end + + t.var = axpby(1, t.var, 0, t.var, 1:nspaces(t), med); + end + + function t = twistdual(t, i, inv) + % Twist the spaces of a tensor if they are dual. + % + % Arguments + % --------- + % t : :class:`.Tensor` + % input tensor. + % + % i : (1, :) :class:`int` or :class:`logical` + % indices to twist. + % + % inv : :class:`logical` + % flag to indicate inverse twisting. + % + % Returns + % ------- + % t : :class:`.Tensor` + % twisted tensor with desired rank. + arguments + t + i + inv = false + end - error('tensors:TBA', 'This method has not been implemented.'); + for j = 1:numel(t) + i_dual = i(isdual(space(t(j), i))); + t(j) = twist(t(j), i_dual, inv); + end end function t = uplus(t) @@ -1037,7 +1463,9 @@ end function t = uminus(t) - t.var = -t.var; + for i = 1:numel(t) + t(i).var = -t(i).var; + end end end @@ -1057,19 +1485,19 @@ % % Arguments % --------- - % A : :class:`Tensor` + % A : :class:`.Tensor` % square input tensor. % % Returns % ------- - % D : (:,:) :class:`Tensor` + % D : (:, :) :class:`.Tensor` % diagonal matrix of eigenvalues. % - % V : (1,:) :class:`Tensor` - % row vector of right eigenvectors such that A * V = V * D. + % V : (1, :) :class:`.Tensor` + % row vector of right eigenvectors such that :code:`A * V = V * D`. % - % W : (1,:) :class:`Tensor` - % row vector of left eigenvectors such that W' * A = D * W'. + % W : (1, :) :class:`Tensor` + % row vector of left eigenvectors such that :code:`W' * A = D * W'`. assert(isequal(A.codomain, A.domain), 'tensors:ArgumentError', ... 'Input should be square.'); @@ -1120,7 +1548,7 @@ function [Q, R] = leftorth(t, p1, p2, alg) % Factorize a tensor into an orthonormal basis `Q` and remainder `R`, such that - % :code:`permute(t, [p1 p2], [length(p1) length(p2)]) = Q * R`. + % :code:`tpermute(t, [p1 p2], [length(p1) length(p2)]) = Q * R`. % % Usage % ----- @@ -1128,30 +1556,30 @@ % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor to factorize. % - % p1, p2 : int + % p1, p2 : :class:`int` % partition of left and right indices, by default this is the partition of the % input tensor. % - % alg : char or string + % alg : :class:`char` or :class:`string` % selection of algorithms for the decomposition: - % - % - 'qr' produces an upper triangular remainder R - % - 'qrpos' corrects the diagonal elements of R to be positive. - % - 'ql' produces a lower triangular remainder R - % - 'qlpos' corrects the diagonal elements of R to be positive. - % - 'polar' produces a Hermitian and positive semidefinite R. - % - 'svd' uses a singular value decomposition. + % + % - :code:`'qr'` produces an upper triangular remainder R + % - :code:`'qrpos'` corrects the diagonal elements of R to be positive. + % - :code:`'ql'` produces a lower triangular remainder R + % - :code:`'qlpos'` corrects the diagonal elements of R to be positive. + % - :code:`'polar'` produces a Hermitian and positive semidefinite R. + % - :code:`'svd'` uses a singular value decomposition. % % Returns % ------- - % Q : :class:`Tensor` - % Orthonormal basis tensor + % Q : :class:`.Tensor` + % orthonormal basis tensor. % - % R : :class:`Tensor` - % Remainder tensor, depends on selected algorithm. + % R : :class:`.Tensor` + % remainder tensor, depends on selected algorithm. arguments t @@ -1162,9 +1590,9 @@ end if isempty(p1), p1 = 1:rank(t, 1); end - if isempty(p2), p2 = rank(t, 1) + (1:rank(t,2)); end + if isempty(p2), p2 = rank(t, 1) + (1:rank(t, 2)); end - t = permute(t, [p1 p2], [length(p1) length(p2)]); + t = tpermute(t, [p1 p2], [length(p1) length(p2)]); dims = struct; [mblocks, dims.charges] = matrixblocks(t); @@ -1181,8 +1609,9 @@ V = t.codomain.new(dims, false); if strcmp(alg, 'polar') - assert(isequal(V, prod(t.domain))); - W = t.domain; + assert(isequal(V, prod(t.domain)), ... + 'linalg:polar', 'polar decomposition should lead to square R.'); + W = V; elseif length(p1) == 1 && V == t.codomain W = t.codomain; elseif length(p2) == 1 && V == t.domain @@ -1200,7 +1629,7 @@ function [R, Q] = rightorth(t, p1, p2, alg) % Factorize a tensor into an orthonormal basis `Q` and remainder `L`, such that - % :code:`permute(t, [p1 p2], [length(p1) length(p2)]) = L * Q`. + % :code:`tpermute(t, [p1 p2], [length(p1) length(p2)]) = L * Q`. % % Usage % ----- @@ -1208,29 +1637,29 @@ % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor to factorize. % - % p1, p2 : int + % p1, p2 : :class:`int` % partition of left and right indices, by default this is the partition of the % input tensor. % - % alg : char or string + % alg : :class:`char` or :class:`string` % selection of algorithms for the decomposition: - % - % - 'rq' produces an upper triangular remainder R - % - 'rqpos' corrects the diagonal elements of R to be positive. - % - 'lq' produces a lower triangular remainder R - % - 'lqpos' corrects the diagonal elements of R to be positive. - % - 'polar' produces a Hermitian and positive semidefinite R. - % - 'svd' uses a singular value decomposition. + % + % - :code:`'rq'` produces an upper triangular remainder R + % - :code:`'rqpos'` corrects the diagonal elements of R to be positive. + % - :code:`'lq'` produces a lower triangular remainder R + % - :code:`'lqpos'` corrects the diagonal elements of R to be positive. + % - :code:`'polar'` produces a Hermitian and positive semidefinite R. + % - :code:`'svd'` uses a singular value decomposition. % % Returns % ------- - % R : :class:`Tensor` + % R : :class:`.Tensor` % Remainder tensor, depends on selected algorithm. % - % Q : :class:`Tensor` + % Q : :class:`.Tensor` % Orthonormal basis tensor. arguments @@ -1240,11 +1669,11 @@ alg {mustBeMember(alg, {'rq', 'rqpos', 'lq', 'lqpos', 'polar', 'svd'})} ... = 'rqpos' end - + if isempty(p1), p1 = 1:rank(t, 1); end if isempty(p2), p2 = rank(t, 1) + (1:rank(t,2)); end - t = permute(t, [p1 p2], [length(p1) length(p2)]); + t = tpermute(t, [p1 p2], [length(p1) length(p2)]); dims = struct; [mblocks, dims.charges] = matrixblocks(t); @@ -1260,8 +1689,9 @@ V = t.domain.new(dims, false); if strcmp(alg, 'polar') - assert(isequal(V, prod(t.codomain))); - W = t.codomain; + assert(isequal(V, prod(t.codomain)), ... + 'linalg:polar', 'polar decomposition should lead to square R.'); + W = V; elseif length(p1) == 1 && V == t.codomain W = t.codomain; elseif length(p2) == 1 && V == t.domain @@ -1279,26 +1709,26 @@ function N = leftnull(t, p1, p2, alg, atol) % Compute the left nullspace of a tensor, such that - % :code:`N' * permute(t, [p1 p2], [length(p1) length(p2)]) = 0`. + % :code:`N' * tpermute(t, [p1 p2], [length(p1) length(p2)]) = 0`. % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor to compute the nullspace. % - % p1, p2 : int + % p1, p2 : :class:`int` % partition of left and right indices, by default this is the partition of the % input tensor. % - % alg : char or string + % alg : :class:`char` or :class:`string` % selection of algorithms for the nullspace: % - % - 'svd' - % - 'qr' + % - :code:`'svd'` + % - :code:`'qr'` % % Returns % ------- - % N : :class:`Tensor` + % N : :class:`.Tensor` % orthogonal basis for the left nullspace. arguments @@ -1312,44 +1742,63 @@ if isempty(p1), p1 = 1:rank(t, 1); end if isempty(p2), p2 = rank(t, 1) + (1:rank(t, 2)); end - t = permute(t, [p1 p2], [length(p1) length(p2)]); + [U, S] = tsvd(t, p1, p2); dims = struct; - [mblocks, dims.charges] = matrixblocks(t); - Ns = cell(size(mblocks)); - dims.degeneracies = zeros(size(mblocks)); + [Sblocks, c1] = matrixblocks(S); + [Ublocks, c2] = matrixblocks(U); - for i = 1:length(mblocks) - Ns{i} = leftnull(mblocks{i}, alg, atol); - dims.degeneracies(i) = size(Ns{i}, 2); + lia = ismember_sorted(c2, c1); + ctr = 0; + for i = 1:length(Ublocks) + if ~lia(i), continue; end + ctr = ctr + 1; + u = Ublocks{i}; + s = Sblocks{ctr}; + if isvector(s) + diags = s(1); + else + diags = diag(s); + end + r = sum(diags > atol); + Ublocks{i} = u(:, (r + 1):size(u, 1)); end - N = t.eye(t.codomain, t.codomain.new(dims, false)); + dims.degeneracies = cellfun(@(x) size(x, 2), Ublocks); + dims.charges = c2; + + mask = dims.degeneracies > 0; + dims.charges = c2(mask); + dims.degeneracies = dims.degeneracies(mask); + Ns = Ublocks(mask); + + W = t.codomain.new(dims, false); + N = t.eye(U.codomain, W); N.var = fill_matrix_data(N.var, Ns, dims.charges); end function N = rightnull(t, p1, p2, alg, atol) - % Compute the right nullspace of a tensor, such that - % :code:`permute(t, [p1 p2], [length(p1) length(p2)]) * N = 0`. + % Compute the right nullspace of a tensor, such that + % :code:`tpermute(t, [p1 p2], [length(p1) length(p2)]) * N = 0`. % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor to compute the nullspace. % - % p1, p2 : int + % p1, p2 : :class:`int` % partition of left and right indices, by default this is the partition of the % input tensor. % - % alg : char or string + % alg : :class:`char` or :class:`string` % selection of algorithms for the nullspace: % - % - 'svd' - % - 'lq' + % - :code:`'svd'` + % - :code:`'lq'` % % Returns % ------- - % N : :class:`Tensor` + % N : :class:`.Tensor` % orthogonal basis for the right nullspace. arguments @@ -1363,62 +1812,107 @@ if isempty(p1), p1 = 1:rank(t, 1); end if isempty(p2), p2 = rank(t, 1) + (1:rank(t, 2)); end - t = permute(t, [p1 p2], [length(p1) length(p2)]); + [~, S, V] = tsvd(t, p1, p2); dims = struct; - [mblocks, dims.charges] = matrixblocks(t); - Ns = cell(size(mblocks)); - dims.degeneracies = zeros(size(mblocks)); + [Sblocks, c1] = matrixblocks(S); + [Vblocks, c2] = matrixblocks(V); - for i = 1:length(mblocks) - Ns{i} = rightnull(mblocks{i}, alg, atol); - dims.degeneracies(i) = size(Ns{i}, 1); + lia = ismember_sorted(c2, c1); + ctr = 0; + for i = 1:length(Vblocks) + if ~lia(i), continue; end + ctr = ctr + 1; + v = Vblocks{i}; + s = Sblocks{ctr}; + if isvector(s) + diags = s(1); + else + diags = diag(s); + end + r = sum(diags > atol); + Vblocks{i} = v((r + 1):size(v, 2), :); end - N = Tensor.eye(t.domain.new(dims, false), t.domain); + dims.degeneracies = cellfun(@(x) size(x, 1), Vblocks); + dims.charges = c2; + + mask = dims.degeneracies > 0; + dims.charges = c2(mask); + dims.degeneracies = dims.degeneracies(mask); + Ns = Vblocks(mask); + + W = t.codomain.new(dims, false); + N = t.eye(W, V.domain); N.var = fill_matrix_data(N.var, Ns, dims.charges); +% +% [~, S, V] = tpermute(t, [p1 p2], [length(p1) length(p2)]); +% +% dims = struct; +% [mblocks, dims.charges] = matrixblocks(t); +% Ns = cell(size(mblocks)); +% dims.degeneracies = zeros(size(mblocks)); +% +% for i = 1:length(mblocks) +% Ns{i} = rightnull(mblocks{i}, alg, atol); +% dims.degeneracies(i) = size(Ns{i}, 1); +% end +% +% mask = dims.degeneracies > 0; +% dims.charges = dims.charges(mask); +% dims.degeneracies = dims.degeneracies(mask); +% Ns = Ns(mask); +% +% N = Tensor.eye(t.domain.new(dims, false), t.domain); +% N.var = fill_matrix_data(N.var, Ns, dims.charges); end function [U, S, V, eta] = tsvd(t, p1, p2, trunc) % Compute the singular value decomposition of a tensor. This computes left and % right isometries U and V, and a non-negative diagonal tensor S such that - % norm(permute(t, [p1 p2], [length(p1) length(p2)]) - U * S * V) = 0 + % :code:`norm(tpermute(t, [p1 p2], [length(p1) length(p2)]) - U * S * V) = 0` % Additionally, the dimension of S can be truncated in such a way to minimize % this norm, which gives the truncation error eta. % % Usage % ----- - % [U, S, V] = tsvd(t, p1, p2) - % [U, S, V, eta] = tsvd(t, p1, p2, trunc, tol) - % S = tsvd(t, ...) + % :code:`[U, S, V] = tsvd(t, p1, p2)` + % + % :code:`[U, S, V, eta] = tsvd(t, p1, p2, trunc, tol)` + % + % :code:`S = tsvd(t, ...)` % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % - % p1, p2 : int + % p1, p2 : :class:`int` % partition of left and right indices, by default this is the partition of the % input tensor. % % Keyword Arguments % ----------------- - % TruncDim : int - % truncate such that the size of S is not larger than this value. + % TruncDim : :class:`int` + % truncate such that the dim of S is not larger than this value for any given + % charge. % - % TruncBelow : numeric + % TruncTotalDim : :class:`int` + % truncate such that the total dim of S is not larger than this value. + % + % TruncBelow : :class:`numeric` % truncate such that there are no singular values below this value. % - % TruncSpace : :class:`AbstractSpace` + % TruncSpace : :class:`.AbstractSpace` % truncate such that the space of S is smaller than this value. % % Returns % ------- - % U, S, V : :class:`Tensor` + % U, S, V : :class:`.Tensor` % left isometry U, non-negative diagonal S and right isometry V that satisfy - % U * S * V = permute(t, [p1 p2], [length(p1) length(p2)]). + % :code:`U * S * V = tpermute(t, [p1 p2], [length(p1) length(p2)])`. % - % eta : numeric + % eta : :class:`numeric` % truncation error. arguments @@ -1426,11 +1920,12 @@ p1 = 1:t.rank(1) p2 = t.rank(1) + (1:t.rank(2)) trunc.TruncDim + trunc.TruncTotalDim trunc.TruncBelow trunc.TruncSpace end - t = permute(t, [p1 p2], [length(p1) length(p2)]); + t = tpermute(t, [p1 p2], [length(p1) length(p2)]); dims = struct; [mblocks, dims.charges] = matrixblocks(t); @@ -1440,7 +1935,7 @@ dims.degeneracies = zeros(size(mblocks)); doTrunc = ~isempty(fieldnames(trunc)); - if doTrunc, eta = 0; end + eta = 0; for i = 1:length(mblocks) if doTrunc [Us{i}, Ss{i}, Vs{i}] = svd(mblocks{i}, 'econ'); @@ -1455,26 +1950,57 @@ for i = 1:length(mblocks) s = diag(Ss{i}); dims.degeneracies(i) = sum(s > trunc.TruncBelow); - eta = eta + sum(s(dims.degeneracies(i) + 1:end)); + eta = eta + sum(s(dims.degeneracies(i) + 1:end).^2 * qdim(dims.charges(i))); Us{i} = Us{i}(:, 1:dims.degeneracies(i)); Ss{i} = diag(s(1:dims.degeneracies(i))); Vs{i} = Vs{i}(1:dims.degeneracies(i), :); end end + if isfield(trunc, 'TruncTotalDim') + qdims = qdim(dims.charges); + totaldim = sum(dims.degeneracies .* qdims); + minvals = cellfun(@(x) min(diag(x)), Ss); + while totaldim > trunc.TruncTotalDim + [~, i] = min(minvals); + eta = eta + Ss{i}(end, end)^2 * qdims(i); + dims.degeneracies(i) = dims.degeneracies(i) - 1; + Us{i} = Us{i}(:, 1:end-1); + Vs{i} = Vs{i}(1:end-1, :); + Ss{i} = Ss{i}(1:end-1, 1:end-1); + if dims.degeneracies(i) > 0 + minvals(i) = Ss{i}(end,end); + else + minvals(i) = Inf; + end + totaldim = totaldim - qdims(i); + end + end if isfield(trunc, 'TruncDim') for i = 1:length(mblocks) dims.degeneracies(i) = min(dims.degeneracies(i), trunc.TruncDim); s = diag(Ss{i}); - eta = eta + sum(s(dims.degeneracies(i) + 1:end)); + eta = eta + sum(s(dims.degeneracies(i) + 1:end).^2 * qdim(dims.charges(i))); Us{i} = Us{i}(:, 1:dims.degeneracies(i)); Ss{i} = diag(s(1:dims.degeneracies(i))); - Vs{i} = Vs{i}(1:1:dims.degeneracies(i), :); + Vs{i} = Vs{i}(1:dims.degeneracies(i), :); end end if isfield(trunc, 'TruncSpace') - error('TBA'); + truncspace = trunc.TruncSpace; + [b, ind] = ismember(truncspace.dimensions.charges, dims.charges); + assert(all(b),"Truncation space contains charges that S does not.") + assert(all(dims.degeneracies(ind) >= truncspace.dimensions.degeneracies), "Truncation space has degeneracies larger than S.") + dims.degeneracies(ind) = truncspace.dimensions.degeneracies; + dims.degeneracies(setxor(ind,1:numel(dims.degeneracies))) = 0; + for i = 1:length(mblocks) + s = diag(Ss{i}); + eta = eta + sum(s(dims.degeneracies(i) + 1:end).^2 * qdim(dims.charges(i))); + Us{i} = Us{i}(:, 1:dims.degeneracies(i)); + Ss{i} = diag(s(1:dims.degeneracies(i))); + Vs{i} = Vs{i}(1:dims.degeneracies(i), :); + end end - + if ~doTrunc W1 = prod(t.codomain); W2 = prod(t.domain); @@ -1483,16 +2009,28 @@ S = Tensor.zeros(W1, W2); V = Tensor.eye(W2, t.domain); else + mask = dims.degeneracies ~= 0; + dims.charges = dims.charges(mask); + dims.degeneracies = dims.degeneracies(mask); W = t.domain.new(dims, false); U = Tensor.eye(t.codomain, W); S = Tensor.zeros(W, W); V = Tensor.eye(W, t.domain); + + Us = Us(mask); + Vs = Vs(mask); + Ss = Ss(mask); end U.var = fill_matrix_data(U.var, Us, dims.charges); S.var = fill_matrix_data(S.var, Ss, dims.charges); V.var = fill_matrix_data(V.var, Vs, dims.charges); + + if nargout <= 1 + U = S; + end + eta = sqrt(eta); end end @@ -1505,12 +2043,12 @@ % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % output tensor. assert(isequal(t.codomain, t.domain), 'tensors:ArgumentError', ... @@ -1524,26 +2062,33 @@ end function t = inv(t) - % Compute the matrix inverse of a square tensor, such that t * inv(t) = I. + % Compute the matrix inverse of a square tensor, such that + % :code:`t * inv(t) = I`. % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % output tensor. - assert(isequal(t.codomain, t.domain), 'tensors:ArgumentError', ... + assert(isisometric(t.codomain, t.domain), 'tensors:ArgumentError', ... 'Input should be square.'); mblocks = matrixblocks(t); for i = 1:length(mblocks) mblocks{i} = inv(mblocks{i}); end - t.var = fill_matrix_data(t.var, mblocks); + + if isequal(t.codomain, t.domain) + t.var = fill_matrix_data(t.var, mblocks); + else + t = t'; + t.var = fill_matrix_data(t.var, mblocks); + end end function A = mpower(X, Y) @@ -1551,25 +2096,25 @@ % % Usage % ----- - % :class:`A = x^Y` + % :code:`A = x^Y` % - % :class:`A = X^y` + % :code:`A = X^y` % % Arguments % --------- - % X, Y : :class:`Tensor` - % Square input tensor. + % X, Y : :class:`.Tensor` + % square input tensor. % - % x, y : numeric - % Input scalars. + % x, y : :class:`numeric` + % input scalars. % % Returns % ------- - % A : :class:`Tensor` - % Output tensor. + % A : :class:`.Tensor` + % output tensor. % tensor to a scalar power - if isnumeric(Y) && isscalar(Y) + if isnumeric(Y) && isscalar(Y) assert(isequal(X.codomain, X.domain), 'tensors:ArgumentError', ... 'Input tensor should be square.'); mblocks = matrixblocks(X); @@ -1600,22 +2145,23 @@ function [A, resnorm] = sqrtm(A) % Compute the principal square root of a square tensor. This is the unique root - % for which every eigenvalue has nonnegative real part. If `t` is singular, then - % the result may not exist. + % for which every eigenvalue has nonnegative real part. If :code:`t` is + % singular, then the result may not exist. % % Arguments % --------- - % A : :class:`Tensor` + % A : :class:`.Tensor` % input tensor. % % Returns % ------- - % X : :class:`Tensor` - % principal square root of the input, which has X^2 = A. + % X : :class:`.Tensor` + % principal square root of the input, which has :code:`X^2 = A`. % - % resnorm : numeric - % the relative residual, norm(A - X^2, 1) / norm(A, 1). If this argument is - % returned, no warning is printed if exact singularity is detected. + % resnorm : :class:`numeric` + % the relative residual, :code:`norm(A - X^2, 1) / norm(A, 1)`. If this + % argument is returned, no warning is printed if exact singularity is + % detected. assert(isequal(A.codomain, A.domain), 'tensors:ArgumentError', ... 'Input should be square.'); @@ -1637,7 +2183,7 @@ end - %% + %% methods function bool = isposdef(t) % Test if a tensor is a positive-definite map. Generally, a Hermitian matrix `M` @@ -1645,19 +2191,19 @@ % nonzero complex column vector `z`. % This is equivalent to any of the following conditions: % - % - M is Hermitian and all eigenvalues are real and positive. - % - M is congruent with a diagonal matrix with positive real entries. - % - There exists an invertible B such that `M = B' * B`. + % - `M` is Hermitian and all eigenvalues are real and positive. + % - `M` is congruent with a diagonal matrix with positive real entries. + % - There exists an invertible `B` such that `M = B' * B`. % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % % Returns % ------- - % bool : logical - % true if `t` is positive definite. + % bool : :class:`logical` + % true if :code:`t` is positive definite. mblocks = matrixblocks(t); for i = 1:length(mblocks) @@ -1675,22 +2221,22 @@ % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % - % side : char + % side : :class:`char` % either 'left', 'right' or 'both' (default). % % Keyword Arguments % ----------------- - % AbsTol, RelTol : numeric - % `norm(M * M' - eye(size(M))) < max(AbsTol, RelTol * norm(M))`. - % By default `AbsTol = 0` and `RelTol = eps`. + % AbsTol, RelTol : :class:`numeric` + % :code:`norm(t * t' - eye(size(t))) < max(AbsTol, RelTol * norm(t))`. + % By default :code:`AbsTol = 0` and :code:`RelTol = eps`. % % Returns % ------- - % bool : logical - % true if t is isometric. + % bool : :class:`logical` + % true if :code:`t` is isometric. arguments t @@ -1699,6 +2245,12 @@ tol.AbsTol = 0 end + if numel(t) > 1 + bool = arrayfun(@(x) isisometry(x, side, ... + 'RelTol', tol.RelTol, 'AbsTol', tol.AbsTol), t); + return + end + mblocks = matrixblocks(t); for i = 1:length(mblocks) if ~isisometry(mblocks{i}, side, 'RelTol', tol.RelTol, 'AbsTol', tol.AbsTol) @@ -1750,6 +2302,11 @@ return end end + bool = true; + end + + function bool = iszero(t) + bool = isempty(t.var) || iszero(t.var); end function r = cond(t, p) @@ -1760,7 +2317,7 @@ % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % % p : 1, 2, inf or 'fro' @@ -1783,289 +2340,22 @@ %% Solvers methods - function varargout = linsolve(A, b, x0, M1, M2, options) - % Find a solution for a linear system `A(x) = b` or `A * x = b`. - % - % Arguments - % --------- - % A : operator - % either a function handle implementing or an object that supports - % right multiplication. - % - % b : :class:`Tensor` - % right-hand side of the equation, interpreted as vector. - % - % x0 : :class:`Tensor` - % optional initial guess for the solution. - % - % M1, M2 : operator - % preconditioner M = M1 or M = M1 * M2 to effectively solve the system A * - % inv(M) * y = b with y = M * x. - % M is either a function handle implementing or an object that supports - % left division. - % - % Keyword Arguments - % ----------------- - % Tol : numeric - % specifies the tolerance of the method, by default this is the square root of - % eps. - % - % Algorithm : char - % specifies the algorithm used. Can be either one of the following: - % - % - 'bicgstab' - % - 'bicgstabl' - % - 'gmres' - % - 'pcg' - % - % MaxIter : int - % Maximum number of iterations. - % - % Restart : int - % For 'gmres', amount of iterations after which to restart. - % - % Verbosity : int - % Level of output information, by default nothing is printed if `flag` is - % returned, otherwise only warnings are given. - % - % - 0 : no information - % - 1 : information at failure - % - 2 : information at convergence - % - % Returns - % ------- - % x : :class:`Tensor` - % solution vector. - % - % flag : int - % a convergence flag: - % - % - 0 : linsolve converged to the desired tolerance. - % - 1 : linsolve reached the maximum iterations without convergence. - % - 2 : linsolve preconditioner was ill-conditioned. - % - 3 : linsolve stagnated. - % - 4 : one of the scalar quantities calculated became too large or too small. - % - % relres : numeric - % relative residual, norm(b - A * x) / norm(b). - % - % iter : int - % iteration number at which x was computed. - % - % resvec : numeric - % vector of estimated residual norms at each part of the iteration. - - arguments - A - b - x0 = [] - M1 = [] - M2 = [] - - options.Tol = eps(underlyingType(b)) ^ (3/4) - options.Algorithm {mustBeMember(options.Algorithm, ... - {'pcg', 'gmres', 'bicgstab', 'bicgstabl'})} = 'gmres' - options.MaxIter = 400 - options.Restart = 30 - options.Verbosity = 0 - end - - % Convert input objects to vectors - b_vec = vectorize(b); - b_sz = size(b_vec); - - if ~isempty(x0) - x0_vec = vectorize(x0); - else - x0_vec = []; - end - - % Convert input operators to handle vectors - if isa(A, 'function_handle') - A_fun = @(x) vectorize(A(devectorize(x, b))); - else - A_fun = @(x) vectorize(A * devectorize(x, b)); - end - - if isempty(M1) - M1_fun = []; - elseif isa(M1, 'function_handle') - M1_fun = @(x) vectorize(M1(devectorize(x, b))); - else - M1_fun = @(x) vectorize(M1 \ devectorize(x, b)); - end - - if isempty(M2) - M2_fun = []; - elseif isa(M2, 'function_handle') - M2_fun = @(x) vectorize(M2(devectorize(x, b))); - else - M2_fun = @(x) vectorize(M2 \ devectorize(x, b)); - end - - % Sanity check on parameters - options.Restart = min(options.Restart, b_sz(1)); - if options.Tol < eps(underlyingType(b))^0.9 - warning('Requested tolerance might be too strict.'); - end - - % Apply MATLAB implementation - switch options.Algorithm - case 'bicgstab' - [varargout{1:nargout}] = bicgstab(A_fun, b_vec, ... - options.Tol, options.MaxIter, M1_fun, M2_fun, x0_vec); - case 'bicgstabl' - [varargout{1:nargout}] = bicgstabl(A_fun, b_vec, ... - options.Tol, options.MaxIter, M1_fun, M2_fun, x0_vec); - case 'gmres' - options.MaxIter = min(b_sz(1), options.MaxIter); - [varargout{1:nargout}] = gmres(A_fun, b_vec, ... - options.Restart, options.Tol, options.MaxIter, ... - M1_fun, M2_fun, x0_vec); - case 'pcg' - [varargout{1:nargout}] = pcg(A_fun, b_vec, ... - options.Tol, options.MaxIter, M1_fun, M2_fun, x0_vec); - end - - - % Convert output - varargout{1} = devectorize(varargout{1}, b); - end - - function varargout = eigsolve(A, x0, howmany, sigma, options) - % Find a few eigenvalues and eigenvectors of an operator. - % - % Usage - % ----- - % :code:`[V, D, flag] = eigsolve(A, x0, howmany, sigma, kwargs)` - % :code:`D = eigsolve(A, x0, ...)` - % - % Arguments - % --------- - % A : :class:`Tensor` or function_handle - % A square tensormap interpreted as matrix. - % A function handle which implements one of the following, depending on sigma: - % - % - A \ x, if `sigma` is 0 or 'smallestabs' - % - (A - sigma * I) \ x, if sigma is a nonzero scalar - % - A * x, for all other cases - % - % x0 : :class:`Tensor` - % initial guess for the eigenvector. If A is a :class:`Tensor`, this defaults - % to a random complex :class:`Tensor`, for function handles this is a required - % argument. - % - % howmany : int - % amount of eigenvalues and eigenvectors that should be computed. By default - % this is 1, and this should not be larger than the total dimension of A. - % - % sigma : 'char' or numeric - % selector for the eigenvalues, should be either one of the following: - % - % - 'largestabs', 'largestreal', 'largestimag' : default, eigenvalues of - % largest magnitude, real part or imaginary part. - % - 'smallestabs', 'smallestreal', 'smallestimag' : eigenvalues of smallest - % magnitude, real part or imaginary part. - % - numeric : eigenvalues closest to sigma. - % - % Keyword Arguments - % ----------------- - % Tol : numeric - % tolerance of the algorithm. - % - % Algorithm : char - % choice of algorithm. Currently only 'eigs' is available, which leverages the - % default Matlab eigs. - % - % MaxIter : int - % maximum number of iterations, 100 by default. - % - % KrylovDim : int - % number of vectors kept in the Krylov subspace. - % - % IsSymmetric : logical - % flag to speed up the algorithm if the operator is symmetric, false by - % default. - % - % Verbosity : int - % Level of output information, by default nothing is printed if `flag` is - % returned, otherwise only warnings are given. - % - % - 0 : no information - % - 1 : information at failure - % - 2 : information at convergence - % - 3 : information at every iteration - % - % Returns - % ------- - % V : (1, howmany) :class:`Tensor` - % vector of eigenvectors. - % - % D : numeric - % vector of eigenvalues if only a single output argument is asked, diagonal - % matrix of eigenvalues otherwise. - % - % flag : int - % if flag = 0 then all eigenvalues are converged, otherwise not. - - arguments - A - x0 = A.randnc(A.domain, []) - howmany = 1 - sigma = 'largestabs' - - options.Tol = 1e-12 - options.Algorithm = 'eigs' - options.MaxIter = 100 - options.KrylovDim = 20 - options.IsSymmetric logical = false - options.Verbosity = 0 - end - - assert(isnumeric(sigma) || ismember(sigma, {'largestabs', 'smallestabs', ... - 'largestreal', 'smallestreal', 'bothendsreal', ... - 'largestimag', 'smallestimag', 'bothendsimag'}), ... - 'tensors:ArgumentError', 'Invalid choice of eigenvalue selector.'); - - x0_vec = vectorize(x0); - sz = size(x0_vec); - - if isa(A, 'function_handle') - A_fun = @(x) vectorize(A(devectorize(x, x0))); - else - A_fun = @(x) vectorize(A * devectorize(x, x0)); - end - - options.KrylovDim = min(sz(1), options.KrylovDim); - - [varargout{1:nargout}] = eigs(A_fun, sz(1), howmany, sigma, ... - 'Tolerance', options.Tol, 'MaxIterations', options.MaxIter, ... - 'SubspaceDimension', options.KrylovDim, 'IsFunctionSymmetric', ... - options.IsSymmetric, 'StartVector', x0_vec); - if nargout > 1 - for i = howmany:-1:1 - V(:, i) = devectorize(varargout{1}(:, i), x0); - end - varargout{1} = V; - end - end - function v = vectorize(t, type) % Collect all parameters in a vector, weighted to reproduce the correct - % inproduct. + % inner product. % % Arguments % --------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % - % type : 'real' or 'complex' + % type : :class:`char`, 'real' or 'complex' % optionally specify if complex entries should be seen as 1 or 2 parameters. % Defaults to 'complex', with complex parameters. % % Returns % ------- - % v : numeric + % v : :class:`numeric` % real or complex vector containing the parameters of the tensor. arguments @@ -2073,7 +2363,7 @@ type = 'complex' end - v = vectorize(t.var, type); + v = vectorize([t.var], type); end function t = devectorize(v, t, type) @@ -2081,19 +2371,19 @@ % % Arguments % --------- - % v : numeric + % v : :class:`numeric` % real or complex vector containing the parameters of the tensor. % - % t : :class:`Tensor` + % t : :class:`.Tensor` % input tensor. % - % type : 'real' or 'complex' + % type : :class:`char`, 'real' or 'complex' % optionally specify if complex entries should be seen as 1 or 2 parameters. % Defaults to 'complex', with complex parameters. % % Returns % ------- - % t : :class:`Tensor` + % t : :class:`.Tensor` % output tensor, filled with the parameters. arguments @@ -2102,6 +2392,14 @@ type = 'complex' end + if numel(t) > 1 + X = devectorize(v, [t.var], type); + for i = 1:numel(t) + t(i).var = X(i); + end + return + end + t.var = devectorize(v, t.var, type); end end @@ -2114,19 +2412,20 @@ % % Arguments % --------- - % t : Tensor + % t : :class:`Tensor` % % Returns % ------- - % a : double + % a : :class:`double` - trees = fusiontrees(t); - blocks = tensorblocks(t); - if isempty(trees) + if isa(t.var, 'TrivialBlock') + blocks = tensorblocks(t); a = blocks{1}; return end + [blocks, trees] = tensorblocks(t); + % Initialize output a = zeros(dims(t)); s = [t.codomain flip(t.domain)]; @@ -2157,11 +2456,70 @@ a = cell2mat(a_cell); end + + function a = sparse(t) + a = SparseTensor(t); + end + + function tdst = desymmetrize(tsrc, mode) + arguments + tsrc + mode {mustBeMember(mode, {'Cartesian', 'Complex'})} = 'Complex' + end + + tdst = repartition(Tensor(double(tsrc)), rank(tsrc)); + if strcmp(mode, 'Complex') + if ~isempty(tsrc.domain), tdst.domain = ComplexSpace(tsrc.domain); end + if ~isempty(tsrc.codomain), tdst.codomain = ComplexSpace(tsrc.codomain); end + end + end + + function mps = FiniteMps(t, varargin) + A = MpsTensor.decompose_local_state(t, varargin{:}); + mps = FiniteMps(A); + end end %% Utility methods + function [I, J, V] = find(t, k, which) + arguments + t + k = [] + which = 'first' + end + assert(strcmp(which, 'first'), 'not implemented yet') + if ~isempty(k) + assert(k <= numel(t)); + else + k = numel(t); + end + + if isempty(t) + I = []; + J = []; + V = []; + return + end + + if ~isempty(k) + if strcmp(which, 'first') + I = 1:k; + else + I = numel(t):-1:numel(t)+1-k; + end + end + I = reshape(I, [], 1); + if nargout < 2, return; end + + sz = size(t); + subs = ind2sub_([sz(1) prod(sz(2:end))], I); + I = subs(:, 1); + J = subs(:, 2); + V = reshape(t, [], 1); + end + function s = GetMD5_helper(t) s = {t.codomain t.domain}; end @@ -2170,18 +2528,68 @@ type = underlyingType(t(1).var); end - function disp(t) + function [codomain, domain] = deduce_spaces(t) + spaces = cell(1, nspaces(t)); + subs = repmat({1}, 1, nspaces(t)); + for i = 1:length(spaces) + for j = flip(1:size(t, i)) + subs{i} = j; + spaces{i}(j) = space(t(subs{:}), i); + end + subs{i} = 1; + end + Nout = indout(t); + if Nout > 0 + codomain = SumSpace(spaces{1:Nout}); + else + codomain = SumSpace([]); + end + if Nout == length(spaces) + domain = SumSpace([]); + else + domain = SumSpace(spaces{(Nout+1):end})'; + end + end + + function disp(t, details) + if nargin == 1 || isempty(details), details = false; end if isscalar(t) r = t.rank; - fprintf('Rank (%d, %d) %s:\n\n', r(1), r(2), class(t)); + fprintf('Rank (%d, %d) %s:\n', r(1), r(2), class(t)); s = space(t); for i = 1:length(s) - fprintf('%d.\t', i); + fprintf('\t%d.\t', i); disp(s(i)); - fprintf('\n'); + fprintf('\b'); + end + fprintf('\n'); + if details + [blocks, charges] = matrixblocks(t); + for i = 1:length(blocks) + if ~isempty(blocks) + fprintf('charge %s:\n', string(charges(i))); + end + disp(blocks{i}); + end end else - builtin('disp', t); + fprintf('%s of size %s:\n', class(t), ... + regexprep(mat2str(size(t)), {'\[', '\]', '\s+'}, {'', '', 'x'})); + subs = ind2sub_(size(t), 1:numel(t)); + spc = floor(log10(max(double(subs), [], 1))) + 1; + if numel(spc) == 1 + fmt = strcat("\t(%", num2str(spc(1)), "u)"); + else + fmt = strcat("\t(%", num2str(spc(1)), "u,"); + for i = 2:numel(spc) - 1 + fmt = strcat(fmt, "%", num2str(spc(i)), "u,"); + end + fmt = strcat(fmt, "%", num2str(spc(end)), "u)"); + end + for i = 1:numel(t) + fprintf('%s\t\t', compose(fmt, subs(i, :))); + disp(t(i), details); + end end end end diff --git a/src/tensors/charges/AbstractCharge.m b/src/tensors/charges/AbstractCharge.m index 0cfaf9c..9f97052 100644 --- a/src/tensors/charges/AbstractCharge.m +++ b/src/tensors/charges/AbstractCharge.m @@ -1,5 +1,5 @@ classdef (Abstract) AbstractCharge - % AbstractCharge - Abstract base class for objects in a fusion category. + % Abstract base class for objects in a fusion category. %% Required categorical data. methods @@ -28,13 +28,13 @@ % % Arguments % -------- - % a : :class:`AbstractCharge` + % a : :class:`.AbstractCharge` % input charge % % Returns % ------- - % abar : :class:`AbstractCharge` - % conjugate charge suche that :code:`one(a)` is an element of :code:`a * abar` + % abar : :class:`.AbstractCharge` + % conjugate charge such that :code:`one(a)` is an element of :code:`a * abar` error('AbstractCharge:requiredMethod', ... 'Error. \nMethod must be overloaded.') end @@ -69,7 +69,7 @@ % % Returns % ------- - % c : :class:`.AbstractCharge` (1, \*) + % c : (1, :) :class:`.AbstractCharge` % the unique elements in the decomposition of the tensor product of ``a`` and % ``b`` error('AbstractCharge:requiredMethod', ... @@ -109,7 +109,7 @@ % % Returns % ------- - % F : :class:`double` (\*, \*, \*, \*) + % F : (:, :, :, :) :class:`double` % recoupling coefficients error('AbstractCharge:requiredMethod', ... 'Error. \nMethod must be overloaded.') @@ -177,7 +177,7 @@ % % Returns % ------- - % C : :class:`double` (\*, \*, \*, \*) + % C : (:, :, :, :) :class:`double` % fusion tensor error('AbstractCharge:optionalMethod', ... 'Error. \nMethod must be overloaded.') @@ -211,7 +211,7 @@ % % Returns % ------- - % R : :class:`double` (\*, \*) + % R : (:, :) :class:`double` % braiding coefficients error('AbstractCharge:optionalMethod', ... 'Error. \nMethod must be overloaded.') @@ -224,7 +224,7 @@ % Compute the fusion to splitting coefficient. % % .. todo:: - % Add diagram? + % Add diagram. % % Arguments % --------- @@ -246,7 +246,7 @@ % Compute the splitting to fusion coefficient. % % .. todo:: - % Add diagram? + % Add diagram. % % Arguments % --------- @@ -303,7 +303,7 @@ % Create a matrix-representation of an arrowflip. % % .. todo:: - % Add diagram or definition? + % Add diagram and proper definition. % % Arguments % --------- @@ -311,7 +311,7 @@ % % Returns % ------- - % F : :class:`double` (\*, \*) + % F : (:, :) :class:`double` % matrix-representation of an arrowflip F = conj(sqrt(qdim(a)) .* fusiontensor(conj(a), a, one(a))); end @@ -320,7 +320,7 @@ % Compute the full recoupling matrix from ``e`` to ``f``. % % .. todo:: - % Add proper definition? + % Add proper definition. % % Usage % ----- @@ -333,14 +333,14 @@ % charges being fused % d : :class:`.AbstractCharge` % total charges - % e : :class:`.AbstractCharge` (1, \*) + % e : (1, :) :class:`.AbstractCharge` % intermediate charges before recoupling - % f : :class:`.AbstractCharge` (1, \*) + % f : (1, :) :class:`.AbstractCharge` % intermediate charge after recoupling % % Returns % ------- - % F : :class:`double` (\*, \*, \*, \*) + % F : (:, :, :, :) :class:`double` % recoupling matrix between all allowed channels if a.fusionstyle == FusionStyle.Unique if nargin < 5, e = a * b; end @@ -414,7 +414,7 @@ % Compute the coefficient obtained by twisting a charge. % % .. todo:: - % Add diagram/definition? + % Add diagram and proper definition. % % Arguments % --------- @@ -449,19 +449,21 @@ %% Utility functions methods function [d, N] = prod(a, dim) - % Total fusion product of charges. - % - % .. todo:: - % Complete docstring + % Compute the total fusion product of array of charges along a given axis. % % Arguments % --------- - % a, b, c, ... : :class:`.AbstractCharge` + % a : :class:`.AbstractCharge` + % input array of charges + % dim : :class:`int` + % array dimension along which to take the fusion product, defaults to first + % non-trivial axis % % Returns % ------- % c : :class:`.AbstractCharge` - % total fusion product determined by subsequently multiplying input charges + % array of total fusion products determined by subsequently multiplying input + % charges along the given direction arguments a dim = find(size(a) ~= 1, 1) @@ -550,11 +552,12 @@ % Cumulative fusion product of elements. % % .. todo:: - % Complete docstring + % Complete docstring. % % Usage % ----- - % :code:`Y = cumprod(X)` computes the cumulative fusion product along the + % :code:`Y = cumprod(X)` + % computes the cumulative fusion product along the % columns of X. % % For ``FusionStyle.Unique``, ``Y`` has the same size as ``X``, which can be @@ -564,7 +567,7 @@ % % Arguments % --------- - % a: :class:`.AbstractCharge` (\*, \*) + % a: (:, :) :class:`.AbstractCharge` % % Returns % ------- @@ -572,6 +575,7 @@ % explanation % vertices : type % explanation + if a.fusionstyle == FusionStyle.Unique charges = a; for i = 2:size(charges, 1) @@ -590,7 +594,8 @@ charges = []; vertices = []; for i = 1:size(a, 2) - [chargepart, vertexpart] = cumprod(a(:, i)); + [chargepart, vertexpart] = cumprod(... + subsref(a, substruct('()', {':', i}))); charges = horzcat(charges, chargepart); vertices = horzcat(vertices, vertexpart); end @@ -605,17 +610,20 @@ end if length(a) == 2 - f = a(1) * a(2); - charges = [repmat(a(1), 1, length(f)); f]; + f = subsref(a, substruct('()', {1})) * subsref(a, substruct('()', {2})); + charges = [repmat(subsref(a, substruct('()', {1})), 1, length(f)); f]; vertices = []; return end - part = cumprod(a(1:end-1)); + part = cumprod(subsref(a, substruct('()', {1:length(a)-1}))); charges = []; for i = 1:size(part, 2) - f = part(end, i) * a(end); - charges = [charges [repmat(part(:, i), 1, length(f)); f]]; + f = subsref(part, substruct('()', {size(part, 1), i})) * ... + subsref(a, substruct('()', {length(a)})); + charges = [charges ... + [repmat(subsref(part, substruct('()', {1:size(part, 1), i})), ... + 1, length(f)); f]]; end vertices = []; return @@ -630,9 +638,10 @@ if length(a) == 2 charges = []; vertices = []; - for f = a(1) * a(2) - N = Nsymbol(a(1), a(2), f); - charges = [charges repmat([a(1); f], 1, N)]; + for f = subsref(a, substruct('()', {1})) * subsref(a, substruct('()', {2})) + N = Nsymbol(subsref(a, substruct('()', {1})), ... + subsref(a, substruct('()', {2})), f); + charges = [charges repmat([subsref(a, substruct('()', {1})); f], 1, N)]; vertices = [vertices 1:N]; end return @@ -642,9 +651,12 @@ charges = []; vertices = []; for i = 1:size(chargepart, 2) - for f = chargepart(end, i) * a(end) - N = Nsymbol(chargepart(end, i), a(end), f); - charges = [charges, repmat([chargepart(:, i); f], 1, N)]; + for f = subsref(chargepart, substruct('()', {size(chargepart, 1), i})) * ... + subsref(a, substruct('()', {length(a)})) + N = Nsymbol(subsref(chargepart, substruct('()', {size(chargepart, 1), i})), ... + subsref(a, substruct('()', {length(a)})), f); + charges = [charges, ... + repmat([subsref(chargepart, substruct('()', {':', i})); f], 1, N)]; vertices = [vertices [repmat(vertexpart(:, i), 1, N); 1:N]]; end end @@ -654,7 +666,7 @@ % Create all combinations of vectors. % % .. todo:: - % Complete docstring + % Complete docstring. % % Usage % ----- @@ -686,11 +698,59 @@ reshape(repmat(X{i}, size(Y, 2), 1), size(X{i}, 1), [])]; end end - end - - methods -% bool = issortedrows(A) -% bools = ne(A, B) -% [B, I] = sort(A, varargin) + + function [lia, locb] = ismember_sorted(a, b) + if isempty(a) || isempty(b) + lia = false(size(a)); + locb = zeros(size(a)); + return + end + + if isscalar(a) || isscalar(b) + if any(size(a) == numel(a)) && any(size(b) == numel(b)) + lia = a == b; + else + lia = reshape(a == b, [], 1); + end + + if ~any(lia) + lia = false(size(a)); + locb = zeros(size(a)); + return + end + if ~isscalar(b) + locb = find(lia); + locb = locb(1); + lia = any(lia); + else + locb = double(lia); + end + return + end + + [sortab, indsortab] = sort([reshape(a, [], 1); reshape(b, [], 1)]); + d = subsref(sortab, substruct('()', {1:length(sortab)-1})) == ... + subsref(sortab, substruct('()', {2:length(sortab)})); + ndx1 = indsortab(d); + + if nargout <= 1 + lia = ismember(1:length(a), ndx1); + else + szuA = length(a); + d = find(d); + [lia, locb] = ismember(1:szuA, ndx1); + newd = d(locb(lia)); + where = indsortab(newd+1) - szuA; + locb(lia) = where; + end + lia = reshape(lia, size(a)); + if nargout > 1 + locb = reshape(locb, size(a)); + end + end + + function s = name(a) + s = class(a); + end end end diff --git a/src/tensors/charges/BraidingStyle.m b/src/tensors/charges/BraidingStyle.m index 5aed581..6956ee4 100644 --- a/src/tensors/charges/BraidingStyle.m +++ b/src/tensors/charges/BraidingStyle.m @@ -1,12 +1,13 @@ classdef BraidingStyle - % BraidingStyle - The braiding behaviour of charges. - % This represents the possibilities for the braiding of two charges. + % The braiding behaviour of charges. % - % Abelian - Trivial braiding with trivial twist. - % Bosonic - Symmetric braiding with trivial twist. - % Fermionic - Symmetric braiding with non-trivial twist. - % Anyonic - Arbitrary braiding and twist. - % None - No braiding defined. + % Enumeration class that encodes the possibilities for the braiding of two charges: + % + % - :code:`Abelian`: Trivial braiding with trivial twist. + % - :code:`Bosonic`: Symmetric braiding with trivial twist. + % - :code:`Fermionic`: Symmetric braiding with non-trivial twist. + % - :code:`Anyonic`: Arbitrary braiding and twist. + % - :code:`None`: No braiding defined. enumeration Abelian @@ -18,15 +19,37 @@ methods function bool = istwistless(style) + % Determine whether a given braiding style has a trivial twist. + % + % Returns :code:`true` for :code:`BraidingStyle.Abelian` and + % :code:`BraidingStyle.Bosonic` and :code:`false` for all other styles. bool = style == BraidingStyle.Abelian || style == BraidingStyle.Bosonic; end function bool = issymmetric(style) + % Determine whether a given braiding style is symmetric. + % + % Returns :code:`true` for all styles except :code:`BraidingStyle.Anyonic` and + % :code:`BraidingStyle.None`. bool = style == BraidingStyle.Abelian || style == BraidingStyle.Bosonic || ... style == BraidingStyle.Fermionic; end function c = and(a, b) + % Determine the braiding style for a direct product of charges. This effectively + % boils down to returning the least specific style. + % + % Arguments + % --------- + % style1 : :class:`.BraidingStyle` + % fusion style of first charge in direct product + % style2 : :class:`.BraidingStyle` + % fusion style of second charge in direct product + % + % Returns + % ------- + % style : :class:`.BraidingStyle` + % fusion style of direct product charge if any([a b] == BraidingStyle.None) c = BraidingStyle.None; diff --git a/src/tensors/charges/FusionStyle.m b/src/tensors/charges/FusionStyle.m index d4cc419..2ec3b82 100644 --- a/src/tensors/charges/FusionStyle.m +++ b/src/tensors/charges/FusionStyle.m @@ -1,25 +1,43 @@ -classdef FusionStyle - % FusionStyle - The fusion product behaviour of charges. - % This represents the possibilities for the decomposition of the fusion product of two charges. +classdef FusionStyle < uint8 + % The fusion product behaviour of charges. % - % Unique - Single unique output. - % Simple - Multiple unique outputs. - % Generic - Multiple outputs. + % Enumeration class that encodes the possible behavior for the decomposition of the + % fusion product of two charges. + % + % - :code:`Unique`: Single unique output. + % - :code:`Simple`: Multiple unique outputs. + % - :code:`Generic`: Multiple outputs. enumeration - Unique - Simple - Generic + Unique (0) + Simple (1) + Generic (2) end methods function bool = hasmultiplicity(style) + % Determine whether a given fusionstyle admits fusion multiplicities. + % + % Returns :code:`true` for :code:`FusionStyle.Generic` and :code:`false` for all + % other styles. bool = style == FusionStyle.Generic; end function style = and(style1, style2) - % Determine the fusionstyle for a direct product of charges. This effectively + % Determine the fusion style for a direct product of charges. This effectively % boils down to returning the least specific style. + % + % Arguments + % --------- + % style1 : :class:`.FusionStyle` + % fusion style of first charge in direct product + % style2 : :class:`.FusionStyle` + % fusion style of second charge in direct product + % + % Returns + % ------- + % style : :class:`.FusionStyle` + % fusion style of direct product charge if style1 == FusionStyle.Generic || style2 == FusionStyle.Generic style = FusionStyle.Generic; diff --git a/src/tensors/charges/GtPattern.m b/src/tensors/charges/GtPattern.m index a629f73..537fbe5 100644 --- a/src/tensors/charges/GtPattern.m +++ b/src/tensors/charges/GtPattern.m @@ -1,22 +1,27 @@ classdef GtPattern - %GTPATTERN Object that represents a pattern devised by Gelfand and Tsetlin. + % Object that represents a pattern devised by Gelfand and Tsetlin, used for enumerating + % :math:`\mathrm{SU}(N)` basis states. % - % /m_{1,N} m_{2,N} ... m_{N,N}\ - % | m_{1,N-1} ... m_{N-1,N-1} | - % M = | ... | - % | m_{1,2} m_{2,2} | - % \ m_{1,1} / + % :: % - % These consist of triangular arrangements of integers M_ij with the - % following property: - % - for 1 <= k < l <= N: - % m_{k,l} >= m_{k,l-1} >= m_{k+1,l} + % /m_{1,N} m_{2,N} ... m_{N,N}\ + % | m_{1,N-1} ... m_{N-1,N-1} | + % M = | ... | + % | m_{1,2} m_{2,2} | + % \ m_{1,1} / % - % They will be represented using arrays of size N x N, where the elements - % outside of the triangular region are assumed to be zero. + % These consist of triangular arrangements of integers :math:`M_{ij}` with the + % following property: + % + % .. math:: + % + % m_{k,l} \geq m_{k,l-1} \geq m_{k+1,l} \quad \text{for} \quad 1 <= k < l <= N + % + % They will be represented using arrays of size :math:`N \times N`, where the elements + % outside of the triangular region are assumed to be zero. - properties % (Access = private) + properties M double N end @@ -106,20 +111,26 @@ end function m = get(p, k, l) - %GET Access an element in the pattern. - % m = get(pat, k, l) - % gets the element specified by m_kl. + % Access an element in the pattern. % - % m = get(pat, ks, l) - % gets a row vector of elements in the l'th row. + % Usage + % ----- + % :code:`m = get(pat, k, l)` + % gets the element specified by :math:`M_{kl}`. % - % m = get(pat, k, ls(:) - % gets a col vector of elements in the k'th column. + % :code:`m = get(pat, ks, l)` + % gets a row vector of elements in the :code:`l`'th row. + % + % :code:`m = get(pat, k, ls)` + % gets a column vector of elements in the :code:`k`'th column. + % assert(isscalar(p)); m = p.M(k + bitshift(((l + 1 + p.N) .* (p.N - l)), -1)); end function p = set(p, k, l, val) + % Set the value of :math:`M_{kl}` to :code:`val`. + % assert(isscalar(p)); assert(checkbounds(p, k, l)); p.M(k + bitshift(((l + 1 + p.N) * (p.N - l)), -1)) = val; @@ -201,21 +212,32 @@ function disp(p) end function sigma = rowsum(p, l) - %ROWSUM Compute the sum of row `l`. - % sigma = rowsum(pat, l) - % computes sigma_l = \sum_{k=1:l} m_{k, l} + % Compute the sum of row :code:`l`. + % + % Usage + % ----- + % :code:`sigma = rowsum(pat, l)` + % computes :math:`\sigma_l = \sum_{k=1}^l m_{k, l}`. sigma = sum(get(p, 1:l, l)); end function w = pWeight(p) - %PWEIGHT Compute the p-weight of a pattern. - % w = pWeight(pat) - % computes the pattern weight W(pat) = (w_1 w_2 ... w_N) where - % w_i = sigma_i - sigma_{i-1}. + % Compute the p-weight of a pattern. + % + % Usage + % ----- + % :code:`w = pWeight(pat)` + % computes the pattern weight :math:`W(\text{pat}) = (w_1 w_2 ... w_N)` where + % :math:`w_i = \sigma_i - \sigma_{i-1}`. % - % This is an alternative weight definition to the z-weight. - % See also GtPattern.zWeight + % Note + % ---- + % This is an alternative weight definition to the z-weight. + % + % See Also + % -------- + % :meth:`.GtPattern.zWeight` M = p.M; ctr = 0; @@ -230,13 +252,17 @@ function disp(p) end function w = zWeight(p) - %ZWEIGHT Compute the z-weight of a pattern - % w = zWeight(pat) - % computes the z-weight L(pat) = (l_1 l_2 ... l_N-1) where - % l_i = sigma_l - 1/2(sigma_{l+1} + sigma_{l-1}) + % Compute the z-weight of a pattern + % + % Usage + % ----- + % :code:`w = zWeight(pat)` + % computes the z-weight :math:`L(\text{pat}) = (l_1 l_2 ... l_N-1)` where + % :math:`l_i = \sigma_l - 1/2(\sigma_{l+1} + \sigma_{l-1})`. % - % This is a generalization of the m-quantum number for angular - % momentum. + % Note + % ---- + % This is a generalization of the :math:`m`-quantum number for angular momentum. sigma = [0 arrayfun(@(l) rowsum(p, l), 1:p.N)]; w = sigma(2:end-1) - (sigma(1:end-2) + sigma(3:end))/2; @@ -244,61 +270,6 @@ function disp(p) end - - methods (Static) - function pat = GeneratePatterns(I) - %GENERATEPATTERNS Generate all possible GtPatterns for a given SU(N) charge. - % pat = GeneratePatterns(charge) - % generates all allowed GtPatterns. - - - %% Special case length 1 - N = length(I); - if N == 1 - pat = GtPattern(1, I(:)); - return - end - - - %% Special case length 2 - if N == 2 - i1 = I(2):I(1); - L = length(i1); - M = zeros(2, 2, L); - M(1, :, :) = repmat(I, 1, 1, L); - M(2, 1, :) = i1; - pat = GtPattern(2, M); - return - end - - - %% Special case length 3 - % if N == 3 - % - % - % - % return; - % end - - - %% Generate all possible weight combinations of one level lower - newIs = cell(1, N-1); - for ii = 1:N-1 - newIs{ii} = I(ii+1):I(ii); - end - - - %% Generate patterns - pat = []; - - for newI = CombVec(newIs{N-1:-1:1}) - newPat = GtPattern.GeneratePatterns(flip(newI)); - pat = [pat, appendRow(newPat, I)]; - end - - - end - end end diff --git a/src/tensors/charges/O2.m b/src/tensors/charges/O2.m index 854bb8d..9595439 100644 --- a/src/tensors/charges/O2.m +++ b/src/tensors/charges/O2.m @@ -1,16 +1,25 @@ classdef O2 < AbstractCharge - % O2 - Irreducible representations of O(2). - % This class represents the representations of O(2), also known as the - % semi-direct product of U(1) and charge conjugation. + % Irreducible representations of :math:`\mathrm{O}(2)`. % - % The representations are labeled using (j, s), indicating the behaviour - % of U(1) and charge conjugation respectively. This leads to two - % 1-dimensional representations (0, 0) and (0, 1), and for any - % non-negative j a two-dimensional representation (j, 2). + % This class represents the representations of :math:`\mathrm{O}(2)`, also known as the + % semi-direct product of :math:`\mathrm{U}(1)` and charge conjugation. + % + % The representations are labeled using :math:`(j, s)`, indicating the behaviour + % of :math:`\mathrm{U}(1)` and charge conjugation respectively. This leads to two + % 1-dimensional representations :math:`(0, 0)` and :math:`(0, 1)`, and for any + % non-negative :math:`j` a two-dimensional representation :math:`(j, 2)`. + % + % Properties + % ---------- + % j : :class:`uint8` + % :math:`\mathrm{U}(1)` label + % + % s : :class:`uint8` + % indicator for type of representation properties - j uint8 % (uint8) U1 label. - s uint8 % (uint8) indicator for type of representation. + j uint8 + s uint8 end methods @@ -221,10 +230,18 @@ end end + function s = GetMD5_helper(data) + s = [data.j] + [data.s]; + end + function bool = issortedrows(A) bool = issortedrows(reshape([A.j] + [A.s], size(A))); end + function bool = issorted(A) + bool = issorted(reshape([A.j] + [A.s], size(A))); + end + function c = mtimes(a, b) assert(isscalar(a) && isscalar(b), ... 'Simple fusion cannot be vectorised.'); diff --git a/src/tensors/charges/ProductCharge.m b/src/tensors/charges/ProductCharge.m index 9eed5b8..e44c4c5 100644 --- a/src/tensors/charges/ProductCharge.m +++ b/src/tensors/charges/ProductCharge.m @@ -11,14 +11,19 @@ % % Usage % ----- - % charges = ProductCharge(charges1, charges2, ...) - % creates an (array of) charges that are representations of the direct product - % group group1 x group2 x ... + % :code:`charges = ProductCharge(charges1, charges2, ...)` creates an (array of) + % charges that are representations of the direct product group + % :math:`G_1 \otimes G_2 \otimes \dots`. % % Arguments % --------- - % charges1, charges2, ... : AbstractCharge - % charges of the separate groups. + % charges1, charges2, ... : :class:`.AbstractCharge` + % charges of the separate groups + % + % Returns + % ------- + % prodcharge : :class:`.ProductCharge` + % resulting product charge arguments (Repeating) charges @@ -35,18 +40,21 @@ prodcharge.charges = charges; end - function a = cat(dim, a, varargin) - % Concatenate charges. + function a = cat(dim, varargin) + % Concatenate charges along a given axis. + mask = cellfun(@isempty, varargin); + firstnonempty = find(~mask, 1); + if isempty(firstnonempty) + a = varargin{1}; + return + end + + a = varargin{firstnonempty}; + mask(firstnonempty) = true; - for i = 1:length(varargin) - if isempty(varargin{i}), continue; end - if isempty(a) - a = varargin{i}; - continue; - end - for j = 1:length(a.charges) - a.charges{j} = cat(dim, a.charges{j}, varargin{i}.charges{j}); - end + for i = 1:length(a.charges) + catcharges = cellfun(@(x) x.charges{i}, varargin(~mask), 'UniformOutput', false); + a.charges{i} = cat(dim, a.charges{i}, catcharges{:}); end end @@ -58,6 +66,10 @@ a = cat(1, a, varargin{:}); end + function s = GetMD5_helper(a) + s = cellfun(@GetMD5_helper, a.charges, 'UniformOutput', false); + end + function varargout = size(prodcharge, varargin) [varargout{1:nargout}] = size(prodcharge.charges{1}, varargin{:}); end @@ -89,7 +101,21 @@ end function [d, N] = prod(a, dim) - % Total fusion product of charges. + % Compute the total fusion product of array of charges along a given axis. + % + % Arguments + % --------- + % a : :class:`.ProductCharge` + % input array of charges + % dim : :class:`int` + % array dimension along which to take the fusion product, defaults to first + % non-trivial axis + % + % Returns + % ------- + % c : :class:`.ProductCharge` + % array of total fusion products determined by subsequently multiplying input + % charges along the given direction arguments a @@ -108,62 +134,119 @@ N = ones(size(d)); end - otherwise - if nargout > 1 - [d, N] = prod@AbstractCharge(a, dim); - else - d = prod@AbstractCharge(a, dim); + case FusionStyle.Simple + d = subsref(a, substruct('()', {1})); + N = 1; + for i = 2:length(a) + a_i = subsref(a, substruct('()', {i})); + d_ = subsref(d, substruct('()', {1})) * ... + a_i; + if nargout > 1 + N_ = repmat(N(1), 1, length(d_)); + end + + for j = 2:length(d) + c = subsref(d, substruct('()', {j})) * ... + a_i; + d_ = [d_ c]; + if nargout > 1 + N_(end + (1:length(c))) = N(j); + end + end + + if nargout < 2 + d = unique(d_); + else + [d, ~, ic] = unique(d_); + N = zeros(size(d)); + for i = 1:length(N) + N(i) = sum(N_(ic == i)); + end + end + end + + case FusionStyle.Generic + error('TBA'); + d = a(1); + N = 1; + for i = 2:length(a) + d_ = d(1) * a(i); + if nargout > 1 + N_ = N(1) .* ... + Nsymbol(repmat(d(1), 1, length(d_)), ... + repmat(a(i), 1, length(d_)), d_); + end + + for j = 2:length(d) + c = d(j) * a(i); + d_(end + (1:length(c))) = c; + if nargout > 1 + N_(end + (1:length(c))) = N(j) .* ... + Nsymbol(repmat(d(j), 1, length(c)), ... + repmat(a(i), 1, length(c)), c); + end + end + + if nargout < 2 + d = unique(d_); + else + [d, ~, ic] = unique(d_); + N = zeros(size(d)); + for i = 1:length(N) + N(i) = sum(N_(ic == i)); + end + end end end end - function varargout = subsref(prodcharge, s) + function varargout = subsref(prodcharges, s) % Overload indexing. % % Usage % ----- - % charges_slice = charges(i1, i2, ...) - % extracts elements out of the charge array. + % :code:`charges_slice = prodcharges(i1, i2, ...)` + % extracts elements out of the charge array. % - % product_slice = charges{i} - % separate out the direct product factors. + % :code:`product_slice = prodcharges{i}` + % separates out the direct product factors. % % Arguments % --------- - % charges : ProductCharge - % array of charges. + % prodcharges : :class:`.ProductCharge` + % array of product charges % - % s : substruct - % structure containing indexing data. + % s : :class:`substruct` + % structure containing indexing data % % Returns % ------- - % charges_slice : ProductCharge - % sliced array of product charges. + % charges_slice : :class:`.ProductCharge` + % sliced array of product charges % - % product_slice : AbstractCharge - % array of factor charges. + % product_slice : :class:`.AbstractCharge` + % array of factor charges switch s(1).type case '.' - [varargout{1:nargout}] = builtin('subsref', prodcharge, s); + [varargout{1:nargout}] = builtin('subsref', prodcharges, s); case '()' - for i = 1:numel(prodcharge.charges) - prodcharge.charges{i} = prodcharge.charges{i}(s(1).subs{:}); + for i = 1:numel(prodcharges.charges) + prodcharges.charges{i} = prodcharges.charges{i}(s(1).subs{:}); end if length(s) == 1 - varargout = {prodcharge}; + varargout = {prodcharges}; return end - [varargout{1:nargout}] = subsref(prodcharge, s(2:end)); + [varargout{1:nargout}] = subsref(prodcharges, s(2:end)); case '{}' assert(length(s) == 1); assert(length(s(1).subs) == 1); assert(nargout == length(s(1).subs{1})); - varargout(1:nargout) = prodcharge.charges(s(1).subs{:}); + varargout(1:nargout) = prodcharges.charges(s(1).subs{:}); otherwise error('Undefined behaviour'); @@ -175,31 +258,36 @@ % % Usage % ----- - % a = subsasgn(a, substruct('()', subs), b) - % a(subs{:}) = b - % assign array slices. % - % a = subsasgn(a, substruct('{}', subs), c) - % a{i} = c - % assign to a factor slice. + % :code:`a = subsasgn(a, substruct('()', subs), b)` + % + % :code:`a(subs{:}) = b` + % + % Assign array slices. + % + % :code:`a = subsasgn(a, substruct('{}', subs), c)` + % + % :code:`a{i} = c` + % + % Assign to a factor slice. % % Arguments % --------- - % a : ProductCharge + % a : :class:`.ProductCharge` % array of charges to assign to. % - % s : substruct + % s : :class:`struct` % structure containing indexing data. % - % b : ProductCharge + % b : :class:`.ProductCharge` % slice to assign. % - % c : AbstractCharge + % c : :class:`.AbstractCharge` % factor to assign. % % Returns % ------- - % a : ProductCharge + % a : :class:`.ProductCharge` % assigned array switch s(1).type @@ -291,6 +379,13 @@ end end + function [c, ia, ib] = intersect(a, b) + c = subsref(a, substruct('()', ... + {mod(find(reshape(a, [], 1) == reshape(b, 1, [])) - 1, length(a)) + 1})); + [~,ia] = ismember(c, a); + [~,ib] = ismember(c, b); + end + function F = Fsymbol(a, b, c, d, e, f) if hasmultiplicity(fusionstyle(a)) error('Not implemented yet.'); @@ -304,6 +399,57 @@ end end + function F = Fmatrix(a, b, c, d, e, f) + % Compute the full recoupling matrix from ``e`` to ``f``. + % + % See also + % -------- + % :meth:`.AbstractCharge.Fmatrix` + if a.fusionstyle == FusionStyle.Unique + if nargin < 5, e = a * b; end + if nargin < 6, f = b * c; end + F = Fsymbol(a, b, c, d, e, f); + return + end + + if nargin < 5, e = intersect(a * b, conj(c * conj(d))); end + if nargin < 6, f = intersect(b * c, conj(conj(d) * a)); end + + if hasmultiplicity(a.fusionstyle) + Fblocks = cell(length(f), length(e)); + for i = 1:length(e) + for j = 1:length(f) + Fblocks{j, i} = Fsymbol(a, b, c, d, ... + subsref(e, substruct('()', {i})), ... + subsref(f, substruct('()', {j}))); + sz = size(Fblocks{j, i}, 1:4); + Fblocks{j, i} = reshape(Fblocks{j, i}, ... + sz(1) * sz(2), sz(3) * sz(4)).'; + end + end + F = cell2mat(Fblocks); + return + end + + F = zeros(length(f), length(e)); + for i = 1:length(e) + for j = 1:length(f) + F(j, i) = Fsymbol(a, b, c, d, ... + subsref(e, substruct('()', {i})), ... + subsref(f, substruct('()', {j}))); + end + end + end + + function disp(a) + s = string(a); + fprintf('\t%s (%s) Array:\n', ... + dim2str(size(a)), name(a)); + for i = 1:size(a, 1) + fprintf('\t\t%s\n', join(s(i, :), ' ')); + end + end + function N = Nsymbol(a, b, c) N = Nsymbol(a.charges{1}, b.charges{1}, c.charges{1}); for i = 2:length(a.charges) @@ -323,17 +469,21 @@ assert(isscalar(a) && isscalar(b)) charges = cell(size(a.charges)); - charges{1} = a.charges{1} * b.charges{1}; + ctr = 1; for i = 1:length(charges) charges{i} = a.charges{i} * b.charges{i}; + n = numel(charges{i}); + charges{i} = reshape(repmat(reshape(charges{i}, 1, []), ctr, 1), 1, []); + for j = 1:i-1 + charges{j} = repmat(charges{j}, 1, n); + end + ctr = ctr * n; end - - for i = 2:length(charges) - n1 = length(charges{i-1}); - n2 = length(charges{i}); - charges{i-1} = repmat(charges{i}, 1, n2); - charges{i} = reshape(repmat(charges{i}, n1, 1), 1, []); - end + c = ProductCharge(charges{:}); + end + + function s = name(a) + s = join(cellfun(@(x)string(class(x)), a.charges), 'x'); end function bool = ne(a, b) @@ -343,6 +493,13 @@ end end + function d = qdim(a) + d = qdim(a.charges{1}); + for i = 2:length(a.charges) + d = d .* qdim(a.charges{i}); + end + end + function R = Rsymbol(a, b, c, inv) if nargin < 4, inv = []; end if hasmultiplicity(fusionstyle(a)) @@ -355,6 +512,23 @@ end end + function theta = twist(a) + theta = twist(a.charges{1}); + for i = 2:length(a.charges) + theta = theta .* twist(a.charges{i}); + end + end + + function bool = issorted(a) + [a, p] = sort(a); + bool = isequal(p, (1:length(a)).'); + end + + function bool = issortedrows(a) + [~, p] = sortrows(a); + bool = isequal(p, (1:size(a, 1)).'); + end + function [a, I] = sort(a, varargin) [I, a.charges{:}] = simulsort(a.charges{:}, varargin{:}); end @@ -362,25 +536,20 @@ function [a, I] = sortrows(a, col, direction) arguments a - col (1,:) double = [] + col (1,:) double = 1:size(a, 2) direction = 'ascend' end - if nargin == 1 || isempty(col) || isequal(col, 1:size(a, 2)) - [I, a.charges{:}] = simulsortrows(a.charges{:}, ... - 'Direction', direction); - return - end - - newcol = size(a.charges, 2) * (col(:) - 1) + (1:size(a.charges, 2)); + newcol = reshape(col + (((1:length(a.charges)).' - 1) .* size(a, 2)), 1, []); [I, a.charges{:}] = simulsortrows(a.charges{:}, ... - 'Col', reshape(newcol, 1, []), ... + 'Col', newcol, ... 'Direction', direction); end function s = string(a) charge_str = cellfun(@string, a.charges, 'UniformOutput', false); s = join(cat(ndims(a) + 1, charge_str{:}), ' x ', ndims(a) + 1); + s = compose("(%s)", s); end function a = one(a) diff --git a/src/tensors/charges/SU2.m b/src/tensors/charges/SU2.m index 645721c..5727c8d 100644 --- a/src/tensors/charges/SU2.m +++ b/src/tensors/charges/SU2.m @@ -1,10 +1,10 @@ classdef SU2 < AbstractCharge & uint8 - % SU2 - Irreducible representations of SU2. - % This class represents the representations of SU2, represented using uint8, such that - % the representative is equal to the quantum dimension. The use of uint8 limits the - % maximum to 255. The spin label can be recovered with spin = (j - 1) / 2. + % Irreducible representations of :math:`\mathrm{SU}(2)`. % - % See also AbstractCharge + % This class represents the representations of :math:`\mathrm{SU}(2)`, represented using + % :code:`uint8`, such that the representative is equal to the quantum dimension. The use + % of :math:`uint8` limits the maximum dimension to 255. The spin label can be recovered + % as :math:`s = (j - 1) / 2`. methods function style = braidingstyle(~) @@ -97,9 +97,9 @@ function varargout = prod(varargin) [varargout{1:nargout}] = prod@AbstractCharge(varargin{:}); - if nargout > 0 - varargout{1} = SU2(varargout{1}); - end +% if nargout > 0 +% varargout{1} = SU2(varargout{1}); +% end end function d = qdim(a) @@ -129,5 +129,9 @@ end charge@uint8(labels); end + + function s = GetMD5_helper(a) + s = uint8(a); + end end end diff --git a/src/tensors/charges/SUN.m b/src/tensors/charges/SUN.m index 6e888ab..4fbae36 100644 --- a/src/tensors/charges/SUN.m +++ b/src/tensors/charges/SUN.m @@ -1,5 +1,13 @@ classdef SUN < AbstractCharge - % Irreducible representations of the special unitary group SU(N). + % Irreducible representations of the special unitary group :math:`\mathrm{SU}(N)`. + % + % .. todo:: + % Explain irrep labeling and give some references. + % + % Properties + % ---------- + % I : (1, :) :class:`uint8` + % integer vector representation label properties I (1,:) uint8 diff --git a/src/tensors/charges/U1.m b/src/tensors/charges/U1.m index b8b77dc..e09758e 100644 --- a/src/tensors/charges/U1.m +++ b/src/tensors/charges/U1.m @@ -1,8 +1,8 @@ classdef U1 < AbstractCharge & int16 - % U1 - Irreducible representations of U(1). - % This class represents the representations of U(1), labeled using integers. + % Irreducible representations of :math:`\mathrm{U}(1)`. % - % See also AbstractCharge + % This class represents the representations of :math:`\mathrm{U}(1)`, labeled using + % integers where fusion is given by addition. methods function charge = U1(varargin) @@ -44,7 +44,6 @@ Nsymbol(e, c, d) .* Nsymbol(a, f, d)); end - function style = fusionstyle(~) style = FusionStyle.Unique; end diff --git a/src/tensors/charges/Z1.m b/src/tensors/charges/Z1.m index 6c28e1d..930cd7e 100644 --- a/src/tensors/charges/Z1.m +++ b/src/tensors/charges/Z1.m @@ -1,5 +1,5 @@ classdef Z1 < AbstractCharge - % Trivial charges. + % Trivial charge. methods function A = Asymbol(~, ~, ~) @@ -33,6 +33,10 @@ C = 1; end + function s = GetMD5_helper(data) + s = uint8(0); + end + function a = intersect(a, ~) end @@ -40,6 +44,10 @@ bool = true; end + function bool = issorted(~) + bool = true; + end + function a = mtimes(a, ~), end function bools = ne(A, B) diff --git a/src/tensors/charges/Z2.m b/src/tensors/charges/Z2.m index 58a9376..2da8e96 100644 --- a/src/tensors/charges/Z2.m +++ b/src/tensors/charges/Z2.m @@ -1,13 +1,22 @@ classdef Z2 < AbstractCharge & logical - % Z2 - Irreducible representations of Z2. - % This class represents the trivial or sign representations of Z2, represented using - % {false, true} with multiplication table given by xor: - % | false | true - % --------------------- - % false | false | true - % true | true | false + % Irreducible representations of :math:`Z_2`. + % + % This class implements the trivial or sign representations of :math:`Z_2`, represented + % using {:code:`false`, :code:`true`} where fusion is given by :math:`\mathrm{XOR}`, + % giving the multiplication table: + % + % .. list-table:: + % + % * - + % - :code:`false` + % - :code:`true` + % * - :code:`false` + % - :code:`false` + % - :code:`true` + % * - :code:`true` + % - :code:`true` + % - :code:`false` % - % See also AbstractCharge methods function A = Asymbol(a, b, c) diff --git a/src/tensors/charges/ZN.m b/src/tensors/charges/ZN.m index 88daf6d..43cbfbe 100644 --- a/src/tensors/charges/ZN.m +++ b/src/tensors/charges/ZN.m @@ -1,8 +1,14 @@ classdef ZN < AbstractCharge & uint8 - % ZN - Irreducible representations of ZN. - % This class represents representations of the cyclic group of order N. - % - % See also AbstractCharge, Z2, Z3, Z4 + % Irreducible representations of :math:`Z_N`. + % + % This class implements the representations of the cyclic group of order :math:`N`, + % represented using unteger labels where fusion is given by addition modulo + % :math:`N`. + % + % Properties + % ---------- + % N : :class:`uint8` + % integer representation label properties N (1,1) uint8 = uint8(1) diff --git a/src/tensors/charges/fSU2.m b/src/tensors/charges/fSU2.m new file mode 100644 index 0000000..c2398d8 --- /dev/null +++ b/src/tensors/charges/fSU2.m @@ -0,0 +1,35 @@ +classdef fSU2 < SU2 + % Fermionic :math:`\mathrm{SU}(2)` charges, used to represent fermionspin. + % + % This is equivalent to representations of :math:`\mathrm{SU}(2) \otimes fZ_2`, but + % restricted to only allow for the combinations of integer with trivial and halfinteger + % with fermion charges. + + methods + function style = braidingstyle(~) + style = BraidingStyle.Fermionic; + end + + function c = mtimes(a, b) + c = fSU2(mtimes@SU2(a, b)); + end + + function e = one(~) + e = fSU2(ones(1, 1, 'uint8')); + end + + function p = parity(a) + p = ~mod(a, 2); + end + + function R = Rsymbol(a, b, c, inv) + if nargin < 4, inv = false; end + R = (-2 .* double(parity(a) & parity(b)) + 1) .* Rsymbol@SU2(a, b, c, inv); + end + + function theta = twist(a) + theta = -2 * double(parity(a)) + 1; + end + end +end + diff --git a/src/tensors/charges/fU1.m b/src/tensors/charges/fU1.m new file mode 100644 index 0000000..fc2a9b5 --- /dev/null +++ b/src/tensors/charges/fU1.m @@ -0,0 +1,49 @@ +classdef fU1 < U1 + % Fermionic :math:`\mathrm{U}(1)` charges. + % + % This is equivalent to representations of :math:`\mathrm{U}(1) \otimes fZ_2`, but + % restricted to only allow for the combinations of even with trivial and odd with + % fermion charges. + + methods + function style = braidingstyle(~) + style = BraidingStyle.Fermionic; + end + + function a = conj(a) + a = fU1(conj@U1(a)); + end + + function varargout = cumprod(a) + [varargout{1:nargout}] = cumprod@U1(a); + varargout{1} = fU1(varargout{1}); + end + + function c = mtimes(a, b) + c = fU1(mtimes@U1(a, b)); + end + + function e = one(a) + e = fU1(one@U1(a)); + end + + function p = parity(a) + p = logical(mod(a, 2)); + end + + function varargout = prod(varargin) + [varargout{1:nargout}] = prod@U1(varargin{:}); + varargout{1} = fU1(varargout{1}); + end + + function R = Rsymbol(a, b, c, inv) + if nargin < 4, inv = false; end + R = (-2 .* double(parity(a) & parity(b)) + 1) .* Rsymbol@U1(a, b, c, inv); + end + + function theta = twist(a) + theta = -2 * double(parity(a)) + 1; + end + end +end + diff --git a/src/tensors/charges/fZ2.m b/src/tensors/charges/fZ2.m index 901acff..f6955b0 100644 --- a/src/tensors/charges/fZ2.m +++ b/src/tensors/charges/fZ2.m @@ -1,7 +1,5 @@ classdef fZ2 < Z2 - % Fermionic charges. - % - % See also AbstractCharge + % Fermionic charges, implemented as a graded :math:`Z_2` symmetry. methods function style = braidingstyle(~) diff --git a/src/tensors/kernels/AbelianBlock.m b/src/tensors/kernels/AbelianBlock.m index 509a6e4..ca595c7 100644 --- a/src/tensors/kernels/AbelianBlock.m +++ b/src/tensors/kernels/AbelianBlock.m @@ -1,7 +1,9 @@ classdef AbelianBlock < MatrixBlock - %ABELIANBLOCK Summary of this class goes here - % Detailed explanation goes here - + % Structure for storing symmetric tensor data for an Abelian symmetry. + % + % This represents the blocks in the block-diagonal decomposition of a tensor defined + % over graded vector spaces corresponding to an Abelian symmetry, allowing for a more + % efficient multiplication. methods function Y = axpby(a, X, b, Y, p, map) @@ -10,7 +12,7 @@ if a == 0 || ... nargin == 4 || ... (isempty(p) && isempty(map)) || ... - (all(p == 1:length(p)) && all(X(1).rank == Y(1).rank)) + (all(p == 1:length(p)) && all(X.rank == Y.rank)) Y = axpby@MatrixBlock(a, X, b, Y); return; @@ -19,31 +21,38 @@ %% General case: addition with permutation % tensor indexing to matrix indexing - rrx = rankrange(X(1).rank); - rry = rankrange(Y(1).rank); + rx = X.rank; + ry = Y.rank; + rrx = rankrange(rx); + rry = rankrange(ry); p_eff(rry) = rrx(p); + p_eff_1 = p_eff(1:ry(1)); + p_eff_2 = p_eff((1:ry(2)) + ry(1)); % extract small tensor blocks doA = a ~= 1; - vars = cell(size(map, 1), 1); + vars_in = cell(size(map, 1), 1); offset = 0; - for i = 1:length(X) - rowsz = X(i).rowsizes; - colsz = X(i).colsizes; + + Xrowsizes = X.rowsizes; + Xcolsizes = X.colsizes; + Xtdims = X.tdims; + Xvar = X.var; + for i = 1:length(Xvar) + rowsz = Xrowsizes{i}; + colsz = Xcolsizes{i}; - var_in = X(i).var; + var_in = Xvar{i}; if doA, var_in = a .* var_in; end - oldtdims = X(i).tdims(:, rrx); - newmdims = [prod(X(i).tdims(:, p_eff(1:Y(1).rank(1))), 2) ... - prod(X(i).tdims(:, p_eff((1:Y(1).rank(2)) + Y(1).rank(1))), 2)]; + oldtdims = Xtdims{i}(:, rrx); + newmdims = [prod(oldtdims(:, p_eff_1), 2) prod(oldtdims(:, p_eff_2), 2)]; for k = 1:length(colsz) - 1 for j = 1:length(rowsz) - 1 ind = j + (k-1) * (length(rowsz) - 1); offset = offset + 1; - - vars{offset} = reshape(permute(reshape(... + vars_in{offset} = reshape(permute(reshape(... var_in(rowsz(j)+1:rowsz(j+1), colsz(k)+1:colsz(k+1)), ... oldtdims(ind, :)), ... p_eff), ... @@ -53,27 +62,30 @@ end % apply map - [row, col] = find(map); - vars(col) = vars(row); + vars_out = cell(size(map, 2), 1); + [rows, cols, vals] = find(map); + for i = 1:length(vals) + vars_out{cols(i)} = vals(i) * vars_in{rows(i)}; + end % inject small tensor blocks if b == 0 offset = 0; - for i = 1:length(Y) - rows = length(Y(i).rowsizes) - 1; - cols = length(Y(i).colsizes) - 1; + for i = 1:length(Y.var) + rows = length(Y.rowsizes{i}) - 1; + cols = length(Y.colsizes{i}) - 1; if rows < cols m = cell(rows, 1); for n = 1:rows - m{n} = cat(2, vars{offset + n + ((1:cols)-1) * rows}); + m{n} = cat(2, vars_out{offset + n + ((1:cols)-1) * rows}); end - Y(i).var = cat(1, m{:}); + Y.var{i} = cat(1, m{:}); else m = cell(cols, 1); for n = 1:cols - m{n} = cat(1, vars{offset + (n-1) * rows + (1:rows)}); + m{n} = cat(1, vars_out{offset + (n-1) * rows + (1:rows)}); end - Y(i).var = cat(2, m{:}); + Y.var{i} = cat(2, m{:}); end offset = offset + rows * cols; end @@ -82,21 +94,21 @@ if b == 1 offset = 0; - for i = 1:length(Y) + for i = 1:length(Y.var) rows = length(Y(i).rowsizes) - 1; cols = length(Y(i).colsizes) - 1; if rows < cols m = cell(rows, 1); for n = 1:rows - m{n} = cat(2, vars{offset + n + ((1:cols)-1) * rows}); + m{n} = cat(2, vars_out{offset + n + ((1:cols)-1) * rows}); end - Y(i).var = Y(i).var + cat(1, m{:}); + Y.var{i} = Y.var{i} + cat(1, m{:}); else m = cell(cols, 1); for n = 1:cols - m{n} = cat(1, vars{offset + (n-1) * rows + (1:rows)}); + m{n} = cat(1, vars_out{offset + (n-1) * rows + (1:rows)}); end - Y(i).var = Y(i).var + cat(2, m{:}); + Y.var{i} = Y.var{i} + cat(2, m{:}); end offset = offset + rows * cols; end @@ -104,138 +116,28 @@ end offset = 0; - for i = 1:length(Y) - rows = length(Y(i).rowsizes) - 1; - cols = length(Y(i).colsizes) - 1; + for i = 1:length(Y.var) + rows = length(Y.rowsizes{i}) - 1; + cols = length(Y.colsizes{i}) - 1; if rows < cols m = cell(rows, 1); for n = 1:rows - m{n} = cat(2, vars{offset + n + ((1:cols)-1) * rows}); + m{n} = cat(2, vars_out{offset + n + ((1:cols)-1) * rows}); end - Y(i).var = b .* Y(i).var + cat(1, m{:}); + Y.var{i} = b .* Y.var{i} + cat(1, m{:}); else m = cell(cols, 1); for n = 1:cols - m{n} = cat(1, vars{offset + (n-1) * rows + (1:rows)}); + m{n} = cat(1, vars_out{offset + (n-1) * rows + (1:rows)}); end - Y(i).var = b .* Y(i).var + cat(2, m{:}); + Y.var{i} = b .* Y.var{i} + cat(2, m{:}); end offset = offset + rows * cols; end - -% rrx = rankrange(x(1).rank); -% rry = rankrange(y(1).rank); -% p_eff(rry) = rrx(p); - -% %% extract blocks -% doA = a ~= 1; -% vars = cell(1, size(map, 1)); -% offset = 0; -% for block = x -% rowsz = block.rowsizes; -% colsz = block.colsizes; -% -% if doA -% varA = a .* block.var; -% else -% varA = block.var; -% end -% -% olddims = block.tdims(:, rrx); -% newdims = [prod(olddims(:, p_eff(1:y(1).rank(1))), 2) ... -% prod(olddims(:, p_eff(y(1).rank(1) + (1:y(1).rank(2)))), 2)]; -% for k = 1:length(colsz) - 1 -% for j = 1:length(rowsz) - 1 -% ind = j + (k-1) * (length(rowsz) - 1); -% offset = offset + 1; -% % vars{offset} = varA(rowsz(j)+1:rowsz(j+1), colsz(k)+1:colsz(k+1)); -% % vars{offset} = reshape(vars{offset}, olddims(ind, :)); -% % vars{offset} = permute(vars{offset}, p_eff); -% % vars{offset} = reshape(vars{offset}, newdims(ind, :)); -% -% % less readible but faster -% vars{offset} = reshape(permute(reshape(... -% varA(rowsz(j)+1:rowsz(j+1), colsz(k)+1:colsz(k+1)), ... -% olddims(ind, :)), p_eff), ... -% newdims(ind, :)); -% end -% end -% end -% -% %% apply map -% [row, col] = find(map); -% vars(col) = vars(row); -% -% %% inject blocks -% if b == 0 -% offset = 0; -% for i = 1:length(y) -% rows = length(y(i).rowsizes) - 1; -% cols = length(y(i).colsizes) - 1; -% if rows < cols -% m = cell(rows, 1); -% for n = 1:rows -% m{n} = cat(2, vars{offset + n + ((1:cols)-1) * rows}); -% end -% y(i).var = cat(1, m{:}); -% else -% m = cell(cols, 1); -% for n = 1:cols -% m{n} = cat(1, vars{offset + (n-1) * rows + (1:rows)}); -% end -% y(i).var = cat(2, m{:}); -% end -% offset = offset + rows * cols; -% end -% return -% end -% -% if b == 1 -% offset = 0; -% for i = 1:length(y) -% rows = length(y(i).rowsizes) - 1; -% cols = length(y(i).colsizes) - 1; -% if rows < cols -% m = cell(rows, 1); -% for n = 1:rows -% m{n} = cat(2, vars{offset + n + ((1:cols)-1) * rows}); -% end -% y(i).var = y(i).var + cat(1, m{:}); -% else -% m = cell(cols, 1); -% for n = 1:cols -% m{n} = cat(1, vars{offset + (n-1) * rows + (1:rows)}); -% end -% y(i).var = y(i).var + cat(2, m{:}); -% end -% offset = offset + rows * cols; -% end -% return -% end -% -% offset = 0; -% for i = 1:length(y) -% rows = length(y(i).rowsizes) - 1; -% cols = length(y(i).colsizes) - 1; -% if rows < cols -% m = cell(rows, 1); -% for n = 1:rows -% m{n} = cat(2, vars{offset + n + ((1:cols)-1) * rows}); -% end -% y(i).var = b .* y(i).var + cat(1, m{:}); -% else -% m = cell(cols, 1); -% for n = 1:cols -% m{n} = cat(1, vars{offset + (n-1) * rows + (1:rows)}); -% end -% y(i).var = b .* y(i).var + cat(2, m{:}); -% end -% offset = offset + rows * cols; -% end end function typename = underlyingType(b) - typename = underlyingType(b(1).var); + typename = underlyingType(b.var{1}); end end end diff --git a/src/tensors/kernels/AbstractBlock.m b/src/tensors/kernels/AbstractBlock.m index 128b1b5..d433725 100644 --- a/src/tensors/kernels/AbstractBlock.m +++ b/src/tensors/kernels/AbstractBlock.m @@ -1,5 +1,6 @@ classdef (Abstract) AbstractBlock % Abstract structure for storing tensor data. + % % This represents the blocks in the block-diagonal decomposition of a general tensor. %#ok<*INUSD> @@ -13,19 +14,19 @@ % % Arguments % --------- - % fun : function_handle + % fun : :class:`function_handle` % initialising function for the tensor data, with signature - % `data = fun(dims)` where dims is a row vector of dimensions. + % :code:`data = fun(dims)` where dims is a row vector of dimensions. % - % codomain : AbstractSpace + % codomain : :class:`.AbstractSpace` % vector of vector spaces that form the codomain. % - % domain : AbstractSpace + % domain : :class:`.AbstractSpace` % vector of vector spaces that form the domain. % % Returns % ------- - % X : AbstractBlock + % X : :class:`.AbstractBlock` % tensor data. if isa(codomain, 'CartesianSpace') || isa(codomain, 'ComplexSpace') || ... @@ -34,13 +35,33 @@ return end - if braidingstyle(codomain, domain) == BraidingStyle.Abelian && ... - fusionstyle(codomain, domain) == FusionStyle.Unique - X = AbelianBlock(codomain, domain); - return - end + assert(~isa(codomain, 'SumSpace') && ~isa(domain, 'SumSpace'), ... + 'tensors:argerror', 'Cannot construct tensor with SumSpaces.'); + + global cache + if isempty(cache), cache = LRU; end - X = MatrixBlock(codomain, domain); + if Options.CacheEnabled() + key = GetMD5({codomain, domain}, 'Array', 'hex'); + med = get(cache, key); + if isempty(med) + if braidingstyle(codomain, domain) == BraidingStyle.Abelian && ... + fusionstyle(codomain, domain) == FusionStyle.Unique + med = AbelianBlock(codomain, domain); + else + med = MatrixBlock(codomain, domain); + end + cache = set(cache, key, med); + end + else + if braidingstyle(codomain, domain) == BraidingStyle.Abelian && ... + fusionstyle(codomain, domain) == FusionStyle.Unique + med = AbelianBlock(codomain, domain); + else + med = MatrixBlock(codomain, domain); + end + end + X = med; end end @@ -48,103 +69,119 @@ %% Required methods methods function Y = axpby(a, X, b, Y, p, map) - % (Abstract) Compute ```Y = permute(X, p) .* a + Y .* b```. + % Compute :code:`Y = permute(X, p) .* a + Y .* b`. % This method is the computationally critical method of this class, thus has - % special cases for scalar multiplication (a == 0), addition (nargin == 4), and - % various optimizations when a == 1, b == 0 | b == 1. Additionally, this method - % should not be called directly as it should not perform any error checks. + % special cases for scalar multiplication (:code:`a == 0`), addition + % (:code:`nargin == 4`), and various optimizations when :code:`a == 1`, + % :code:`b == 0 || b == 1`. Additionally, this method should not be called + % directly as it should not perform any error checks. % % Arguments % --------- - % a : double + % a : :class:`double` % scalar to multiply with X. % - % X : :class:`AbstractBlock` + % X : :class:`.AbstractBlock` % list of source blocks. % - % b : double + % b : :class:`double` % scalar to multiply with Y. % - % Y : :class:`AbstractBlock` + % Y : :class:`.AbstractBlock` % list of destination blocks. % - % p : int + % p : :class:`int` % permutation vector for X. % - % map : (sparse) double + % map : (sparse) :class:`double` % coefficient matrix for permuting X. % % Returns % ------- - % Y : :class:`AbstractBlock` - % Result of computing Y = permute(X, p) .* a + Y .* b. - + % Y : :class:`.AbstractBlock` + % Result of computing :code:`Y = permute(X, p) .* a + Y .* b`. + % + % Note + % ---- + % This is an abstract method that should be overloaded for each subtype. error('This method should be overloaded.'); end function [mblocks, mcharges] = matrixblocks(b) - % (Abstract) Extract a list of coupled matrix blocks. + % Extract a list of coupled matrix blocks. % % Arguments % --------- - % b : AbstractBlock + % b : :class:`.AbstractBlock` % list of input data. % % Returns % ------- - % mblocks : cell + % mblocks : :class:`cell` % list of non-zero coupled matrix blocks, sorted according to its charge. % - % mcharges : AbstractCharge + % mcharges : :class:`.AbstractCharge` % list of coupled charges. + % + % Note + % ---- + % This is an abstract method that should be overloaded for each subtype. error('This method should be overloaded.'); end function C = mul(C, A, B, a, b) - % (Abstract) Compute ```C = (A .* a) * (B .* b)```. + % Compute :code:`C = (A .* a) * (B .* b)`. % Compute the matrix product of two source tensor structures, and store the % result in the destination tensor structure. This method should not perform any % error checks. % % Arguments % --------- - % C : AbstractBlock + % C : :class:`.AbstractBlock` % location to store the result % - % A : AbstractBlock + % A : :class:`.AbstractBlock` % first matrix factor % - % B : AbstractBlock + % B : :class:`.AbstractBlock` % second matrix factor % - % a : double = 1 + % a : :class:`double` = 1 % first scalar factor % - % b : double = 1 + % b : :class:`double` = 1 % second scalar factor % % Returns % ------- - % C : AbstractBlock + % C : :class:`.AbstractBlock` % Result of computing C = (A .* a) * (B .* b) + % + % Note + % ---- + % This is an abstract method that should be overloaded for each subtype. error('This method should be overloaded.'); end function tblocks = tensorblocks(b) - % (Abstract) Extract a list of uncoupled tensor blocks. + % Extract a list of uncoupled tensor blocks. % % Arguments % --------- - % b : AbstractBlock + % b : :class:`.AbstractBlock` % list of input data. % % Returns % ------- - % tblocks : cell + % tblocks : :class:`cell` % list of non-zero uncoupled tensor blocks, sorted according to the coupled % charge and then in column-major order according to the uncoupled charges. + % + % Note + % ---- + % This is an abstract method that should be overloaded for each subtype. error('This method should be overloaded.'); end @@ -154,103 +191,106 @@ %% Optional methods methods function Y = axpy(a, X, Y, p, map) - % Compute ```Y = permute(X, p) .* a + Y```. + % Compute :code:`Y = permute(X, p) .* a + Y`. % This method is a convenience method that automatically falls back on - % ```Y = axpby(a, X, 1, Y, p, map)```, but can be overloaded if the additional + % :code:`Y = axpby(a, X, 1, Y, p, map)`, but can be overloaded if the additional % efficiency is desired. % % Arguments % --------- - % a : double + % a : :class:`double` % scalar to multiply with X. % - % X : AbstractBlock + % X : :class:`.AbstractBlock` % list of source blocks. % - % Y : AbstractBlock + % Y : :class:`.AbstractBlock` % list of destination blocks. % - % p : int + % p : :class:`int` % permutation vector for X. % - % map : (sparse) double + % map : (sparse) :class:`double` % coefficient matrix for permuting X. % % Returns % ------- - % Y : AbstractBlock - % Result of computing Y = permute(X, p) .* a + Y. + % Y : :class:`.AbstractBlock` + % Result of computing :code:`Y = permute(X, p) .* a + Y`. Y = axpby(a, X, 1, Y, p, map); end function Y = minus(X, Y) - % Subtraction of X and Y. + % Subtraction of :code:`X` and :code`Y`. % % Usage % ----- - % Y = minus(X, Y) - % Y = X - Y + % :code:`Y = minus(X, Y)` + % + % :code:`Y = X - Y` % % Arguments % --------- - % X : AbstractBlock + % X : :class:`.AbstractBlock` % first list of input data. % - % Y : AbstractBlock + % Y : :class:`.AbstractBlock` % second list of input data. % % Returns % ------- - % Y : AbstractBlock - % list of output matrices. + % Y : :class:`.AbstractBlock` + % list of output data. Y = axpby(1, X, -1, Y); end function Y = plus(X, Y) - % Addition of X and Y. + % Addition of :code:`X` and :code:`Y`. % % Usage % ----- - % Y = plus(X, Y) - % Y = X + Y + % :code:`Y = plus(X, Y)` + % + % :code:`Y = X + Y` % % Arguments % --------- - % X : AbstractBlock + % X : :class:`.AbstractBlock` % first list of input data. % - % Y : AbstractBlock + % Y : :class:`.AbstractBlock` % second list of input data. % % Returns % ------- - % Y : MatrixBlock + % Y : :class:`.AbstractBlock` % list of output data. Y = axpby(1, X, 1, Y); end function Y = times(Y, a) - % Scalar multiplication of Y and a. + % Scalar multiplication of :code:`Y` and :code:`a`. % % Usage % ----- - % Y = times(Y, a) - % Y = Y .* a + % :code:`Y = times(Y, a)` + % + % :code:`Y = Y .* a` % % Arguments % --------- - % Y : AbstractBlock + % Y : :class:`.AbstractBlock` % list of input data. % - % a : double + % a : :class:`double` % scalar factor. % % Returns % ------- - % Y : AbstractBlock + % Y : :class:`.AbstractBlock` % list of output data. Y = axpby(0, [], a, Y); @@ -261,20 +301,20 @@ % % Usage % ----- - % Y = rdivide(Y, a) - % Y = Y ./ a + % :code:`Y = rdivide(Y, a)` + % :code:`Y = Y ./ a` % % Arguments % --------- - % Y : AbstractBlock + % Y : :class:`.AbstractBlock` % list of input data. % - % a : double + % a : :class:`double` % scalar factor. % % Returns % ------- - % Y : AbstractBlock + % Y : :class:`.AbstractBlock` % list of output data. Y = axpby(0, [], 1/a, Y); @@ -285,17 +325,18 @@ % % Usage % ----- - % A = uplus(A) - % A = +A + % :code:`A = uplus(A)` + % + % :code:`A = +A` % % Arguments % --------- - % A : AbstractBlock + % A : :class:`.AbstractBlock` % list of input data. % % Returns % ------- - % A : AbstractBlock + % A : :class:`.AbstractBlock` % list of output data. end @@ -305,18 +346,19 @@ % % Usage % ----- - % A = uminus(A) - % A = -A + % :code:`A = uminus(A)` + % + % :code:`A = -A` % % Arguments % --------- - % A : MatrixBlock - % list of input matrices. + % A : :class:`.AbstractBlock` + % list of input data. % % Returns % ------- - % A : MatrixBlock - % list of output matrices. + % A : :class:`.AbstractBlock` + % list of output data. Y = Y .* (-1); end @@ -326,20 +368,46 @@ % % Arguments % --------- - % X : :class:`AbstractBlock` + % X : :class:`.AbstractBlock` % input block. % % Returns % ------- - % type : char + % type : :class:`char` % the scalar type of the data, which is one of the following: % - % - 'single' or 'double' - % - 'logical' - % - 'int8', 'int16', 'int32' or 'int64' - % - 'uint8', 'uint16', 'uint32' or 'uint64' + % - :class:`single` or :class:`double` + % - :class:`logical` + % - :class:`int8`, :class:`int16`, :class:`int32` or :class:`int64` + % - :class:`uint8`, :class:`uint16`, :class:`uint32` or :class:`uint64` error('tensors:AbstractMethod', 'This method should be overloaded.'); end + + function X = ctranspose(X) + % Adjoint of a tensor. + % + % Usage + % ----- + % :code:`X = ctranspose(X)` + % + % :code:`X = X'` + % + % Arguments + % --------- + % X : :class:`.AbstractBlock` + % list of input data. + % + % Returns + % ------- + % X : :class:`.AbstractBlock` + % list of adjoint output data. + + end + + + function bool = iszero(X) + bool = isempty(X.var); + end end end \ No newline at end of file diff --git a/src/tensors/kernels/MatrixBlock.m b/src/tensors/kernels/MatrixBlock.m index a23ce38..ef3f45b 100644 --- a/src/tensors/kernels/MatrixBlock.m +++ b/src/tensors/kernels/MatrixBlock.m @@ -1,14 +1,19 @@ classdef MatrixBlock < AbstractBlock - %MATRIXBLOCK Summary of this class goes here - % Detailed explanation goes here + % Structure for storing symmetric tensor data. + % + % This represents the blocks in the block-diagonal decomposition of a tensor defined + % over graded vector spaces. + % + % .. todo:: + % Document properties and behavior. %#ok<*INUSD> properties charge - var - rowsizes - colsizes - tdims + var = {} + rowsizes = {} + colsizes = {} + tdims = {} rank end @@ -17,8 +22,18 @@ if nargin == 0, return; end rank = [length(codomain) length(domain)]; + b.rank = rank; trees = fusiontrees(codomain, domain); + if isempty(trees) + warning('tensors:empty', ... + 'No fusion channels available for the given spaces.'); +% b = MatrixBlock.empty(0, 1); + return + end + assert(~isempty(trees), 'tensors:empty', ... + 'no fusion channels available for the given spaces.'); + [c, ~, ic] = unique(trees.coupled); uncoupled = trees.uncoupled; @@ -32,6 +47,13 @@ splits = split(trees); fuses = fuse(trees); + + b.charge = reshape(c, 1, []); + b.var = cell(size(b.charge)); + b.rowsizes = cell(size(b.charge)); + b.colsizes = cell(size(b.charge)); + b.tdims = cell(size(b.charge)); + for i = length(c):-1:1 ids = ic == i; @@ -43,12 +65,10 @@ coldims = mdims(ids, 2); colsizes = [0 cumsum(coldims(ia)).']; - b(i).charge = c(i); - b(i).var = uninit([rowsizes(end) colsizes(end)]); - b(i).rowsizes = rowsizes; - b(i).colsizes = colsizes; - b(i).tdims = tdims(ids, :); - b(i).rank = rank; + b.var{i} = uninit([rowsizes(end) colsizes(end)]); + b.rowsizes{i} = rowsizes; + b.colsizes{i} = colsizes; + b.tdims{i} = tdims(ids, :); end end @@ -91,8 +111,12 @@ %% Special case 2: addition without permutation + rx = X.rank; + ry = Y.rank; + if nargin == 4 || (isempty(p) && isempty(map)) || ... - (all(p == 1:length(p)) && all(X(1).rank == Y(1).rank)) + (all(p == 1:length(p)) && isequal(rx, ry) && ... + isequal(map, speye(size(map)))) % reduce to scalar multiplication if b == 0 % a ~= 0 -> case 1 Y = X .* a; @@ -104,49 +128,55 @@ if a == 1 && b == -1, Y = X - Y; return; end if a == -1 && b == 1, Y = Y - X; return; end + Yvar = Y.var; + Xvar = X.var; + % general addition + scalar multiplication cases - if a == 1 % b ~= 1 - for i = 1:length(Y) - Y(i).var = Y(i).var .* b + X(i).var; + if a == 1 % b ~= 1 + for i = 1:length(Yvar) + Yvar{i} = Yvar{i} .* b + Xvar{i}; end - return - end - - if b == 1 % a ~= 1 - for i = 1:length(Y) - Y(i).var = Y(i).var + X(i).var .* a; + elseif b == 1 % a ~= 1 + for i = 1:length(Yvar) + Yvar{i} = Yvar{i} + Xvar{i} .* a; + end + else % a ~= [0 1], b ~= [0 1] + for i = 1:length(Yvar) + Yvar{i} = Yvar{i} .* b + Xvar{i} .* a; end - return end - % a ~= [0 1], b ~= [0 1] - for i = 1:length(Y) - Y(i).var = Y(i).var .* b + X(i).var .* a; - end + Y.var = Yvar; return end %% General case: addition with permutation % tensor indexing to matrix indexing - rrx = rankrange(X(1).rank); - rry = rankrange(Y(1).rank); + rrx = rankrange(rx); + rry = rankrange(ry); p_eff(rry) = rrx(p); + p_eff_1 = p_eff(1:ry(1)); + p_eff_2 = p_eff((1:ry(2)) + ry(1)); % extract small tensor blocks doA = a ~= 1; vars_in = cell(size(map, 1), 1); offset = 0; - for i = 1:length(X) - rowsz = X(i).rowsizes; - colsz = X(i).colsizes; + + Xrowsizes = X.rowsizes; + Xcolsizes = X.colsizes; + Xtdims = X.tdims; + Xvar = X.var; + for i = 1:length(Xvar) + rowsz = Xrowsizes{i}; + colsz = Xcolsizes{i}; - var_in = X(i).var; + var_in = Xvar{i}; if doA, var_in = a .* var_in; end - oldtdims = X(i).tdims(:, rrx); - newmdims = [prod(X(i).tdims(:, p_eff(1:Y(1).rank(1))), 2) ... - prod(X(i).tdims(:, p_eff((1:Y(1).rank(2)) + Y(1).rank(1))), 2)]; + oldtdims = Xtdims{i}(:, rrx); + newmdims = [prod(oldtdims(:, p_eff_1), 2) prod(oldtdims(:, p_eff_2), 2)]; for k = 1:length(colsz) - 1 for j = 1:length(rowsz) - 1 @@ -173,46 +203,50 @@ end % inject small tensor blocks + Yrowsizes = Y.rowsizes; + Ycolsizes = Y.colsizes; + Yvar = Y.var; if b == 0 offset = 0; - for i = 1:length(Y) - rows = length(Y(i).rowsizes) - 1; - cols = length(Y(i).colsizes) - 1; + for i = 1:length(Yvar) + rows = length(Yrowsizes{i}) - 1; + cols = length(Ycolsizes{i}) - 1; if rows < cols m = cell(rows, 1); for n = 1:rows m{n} = cat(2, vars_out{offset + n + ((1:cols)-1) * rows}); end - Y(i).var = cat(1, m{:}); + Yvar{i} = cat(1, m{:}); else m = cell(cols, 1); for n = 1:cols m{n} = cat(1, vars_out{offset + (n-1) * rows + (1:rows)}); end - Y(i).var = cat(2, m{:}); + Yvar{i} = cat(2, m{:}); end offset = offset + rows * cols; end + Y.var = Yvar; return end if b == 1 offset = 0; - for i = 1:length(Y) - rows = length(Y(i).rowsizes) - 1; - cols = length(Y(i).colsizes) - 1; + for i = 1:length(Yvar) + rows = length(Yrowsizes{i}) - 1; + cols = length(Ycolsizes{i}) - 1; if rows < cols m = cell(rows, 1); for n = 1:rows m{n} = cat(2, vars_out{offset + n + ((1:cols)-1) * rows}); end - Y(i).var = Y(i).var + cat(1, m{:}); + Yvar{i} = Yvar{i} + cat(1, m{:}); else m = cell(cols, 1); for n = 1:cols m{n} = cat(1, vars_out{offset + (n-1) * rows + (1:rows)}); end - Y(i).var = Y(i).var + cat(2, m{:}); + Yvar{i} = Yvar{i} + cat(2, m{:}); end offset = offset + rows * cols; end @@ -220,21 +254,21 @@ end offset = 0; - for i = 1:length(Y) - rows = length(Y(i).rowsizes) - 1; - cols = length(Y(i).colsizes) - 1; + for i = 1:length(Yvar) + rows = length(Yrowsizes{i}) - 1; + cols = length(Ycolsizes{i}) - 1; if rows < cols m = cell(rows, 1); for n = 1:rows m{n} = cat(2, vars_out{offset + n + ((1:cols)-1) * rows}); end - Y(i).var = b .* Y(i).var + cat(1, m{:}); + Yvar{i} = b .* Yvar{i} + cat(1, m{:}); else m = cell(cols, 1); for n = 1:cols m{n} = cat(1, vars_out{offset + (n-1) * rows + (1:rows)}); end - Y(i).var = b .* Y(i).var + cat(2, m{:}); + Yvar{i} = b .* Yvar{i} + cat(2, m{:}); end offset = offset + rows * cols; end @@ -265,34 +299,56 @@ % C : MatrixBlock % Result of computing C = (A .* a) * (B .* b) - [indA, locA] = ismember([C.charge], [A.charge]); - [indB, locB] = ismember([C.charge], [B.charge]); + Acharge = A.charge; + Bcharge = B.charge; + Ccharge = C.charge; + + if isequal(Acharge, Ccharge) + indA = true(size(Acharge)); + locA = 1:length(Acharge); + else + [indA, locA] = ismember_sorted(Ccharge, Acharge); + end + if isequal(Bcharge, Ccharge) + indB = true(size(Bcharge)); + locB = 1:length(Bcharge); + else + [indB, locB] = ismember_sorted(Ccharge, Bcharge); + end + + Avar = A.var; + Bvar = B.var; + Cvar = C.var; if nargin == 3 || (a == 1 && b == 1) for i = find(indA & indB) - C(i).var = A(locA(i)).var * B(locB(i)).var; + Cvar{i} = Avar{locA(i)} * Bvar{locB(i)}; end + C.var = Cvar; return end if a == 1 % b ~= 1 for i = find(indA & indB) - C(i).var = (A(locA(i)).var .* a) * B(locB(i)).var; + Cvar{i} = (Avar{locA(i)} .* a) * Bvar{locB(i)}; end + C.var = Cvar; return end if b == 1 % a ~= 1 for i = find(indA & indB) - C(i).var = A(locA(i)).var * (B(locB(i)).var .* b); + Cvar{i} = Avar{locA(i)} * (Bvar{locB(i)} .* b); end + C.var = Cvar; return end % a ~= 1 && b ~= 1 for i = find(indA & indB) - C(i).var = (A(locA(i)).var .* a) * (B(locB(i)).var .* b); + Cvar{i} = (Avar{locA(i)} .* a) * (Bvar{locB(i)} .* b); end + C.var = Cvar; end function tblocks = tensorblocks(b) @@ -309,22 +365,22 @@ % list of non-zero small tensor blocks, in column-major order. nblocks = 0; - for i = 1:length(b) - nblocks = nblocks + size(b(i).tdims, 1); + for i = 1:length(b.var) + nblocks = nblocks + size(b.tdims{i}, 1); end tblocks = cell(nblocks, 1); offset = 0; - p = rankrange(b(1).rank); - for i = 1:length(b) - rowsz = b(i).rowsizes; - colsz = b(i).colsizes; + p = rankrange(b.rank); + for i = 1:length(b.var) + rowsz = b.rowsizes{i}; + colsz = b.colsizes{i}; for k = 1:length(colsz) - 1 for j = 1:length(rowsz) - 1 offset = offset + 1; tblocks{offset} = permute(reshape(... - b(i).var(rowsz(j)+1:rowsz(j+1), colsz(k)+1:colsz(k+1)), ... - b(i).tdims(j + (k-1) * (length(rowsz)-1), p)), ... + b.var{i}(rowsz(j)+1:rowsz(j+1), colsz(k)+1:colsz(k+1)), ... + b.tdims{i}(j + (k-1) * (length(rowsz)-1), p)), ... p); end end @@ -347,41 +403,61 @@ % mcharges : AbstractCharge % list of coupled charges. - mblocks = {b.var}; + mblocks = b.var; if nargout > 1 - mcharges = [b.charge]; + mcharges = b.charge; end end function b = fill_matrix_data(b, vars, charges) if nargin < 3 || isempty(charges) - assert(length(vars) == length(b)); - for i = 1:length(b) - b(i).var = vars{i}; + assert(length(vars) == length(b.var), ... + 'Invalid number of blocks'); + for i = 1:length(b.var) + b.var{i} = vars{i}; end return end - - [lia, locb] = ismember(charges, [b.charge]); + [lia, locb] = ismember(charges, b.charge); assert(all(lia)); - for i = 1:length(vars) - b(locb(i)).var = vars{i}; - end + b.var(locb) = vars; end function b = fill_matrix_fun(b, fun, charges) if nargin < 3 || isempty(charges) - for i = 1:length(b) - b(i).var = fun(size(b(i).var), b(i).charge); + for i = 1:length(b.var) + b.var{i} = fun(size(b.var{i})); end else - [lia, locb] = ismember(charges, [b.charge]); + [lia, locb] = ismember(charges, b.charge); for i = locb(lia) - b(i).var = fun(size(b(i).var), b(i).charge); + b.var{i} = fun(size(b.var{i}), b.charge(i)); end end end + function b = fill_tensor_data(b, data) + ctr = 0; + p = rankrange(b.rank); + + for i = 1:length(b.var) + rowsz = b.rowsizes{i}; + colsz = b.colsizes{i}; + for k = 1:length(colsz) - 1 + for j = 1:length(rowsz) - 1 + ctr = ctr + 1; + b.var{i}(rowsz(j)+1:rowsz(j+1), colsz(k)+1:colsz(k+1)) = ... + reshape(permute(data{ctr}, p), ... + rowsz(j+1)-rowsz(j), colsz(k+1)-colsz(k)); + end + end + end + end + + function b = fill_tensor_fun(b, fun, trees) + + end + function Y = minus(X, Y) % Subtraction of X and Y. % @@ -403,8 +479,8 @@ % Y : MatrixBlock % list of output matrices. - for i = 1:length(Y) - Y(i).var = X(i).var - Y(i).var; + for i = 1:length(Y.var) + Y.var{i} = X.var{i} - Y.var{i}; end end @@ -429,8 +505,8 @@ % Y : MatrixBlock % list of output matrices. - for i = 1:length(Y) - Y(i).var = Y(i).var + X(i).var; + for i = 1:length(Y.var) + Y.var{i} = X.var{i} + Y.var{i}; end end @@ -458,8 +534,8 @@ if a == 1, return; end if a == -1, Y = -Y; return; end - for i = 1:length(Y) - Y(i).var = Y(i).var .* a; + for i = 1:length(Y.var) + Y.var{i} = Y.var{i} .* a; end end @@ -487,8 +563,8 @@ if a == 1, return; end if a == -1, Y = -Y; return; end - for i = 1:length(Y) - Y(i).var = Y(i).var ./ a; + for i = 1:length(Y.var) + Y.var{i} = Y.var{i} ./ a; end end @@ -530,13 +606,13 @@ % A : MatrixBlock % list of output matrices. - for i = 1:length(Y) - Y(i).var = -Y(i).var; + for i = 1:length(Y.var) + Y.var{i} = -Y.var{i}; end end function t = underlyingType(X) - t = underlyingType(X(1).var); + t = underlyingType(X.var{1}); end function X = ctranspose(X) @@ -544,67 +620,104 @@ % % Usage % ----- - % X = ctranspose(X) - % X = X' + % :code:`X = ctranspose(X)` + % + % :code:`X = X'` % % Arguments % --------- - % X : :class:`MatrixBlock` + % X : :class:`.MatrixBlock` % list of input matrices. % % Returns % ------- - % X : :class:`MatrixBlock` + % X : :class:`.MatrixBlock` % list of adjoint output matrices. - for i = 1:length(X) - X(i).var = X(i).var'; - [X(i).rowsizes, X(i).colsizes] = swapvars(X(i).rowsizes, X(i).colsizes); - X(i).tdims = fliplr(X(i).tdims); - X(i).rank = fliplr(X(i).rank); + X.rank = fliplr(X.rank); + for i = 1:length(X.var) + X.var{i} = X.var{i}'; + ncols = length(X.colsizes{i}) - 1; + nrows = length(X.rowsizes{i}) - 1; + X.tdims{i} = reshape(permute(... + reshape(fliplr(X.tdims{i}), nrows, ncols, []), ... + [2 1 3]), ... + ncols * nrows, []); end + + [X.rowsizes, X.colsizes] = swapvars(X.rowsizes, X.colsizes); end function v = vectorize(X, type) + if numel(X) > 1 + vs = cell(size(X)); + for i = 1:numel(vs) + vs{i} = vectorize(X(i), type); + end + v = vertcat(vs{:}); + return + end + + qdims = sqrt(qdim(X.charge)); switch type case 'complex' - blocks = cell(size(X)); - for i = 1:length(X) - blocks{i} = reshape(X(i).var .* sqrt(qdim(X(i).charge)), [], 1); + blocks = cell(size(X.var)); + for i = 1:length(X.var) + blocks{i} = reshape(X.var{i} .* qdims(i), [], 1); end v = vertcat(blocks{:}); case 'real' - blocks = cell(size(X)); - for i = 1:length(X) - X(i).var = X(i).var .* sqrt(qdim(X(i).charge)); - blocks{i} = [reshape(real(X(i).var), [], 1) - reshape(imag(X(i).var), [], 1)]; + blocks = cell(size(X.var)); + for i = 1:length(X.var) + tmp = X.var{i} .* qdims(i); + blocks{i} = [reshape(real(tmp), [], 1) + reshape(imag(tmp), [], 1)]; end v = vertcat(blocks{:}); end end function X = devectorize(v, X, type) - switch type - case 'complex' - ctr = 0; - for i = 1:length(X) - n = numel(X(i).var); - X(i).var = reshape(v(ctr + (1:n)), size(X(i).var)) ./ ... - sqrt(qdim(X(i).charge)); - ctr = ctr + n; - end - - case 'real' - ctr = 0; - for i = 1:length(X) - n = numel(X(i).var); - sz = size(X(i).var); - X(i).var = complex(reshape(v(ctr + (1:n)), sz), ... - reshape(v(ctr + (n + 1:2 * n)), sz)) ./ sqrt(qdim(X(i).charge)); - ctr = ctr + 2 * n; + ctr = 0; + for i = 1:numel(X) + qdims = sqrt(qdim(X(i).charge)); + switch type + case 'complex' + for j = 1:length(X(i).var) + n = numel(X(i).var{j}); + X(i).var{j} = reshape(v(ctr + (1:n)), size(X(i).var{j})) ./ qdims(j); + ctr = ctr + n; + end + + case 'real' + for j = 1:length(X(i).var) + n = numel(X(i).var{j}); + sz = size(X(i).var{j}); + X(i).var{j} = complex(reshape(v(ctr + (1:n)), sz), ... + reshape(v(ctr + (n + 1:2 * n)), sz)) ./ qdims(j); + ctr = ctr + 2 * n; + end + end + end + end + end + + methods + function assertBlocksizes(X) + for i = 1:numel(X.var) + assert(isequal(size(X.var{i}), [X.rowsizes{i}(end) X.colsizes{i}(end)]), ... + 'kernel:dimerror', 'Wrong size of block'); + rows = length(X.rowsizes{i}) - 1; + cols = length(X.colsizes{i}) - 1; + matdims = [prod(X.tdims{i}(:, 1:X.rank(1)), 2) ... + prod(X(i).tdims(:, X.rank(1)+1:end), 2)]; + for k = 1:cols + for j = 1:rows + assert(matdims(j + (k-1) * rows, 1) == X.rowsizes{i}(j+1) - X.rowsizes{i}(j)); + assert(matdims(j + (k-1) * rows, 2) == X.colsizes{i}(k+1) - X.colsizes{i}(k)); end + end end end end diff --git a/src/tensors/kernels/TrivialBlock.m b/src/tensors/kernels/TrivialBlock.m index 5959f7c..a3498f3 100644 --- a/src/tensors/kernels/TrivialBlock.m +++ b/src/tensors/kernels/TrivialBlock.m @@ -1,5 +1,8 @@ classdef TrivialBlock < AbstractBlock - % TrivialBlock - Data structure for tensors without symmetry. + % Data structure for tensors without symmetry. + % + % .. todo:: + % Document properties and behavior. properties var @@ -18,7 +21,7 @@ function Y = axpby(a, X, b, Y, p, ~) - %% Special case 1: scalar multiplication + %%% Special case 1: scalar multiplication if a == 0 if b == 1 return; @@ -117,25 +120,6 @@ end function Y = rdivide(Y, a) - % Scalar division of Y and a. - % - % Usage - % ----- - % Y = rdivide(Y, a) - % Y = Y ./ a - % - % Arguments - % --------- - % Y : MatrixBlock - % list of input matrices. - % - % a : double - % scalar factor. - % - % Returns - % ------- - % Y : MatrixBlock - % list of output matrices. if a == 1, return; end if a == -1, Y = -Y; return; end @@ -193,6 +177,15 @@ end function v = vectorize(X, type) + if numel(X) > 1 + vs = cell(size(X)); + for i = 1:numel(X) + vs{i} = vectorize(X(i), type); + end + v = vertcat(vs{:}); + return + end + switch type case 'complex' v = X.var(:); @@ -203,14 +196,19 @@ end function X = devectorize(v, X, type) - switch type - case 'complex' - X.var = reshape(v, size(X.var)); - - case 'real' - m = size(v, 1) / 2; - sz = size(X.var); - X.var = complex(reshape(v(1:m), sz), reshape(v(m + 1:2 * m), sz)); + ctr = 0; + for i = 1:numel(X) + switch type + case 'complex' + X(i).var = reshape(v(ctr + (1:numel(X(i).var))), size(X(i).var)); + ctr = ctr + numel(X(i).var); + case 'real' + v_ = v(ctr + 1:2*numel(X(i).var)); + m = size(v_, 1) / 2; + sz = size(X(i).var); + X(i).var = complex(reshape(v_(1:m), sz), reshape(v_(m + 1:2 * m), sz)); + ctr = ctr + numel(X(i).var) * 2; + end end end diff --git a/src/tensors/spaces/AbstractSpace.m b/src/tensors/spaces/AbstractSpace.m index 881f97d..6973626 100644 --- a/src/tensors/spaces/AbstractSpace.m +++ b/src/tensors/spaces/AbstractSpace.m @@ -1,60 +1,72 @@ classdef (Abstract) AbstractSpace % Abstract structure of a tensor index. + % + % Properties + % ---------- + % dimensions : :class:`int` or :class:`struct` + % Specification of the internal dimensions + % + % dual : :class:`logical` + % Flag to indicate if the space is a dual space - properties (Access = protected) - dimensions % Specification of the internal dimensions - isdual (1,1) logical = false % Flag to indicate if the space is a dual space + properties + dimensions + dual (1,1) logical = false end %% Constructors methods - function spaces = AbstractSpace(dimensions, isdual) + function spaces = AbstractSpace(dimensions, dual) % Construct an array of vector spaces. % % Repeating Arguments % ------------------- - % dimensions : int or struct + % dimensions : :class:`int` or :class:`struct` % a variable which represents the internal dimension of the space. % - % isdual : logical + % isdual : :class:`logical` % a variable which indicates if a space is dual. % % Returns % ------- - % spaces : :class:`AbstractSpace` + % spaces : :class:`.AbstractSpace` % array of spaces. arguments (Repeating) dimensions - isdual + dual end if nargin == 0, return; end for i = length(dimensions):-1:1 spaces(i).dimensions = dimensions{i}; - spaces(i).isdual = isdual{i}; + spaces(i).dual = dual{i}; end end end methods (Static) - function spaces = new(dimensions, isdual) + function spaces = new(dimensions, dual) % Construct a vector space. This is a utility method to be able to access the % constructor of a subclass. % % Repeating Arguments % ------------------- - % dimensions : int or struct + % dimensions : :class:`int` or :class:`struct` % a variable which represents the internal dimension of the space. % - % isdual : logical + % dual : :class:`logical` % a variable which indicates if a space is dual. % % Returns % ------- - % spaces : :class:`AbstractSpace` + % spaces : :class:`.AbstractSpace` % array of spaces. + % + % Note + % ---- + % This abstract constructor should be overloaded for every concrete subtype. error('tensors:AbstractMethod', 'This method should be overloaded.'); end @@ -63,18 +75,22 @@ %% Structure methods + function f = isdual(space) + f = [space.dual]; + end + function c = charges(spaces) % Compute all charge combinations of a space. If no internal structure is % present, this yields an empty result. % % Arguments % --------- - % spaces : (1, :) :class:`AbstractSpace` + % spaces : (1, :) :class:`.AbstractSpace` % input spaces. % % Returns % ------- - % c : (:, :) :class:`AbstractCharge` + % c : (:, :) :class:`.AbstractCharge` % list of charge combinations, where each row is a combination. error('tensors:AbstractMethod', 'This method should be overloaded.'); @@ -85,12 +101,12 @@ % % Arguments % --------- - % spaces : (1, :) :class:`AbstractSpace` + % spaces : (1, :) :class:`.AbstractSpace` % input spaces. % % Returns % ------- - % d : (:, :) int + % d : (:, :) :class:`int` % list of degeneracy combinations, where each row is an entry. error('tensors:AbstractMethod', 'This method should be overloaded.'); @@ -101,32 +117,44 @@ % % Arguments % --------- - % spaces : (1, :) :class:`AbstractSpace` + % spaces : (1, :) :class:`.AbstractSpace` % input spaces. % % Returns % ------- - % d : (1, :) numeric + % d : (1, :) :class:`double` % total dimension of each of the input spaces. error('tensors:AbstractMethod', 'This method should be overloaded.'); end + function s = subspaces(s) + end + function trees = fusiontrees(codomain, domain) - % Compute all allowed fusiontrees that connect domain and codomain. If the - % spaces have no internal structure than this returns `[]`. + % Compute all allowed fusiontrees that connect domain and codomain. Only the + % trivial fusion tree is allowed, so this returns empty. % % Arguments % --------- - % codomain, domain : :class:`AbstractSpace` + % codomain, domain : :class:`.GradedSpace` % input spaces. % % Returns % ------- - % trees : :class:`FusionTree` - % list of fusiontrees that are allowed. + % trees : :class:`.FusionTree` + % list of all allowed fusion trees. - error('tensors:AbstractMethod', 'This method should be overloaded.'); + rank = [length(codomain) length(domain)]; + spaces = [codomain flip(domain)]; + + args = cell(2, sum(rank)); + for i = 1:size(args, 2) + args{1, i} = charges(spaces(i)); + args{2, i} = isdual(spaces(i)); + end + + trees = FusionTree.new(rank, args{:}); end function style = braidingstyle(codomain, domain) @@ -134,12 +162,12 @@ % % Arguments % --------- - % codomain, domain : (1, :) :class:`AbstractSpace` + % codomain, domain : (1, :) :class:`.AbstractSpace` % input spaces. % % Returns % ------- - % style : :class:`BraidingStyle` + % style : :class:`.BraidingStyle` % braiding style of the internal structure. error('tensors:AbstractMethod', 'This method should be overloaded.'); @@ -150,12 +178,12 @@ % % Arguments % --------- - % codomain, domain : (1, :) :class:`AbstractSpace` + % codomain, domain : (1, :) :class:`.AbstractSpace` % input spaces. % % Returns % ------- - % style : :class:`FusionStyle` + % style : :class:`.FusionStyle` % fusion style of the internal structure. error('tensors:AbstractMethod', 'This method should be overloaded.'); @@ -170,16 +198,16 @@ % % Arguments % --------- - % spaces : (1, :) :class:`AbstractSpace` + % spaces : (1, :) :class:`.AbstractSpace` % input spaces. % % Returns % ------- - % spaces : (1, :) :class:`AbstractSpace` + % spaces : (1, :) :class:`.AbstractSpace` % dual spaces. for i = 1:length(spaces) - spaces(i).isdual = ~spaces(i).isdual; + spaces(i).dual = ~spaces(i).dual; end end @@ -189,12 +217,12 @@ % % Arguments % --------- - % spaces : (1, :) :class:`AbstractSpace` + % spaces : (1, :) :class:`.AbstractSpace` % input product space. % % Returns % ------- - % spaces : (1, :) :class:`AbstractSpace` + % spaces : (1, :) :class:`.AbstractSpace` % dual product space. spaces = conj(spaces(length(spaces):-1:1)); @@ -205,28 +233,45 @@ % % Arguments % --------- - % space1, space2 : (1,1) :class:`AbstractSpace` + % space1, space2 : (1, 1) :class:`.AbstractSpace` % input spaces. % % Returns % ------- - % space : (1,1) :class:`AbstractSpace` + % space : (1, 1) :class:`.AbstractSpace` % fused space. error('tensors:AbstractMethod', 'This method should be overloaded.'); end + function space = plus(space1, space2) + % Find the direct sum of two spaces. + % + % Arguments + % --------- + % space1, space2 : (1, 1) :class:`.AbstractSpace` + % input spaces. + % + % Returns + % ------- + % space : (1, 1) :class:`.AbstractSpace` + % direct sum space. + + + error('tensors:AbstractMethod', 'This method should be overloaded.'); + end + function space = prod(spaces) % Fuse a product space to a single space. % % Arguments % --------- - % spaces : (1, :) :class:`AbstractSpace` - % Array of input spaces. + % spaces : (1, :) :class:`.AbstractSpace` + % array of input spaces. % % Returns % ------- - % space : (1, 1) :class:`AbstractSpace` + % space : (1, 1) :class:`.AbstractSpace` % Fused space which is isomorphic to the input product space. % TODO this is probably faster by fusing from both ends to the middle. @@ -236,6 +281,53 @@ end end + function space = sum(spaces) + % Fuse a list of spaces to the direct sum space. + % + % Arguments + % --------- + % spaces : (1, :) :class:`.AbstractSpace` + % array of input spaces. + % + % Returns + % ------- + % space : (1, 1) :class:`.AbstractSpace` + % direct sum space. + space = spaces(1); + for i = 2:length(spaces) + space = space + spaces(i); + end + end + + function spaces = insertone(spaces, i, dual) + % Insert a trivial space at a given position. + % + % Arguments + % --------- + % spaces : (1, :) :class:`.AbstractSpace` + % array of input spaces. + % + % i : :class:`int` + % position at which to insert trivial space, defaults to the end. + % + % dual : :class:`logical` + % indicate if trivial space should be dualized, defaults to :code:`false`. + % + % Returns + % ------- + % spaces : (1, :) :class:`.AbstractSpace` + % array of output spaces. + arguments + spaces + i = length(spaces) + 1 + dual = false + end + + trivialspace = one(spaces); + if dual, trivialspace = conj(trivialspace); end + spaces = [spaces(1:i-1) trivialspace spaces(i:end)]; + end + %% Utility function bools = eq(spaces1, spaces2) @@ -243,17 +335,36 @@ % % Arguments % --------- - % spaces1, spaces2 : (1, :) :class:`AbstractSpace` + % spaces1, spaces2 : (1, :) :class:`.AbstractSpace` % input spaces, either of equal size or scalar. % % Returns % ------- - % bools : (1, :) logical + % bools : (1, :) :class:`logical` % flags that indicate if the element spaces are equal. error('tensors:AbstractMethod', 'This method should be overloaded.'); end + function bool = lt(space1, space2) + bool = ~ge(space1, space2); + end + + function bool = gt(space1, space2) + bool = ~le(space1, space2); + end + + function bool = ge(space1, space2) + bool = le(space2, space1); + end + + function bool = istrivial(space) + % Check whether a space is isomorphic to a trivial space. + + E = one(space); + bool = space == E || space == conj(E); + end + function bool = isequal(spaces) % Check whether all input spaces are equal. Spaces are considered equal if they % are of same size, and are equal element-wise. For convenience, empty spaces @@ -261,16 +372,16 @@ % % Usage % ----- - % bool = isequal(spaces{:}) + % :code:`bool = isequal(spaces{:})` % % Repeating Arguments % ------------------- - % spaces : (1,:) :class:`AbstractSpace` + % spaces : (1, :) :class:`.AbstractSpace` % input spaces to compare. % % Returns % ------- - % bool : (1,1) logical + % bool : (1, 1) :class:`logical` % true if all inputs are equal. arguments (Repeating) @@ -286,22 +397,49 @@ (isequal(size(spaces{1}), size(spaces{2})) && ... all(spaces{1} == spaces{2})); end + + function bool = isisometric(space1, space2) + bool = prod(space1) == prod(space2); + end function hashable = GetMD5_helper(spaces) % Helper function for hash algorithm. This converts the space object to a data - % structure which can be processed by :func:`GetMD5`. + % structure which can be processed by :func:`.GetMD5`. % % Arguments % --------- - % spaces : :class:`AbstractSpace` + % spaces : :class:`.AbstractSpace` % input spaces. % % Returns % ------- - % hashable : cell - % data which can be accepted by :func:`GetMD5`. + % hashable : :class:`cell` + % data which can be accepted by :func:`.GetMD5`. - hashable = {spaces.dimensions, spaces.isdual}; + hashable = {spaces.dimensions, spaces.dual}; + end + + function disp(spaces) + % Custom display of spaces. + + s = settings; + shortformat = strcmp('short', s.matlab.commandwindow.NumericFormat.ActiveValue); + + if isscalar(spaces) + fprintf('\t%s\n\n', string(spaces, 'IncludeDetails', ~shortformat)); + return + end + + sz = size(spaces); + assert(length(sz) == 2); + dim_str = sprintf('%dx%d', sz(1), sz(2)); + fprintf('\t%s Product %s:\n', dim_str, name(spaces)); + for i = 1:length(spaces) + subspacestring = string(spaces(i), ... + 'IncludeType', false, 'IncludeDetails', ~shortformat); + fprintf('\t\t%d.\t%s\n', i, subspacestring); + end + fprintf('\n'); end end end diff --git a/src/tensors/spaces/Arrow.m b/src/tensors/spaces/Arrow.m index 83b9af1..d867e97 100644 --- a/src/tensors/spaces/Arrow.m +++ b/src/tensors/spaces/Arrow.m @@ -1,10 +1,12 @@ classdef Arrow < logical -% Arrow - Direction of tensor leg. -% Enumeration class with directions. +% Enumeration class reprenting the possible directions of a tensor leg: +% +% - :code:`in`: Incoming tensor leg, encoded as a logical :code:`true` +% - :code:`out`: Outgoing tensor leg, encoded as a logical :code:`false` enumeration - in (false) - out (true) + in (true) + out (false) end end diff --git a/src/tensors/spaces/CartesianSpace.m b/src/tensors/spaces/CartesianSpace.m index 1c40aa1..eb4adc5 100644 --- a/src/tensors/spaces/CartesianSpace.m +++ b/src/tensors/spaces/CartesianSpace.m @@ -8,7 +8,7 @@ % % Repeating Arguments % ------------------- - % dimensions : (1, 1) int + % dimensions : (1, 1) :class:`int` % dimension of the vector space. % % ~ : any @@ -16,7 +16,7 @@ % % Returns % ------- - % spaces : :class:`CartesianSpace` + % spaces : :class:`.CartesianSpace` % array of cartesian spaces. arguments (Repeating) @@ -42,22 +42,24 @@ % % Usage % ----- - % spaces = CartesianSpace.new(dimensions, ~, ...) + % :code:`spaces = CartesianSpace.new(dims)` % - % spaces = CartesianSpace.new(dims) + % :code:`spaces = CartesianSpace.new(dimensions, ~, ...)` + % + % Arguments + % --------- + % dims : (1, :) :class:`int` + % vector of dimensions of all spaces. % % Repeating Arguments % ------------------- - % dimensions : struct + % dimensions : :class:`struct` % a variable which represents the internal dimension of the space. % - % dims : (1, :) int - % vector of dimensions of all spaces. - % % Returns % ------- - % spaces : :class:`CartesianSpace` - % array of spaces. + % spaces : (1, :) :class:`.CartesianSpace` + % array of cartesian vector spaces. if isstruct(varargin{1}) assert(mod(nargin, 2) == 0) @@ -83,21 +85,21 @@ %% Structure methods - function c = charges(~) + function c = charges(spaces) % Compute all charge combinations of a space. No internal structure is present, % this yields an empty result. % % Arguments % --------- - % spaces : (1, :) :class:`CartesianSpace` + % spaces : (1, :) :class:`.CartesianSpace` % input spaces. % % Returns % ------- - % c : [] - % empty result. + % c : (1, :) :class:`.Z1` + % array of trivial spaces of corresponding length. - c = []; + c = repmat(Z1, length(spaces), 1); end function d = degeneracies(spaces) @@ -105,12 +107,12 @@ % % Arguments % --------- - % spaces : (1, :) :class:`CartesianSpace` + % spaces : (1, :) :class:`.CartesianSpace` % input spaces. % % Returns % ------- - % d : (1, :) int + % d : (1, :) :class:`int` % list of degeneracy combinations, with 1 element. d = [spaces.dimensions]; @@ -121,46 +123,29 @@ % % Arguments % --------- - % spaces : (1, :) :class:`CartesianSpace` + % spaces : (1, :) :class:`.CartesianSpace` % input spaces. % % Returns % ------- - % d : (1, :) int + % d : (1, :) :class:`int` % total dimension of each of the input spaces. d = [spaces.dimensions]; end - function trees = fusiontrees(~, ~) - % Compute all allowed fusiontrees that connect domain and codomain. Only the - % trivial fusion tree is allowed, so this returns empty. - % - % Arguments - % --------- - % codomain, domain : :class:`CartesianSpace` - % input spaces. - % - % Returns - % ------- - % trees : :class:`FusionTree` - % only the trivial tree is allowed. - - trees = FusionTree(); - end - function style = braidingstyle(~, ~) % Determine the braiding style of the internal structure of a space. % % Arguments % --------- - % codomain, domain : (1, :) :class:`CartesianSpace` + % codomain, domain : (1, :) :class:`.CartesianSpace` % input spaces. % % Returns % ------- - % style : :class:`BraidingStyle` - % Trivial braiding style + % style : :class:`.BraidingStyle` + % trivial braiding style, :code:`BraidingStyle.Abelian`. style = BraidingStyle.Abelian; end @@ -170,16 +155,30 @@ % % Arguments % --------- - % codomain, domain : (1, :) :class:`CartesianSpace` + % codomain, domain : (1, :) :class:`.CartesianSpace` % input spaces. % % Returns % ------- - % style : :class:`FusionStyle` - % fusion style of the internal structure. + % style : :class:`.FusionStyle` + % fusion style of the internal structure, :code:`FusionStyle.Unique`. style = FusionStyle.Unique; end + + function space = one(~) + space = CartesianSpace(1, []); + end + + function spaces = insertone(spaces, i, ~) + arguments + spaces + i = length(spaces) + 1 + ~ + end + + spaces = [spaces(1:i-1) one(spaces) spaces(i:end)]; + end end @@ -190,12 +189,12 @@ % % Arguments % --------- - % spaces : (1, :) :class:`CartesianSpace` + % spaces : (1, :) :class:`.CartesianSpace` % input spaces. % % Returns % ------- - % spaces : (1, :) :class:`CartesianSpace` + % spaces : (1, :) :class:`.CartesianSpace` % dual spaces. end @@ -204,32 +203,41 @@ % % Arguments % --------- - % space1, space2 : (1, 1) :class:`CartesianSpace` + % space1, space2 : (1, 1) :class:`.CartesianSpace` % input spaces. % % Returns % ------- - % space : (1, 1) :class:`CartesianSpace` + % space : (1, 1) :class:`.CartesianSpace` % fused space. space.dimensions = space.dimensions * space2.dimensions; end - function space = prod(spaces) + function space = prod(spaces, ~) % Fuse a product space to a single space. % % Arguments % --------- - % spaces : (1, :) :class:`CartesianSpace` - % Array of input spaces. + % spaces : (1, :) :class:`.CartesianSpace` + % array of input spaces. % % Returns % ------- - % space : (1, 1) :class:`CartesianSpace` - % Fused space which is isomorphic to the input product space. + % space : (1, 1) :class:`.CartesianSpace` + % fused space which is isomorphic to the input product space. space = CartesianSpace(prod(dims(spaces)), []); end + + function space = plus(space, space2) + space.dimensions = space.dimensions + space2.dimensions; + end + + function space = sum(spaces) + + space = CartesianSpace(sum(dims(spaces)), []); + end end @@ -240,56 +248,69 @@ % % Arguments % --------- - % spaces1, spaces2 : (1, :) :class:`CartesianSpace` + % spaces1, spaces2 : (1, :) :class:`.CartesianSpace` % input spaces, either of equal size or scalar. % % Returns % ------- - % bools : (1, :) logical + % bools : (1, :) :class:`logical` % flags that indicate if the element spaces are equal. bools = [spaces1.dimensions] == [spaces2.dimensions]; end - function bool = ge(space1, space2) - bool = le(space2, space1); - end - function bool = le(space1, space2) assert(isscalar(space1) && isscalar(space2)); bool = degeneracies(space1) <= degeneracies(space2); end + function space = infimum(space1, space2) + assert(isscalar(space1) && isscalar(space2)); + space = CartesianSpace.new(min(dims(space1), dims(space2))); + end + function hashable = GetMD5_helper(spaces) % Helper function for hash algorithm. This converts the space object to a data - % structure which can be processed by :func:`GetMD5`. + % structure which can be processed by :func:`.GetMD5`. % % Arguments % --------- - % spaces : (1, :) :class:`CartesianSpace` + % spaces : (1, :) :class:`.CartesianSpace` % input spaces. % % Returns % ------- - % hashable : (1, :) int - % data which can be accepted by :func:`GetMD5`. + % hashable : (1, :) :class:`int` + % data which can be accepted by :func:`.GetMD5`. hashable = [spaces.dimensions]; end - function disp(spaces) - if isscalar(spaces) - fprintf('CartesianSpace of dimension %d:\n', spaces.dimensions); + function s = string(spaces, kwargs) + arguments + spaces + kwargs.IncludeType = true + kwargs.IncludeDetails = true + end + + if numel(spaces) > 1 + kwargs = namedargs2cell(kwargs); + s = arrayfun(@(x) string(x, kwargs{:}), spaces); return end - fprintf('%dx%d Product space with elements:\n\n', ... - size(spaces, 1), size(spaces, 2)); - for i = 1:length(spaces) - fprintf('%d.\t', i); - disp(spaces(i)); - fprintf('\n'); + dimstring = sprintf("%d", dims(spaces)); + + if kwargs.IncludeType + typestring = name(spaces); + s = sprintf("%s: %s", typestring, dimstring); + else + s = sprintf("%s", dimstring); end end + + function s = name(~) + s = "CartesianSpace"; + end end end diff --git a/src/tensors/spaces/ComplexSpace.m b/src/tensors/spaces/ComplexSpace.m index 04b7da2..6f4a7c9 100644 --- a/src/tensors/spaces/ComplexSpace.m +++ b/src/tensors/spaces/ComplexSpace.m @@ -8,15 +8,15 @@ % % Repeating Arguments % ------------------- - % dimensions : (1, 1) int + % dimensions : (1, 1) :class:`int` % dimension of the vector space. % - % isdual : (1, 1) logical + % isdual : (1, 1) :class:`logical` % flag to denote dual spaces. % % Returns % ------- - % spaces : :class:`ComplexSpace` + % spaces : (1, :) :class:`.ComplexSpace` % array of complex spaces. arguments (Repeating) @@ -41,15 +41,15 @@ % % Repeating Arguments % ------------------- - % dimensions : int or struct + % dimensions : :class:`int` or :class:`struct` % a variable which represents the internal dimension of the space. % - % isdual : logical + % isdual : :class:`logical` % a variable which indicates if a space is dual. % % Returns % ------- - % spaces : :class:`ComplexSpace` + % spaces : :class:`.ComplexSpace` % array of spaces. arguments (Repeating) @@ -74,21 +74,21 @@ %% Structure methods - function c = charges(~) + function c = charges(spaces) % Compute all charge combinations of a space. No internal structure is present, % this yields an empty result. % % Arguments % --------- - % spaces : (1, :) :class:`ComplexSpace` + % spaces : (1, :) :class:`.ComplexSpace` % input spaces. % % Returns % ------- - % c : [] - % empty result. + % c : (1, :) :class:`.Z1` + % array of trivial spaces of corresponding length. - c = []; + c = repmat(Z1, length(spaces), 1); end function d = degeneracies(spaces) @@ -96,12 +96,12 @@ % % Arguments % --------- - % spaces : (1, :) :class:`ComplexSpace` + % spaces : (1, :) :class:`.ComplexSpace` % input spaces. % % Returns % ------- - % d : (1, :) int + % d : (1, :) :class:`int` % list of degeneracy combinations, with 1 element. d = [spaces.dimensions]; @@ -112,46 +112,29 @@ % % Arguments % --------- - % spaces : (1, :) :class:`ComplexSpace` + % spaces : (1, :) :class:`.ComplexSpace` % input spaces. % % Returns % ------- - % d : (1, :) int + % d : (1, :) :class:`int` % total dimension of each of the input spaces. d = [spaces.dimensions]; end - - function trees = fusiontrees(~, ~) - % Compute all allowed fusiontrees that connect domain and codomain. Only the - % trivial fusion tree is allowed, so this returns empty. - % - % Arguments - % --------- - % codomain, domain : :class:`ComplexSpace` - % input spaces. - % - % Returns - % ------- - % trees : :class:`FusionTree` - % only the trivial tree is allowed. - - trees = FusionTree(); - end function style = braidingstyle(~, ~) % Determine the braiding style of the internal structure of a space. % % Arguments % --------- - % codomain, domain : (1, :) :class:`ComplexSpace` + % codomain, domain : (1, :) :class:`.ComplexSpace` % input spaces. % % Returns % ------- - % style : :class:`BraidingStyle` - % Trivial braiding style + % style : :class:`.BraidingStyle` + % trivial braiding style, :code:`BraidingStyle.Abelian`. style = BraidingStyle.Abelian; end @@ -161,16 +144,20 @@ % % Arguments % --------- - % codomain, domain : (1, :) :class:`ComplexSpace` + % codomain, domain : (1, :) :class:`.ComplexSpace` % input spaces. % % Returns % ------- - % style : :class:`FusionStyle` - % fusion style of the internal structure. + % style : :class:`.FusionStyle` + % fusion style of the internal structure, :code:`FusionStyle.Unique`. style = FusionStyle.Unique; end + + function space = one(~) + space = ComplexSpace(1, false); + end end @@ -181,32 +168,54 @@ % % Arguments % --------- - % space1, space2 : (1, 1) :class:`ComplexSpace` + % space1, space2 : (1, 1) :class:`.ComplexSpace` % input spaces. % % Returns % ------- - % space : (1, 1) :class:`ComplexSpace` + % space : (1, 1) :class:`.ComplexSpace` % fused space. space.dimensions = space.dimensions * space2.dimensions; - space.isdual = false; + space.dual = false; end - function space = prod(spaces) + function space = prod(spaces, isdual) % Fuse a product space to a single space. % % Arguments % --------- - % spaces : (1, :) :class:`ComplexSpace` + % spaces : (1, :) :class:`.ComplexSpace` % Array of input spaces. % % Returns % ------- - % space : (1, 1) :class:`ComplexSpace` + % space : (1, 1) :class:`.ComplexSpace` % Fused space which is isomorphic to the input product space. + arguments + spaces + isdual = false + end + space = ComplexSpace(prod(dims(spaces)), isdual); + end + + function space1 = infimum(space1, space2) + assert(isscalar(space1) && isscalar(space2)); + assert(isdual(space1) == isdual(space2)); + space1.dimensions = min(dims(space1), dims(space2)); + end + + function space = plus(space, space2) - space = ComplexSpace(prod(dims(spaces)), false); + assert(isdual(space) == isdual(space2), ... + 'direct sum only defined when isdual is equal.'); + space.dimensions = space.dimensions + space2.dimensions; + end + + function space = sum(spaces) + assert(all(isdual(spaces) == isdual(spaces(1))), ... + 'direct sum only defined when isdual is equal.'); + space = ComplexSpace(sum(dims(spaces)), isdual(spaces(1))); end end @@ -218,12 +227,12 @@ % % Arguments % --------- - % spaces1, spaces2 : (1, :) :class:`ComplexSpace` + % spaces1, spaces2 : (1, :) :class:`.ComplexSpace` % input spaces, either of equal size or scalar. % % Returns % ------- - % bools : (1, :) logical + % bools : (1, :) :class:`logical` % flags that indicate if the element spaces are equal. bools = [spaces1.dimensions] == [spaces2.dimensions] & ... @@ -241,20 +250,47 @@ function hashable = GetMD5_helper(spaces) % Helper function for hash algorithm. This converts the space object to a data - % structure which can be processed by :func:`GetMD5`. + % structure which can be processed by :func:`.GetMD5`. % % Arguments % --------- - % spaces : (1, :) :class:`ComplexSpace` + % spaces : (1, :) :class:`.ComplexSpace` % input spaces. % % Returns % ------- - % hashable : (1, :) int - % data which can be accepted by :func:`GetMD5`. + % hashable : (1, :) :class:`int` + % data which can be accepted by :func:`.GetMD5`. hashable = [spaces.dimensions spaces.isdual]; end + + function s = string(spaces, kwargs) + arguments + spaces + kwargs.IncludeType = true + kwargs.IncludeDetails = true + end + + if numel(spaces) > 1 + kwargs = namedargs2cell(kwargs); + s = arrayfun(@(x) string(x, kwargs{:}), spaces); + return + end + + dimstring = sprintf("%d", dims(spaces)); + if isdual(spaces), dimstring = dimstring + "*"; end + + if kwargs.IncludeType + typestring = name(spaces); + s = sprintf("%s: %s", typestring, dimstring); + else + s = sprintf("%s", dimstring); + end + end + + function s = name(~) + s = "ComplexSpace"; + end end - end \ No newline at end of file diff --git a/src/tensors/spaces/GradedSpace.m b/src/tensors/spaces/GradedSpace.m index 916ca67..358282c 100644 --- a/src/tensors/spaces/GradedSpace.m +++ b/src/tensors/spaces/GradedSpace.m @@ -3,26 +3,26 @@ %% Constructors methods - function spaces = GradedSpace(dimensions, isdual) + function spaces = GradedSpace(dimensions, dual) % Construct an array of vector spaces. % % Repeating Arguments % ------------------- - % dimensions : (1, 1) struct - % internal structure of the vector space, with fields 'charges' and - % 'degeneracies'. + % dimensions : (1, 1) :class:`struct` + % internal structure of the vector space, with fields :code:`charges` and + % :code:`degeneracies`. % - % isdual : (1, 1) logical + % dual : (1, 1) :class:`logical` % flag to denote dual spaces. % % Returns % ------- - % spaces : :class:`GradedSpace` - % array of cartesian spaces. + % spaces : :class:`.GradedSpace` + % array of graded spaces. arguments (Repeating) dimensions (1, 1) struct - isdual (1, 1) logical + dual (1, 1) logical end if nargin == 0 @@ -40,15 +40,41 @@ 'tensors:ArgumentError', ... 'Charges should be unique.'); + assert(all(dimensions{i}.degeneracies >= 0), 'tensors:argerror', ... + 'degeneracies should be positive.'); + mask = dimensions{i}.degeneracies == 0; + if any(mask) + warning('degeneracies should be strictly positive, removing 0.'); + dimensions{i}.charges = dimensions{i}.charges(mask); + dimensions{i}.degeneracies = dimensions{i}.degeneracies(mask); + end [dimensions{i}.charges, I] = sort(dimensions{i}.charges); dimensions{i}.degeneracies = dimensions{i}.degeneracies(I); end - args = [dimensions; isdual]; + args = [dimensions; dual]; end spaces = spaces@AbstractSpace(args{:}); end + + function space = one(spaces) + space = GradedSpace(... + struct('charges', one(spaces(1).dimensions.charges), 'degeneracies', 1), ... + false); + end + + function space = infimum(space1, space2) + assert(isscalar(space1) && isscalar(space2)); + assert(isdual(space1) == isdual(space2)); + + [dims.charges, ia, ib] = intersect(charges(space1), charges(space2)); + d1 = degeneracies(space1); + d2 = degeneracies(space2); + dims.degeneracies = min(d1(ia), d2(ib)); + + space = GradedSpace.new(dims, isdual(space1)); + end end methods (Static) @@ -58,44 +84,47 @@ % % Usage % ----- - % spaces = GradedSpace.new(charges, degeneracies, isdual, ...) + % :code:`spaces = GradedSpace.new(charges, degeneracies, dual, ...)` % - % spaces = GradedSpace.new(dimensions, isdual, ...) + % :code:`spaces = GradedSpace.new(dimensions, dual, ...)` % % Repeating Arguments % ------------------- - % dimensions : struct + % dimensions : :class:`struct` % a variable which represents the internal dimension of the space. % - % charges : :class:`AbstractCharge` + % charges : (1, :) :class:`.AbstractCharge` % charges for the internal structure of the space. % - % degeneracies : int + % degeneracies : (1, :) :class:`int` % degeneracies for the internal structure of the space. % - % isdual : logical + % dual : :class:`logical` % a variable which indicates if a space is dual. % % Returns % ------- - % spaces : :class:`GradedSpace` - % array of spaces. + % spaces : :class:`.GradedSpace` + % array of graded spaces. - if isstruct(varargin{1}) % default + if isstruct(varargin{1}) % default assert(mod(nargin, 2) == 0); + for i = 1:2:nargin + if varargin{i+1}, varargin{i}.charges = conj(varargin{i}.charges); end + end spaces = GradedSpace(varargin{:}); return end if isa(varargin{1}, 'AbstractCharge') - assert(mod(nargin, 3) == 0); + assert(mod(nargin, 3) == 0, 'Unknown caller syntax'); args = cell(2, nargin / 3); for i = 1:size(args, 2) args{1, i} = struct('charges', varargin{3 * i - 2}, ... 'degeneracies', varargin{3 * i - 1}); args{2, i} = varargin{3 * i}; end - spaces = GradedSpace(args{:}); + spaces = GradedSpace.new(args{:}); return end @@ -111,22 +140,23 @@ % % Arguments % --------- - % spaces : (1, :) :class:`GradedSpace` + % spaces : (1, :) :class:`.GradedSpace` % input spaces. % % Returns % ------- - % c : (:, :) :class:`AbstractCharge` - % list of charge combinations, where each row is an entry. + % c : (:, :) :class:`.AbstractCharge` + % list of charge combinations, where each row represents a possible + % combination. - if spaces(1).isdual + if isdual(spaces(1)) c = conj(spaces(1).dimensions.charges); else c = spaces(1).dimensions.charges; end for i = 2:length(spaces) - if spaces(i).isdual + if isdual(spaces(i)) c = combvec(c, conj(spaces(i).dimensions.charges)); else c = combvec(c, spaces(i).dimensions.charges); @@ -139,13 +169,14 @@ % % Arguments % --------- - % spaces : (1, :) :class:`GradedSpace` + % spaces : (1, :) :class:`.GradedSpace` % input spaces. % % Returns % ------- - % d : (:, :) int - % list of degeneracy combinations, where each row is an entry. + % d : (:, :) :class:`int` + % list of degeneracy combinations, where each row represents a possible + % combination. d = spaces(1).dimensions.degeneracies; for i = 2:length(spaces) @@ -158,12 +189,12 @@ % % Arguments % --------- - % spaces : (1, :) :class:`GradedSpace` + % spaces : (1, :) :class:`.GradedSpace` % input spaces. % % Returns % ------- - % d : (1, :) int + % d : (1, :) :class:`int` % total dimension of each of the input spaces. d = zeros(size(spaces)); @@ -173,62 +204,40 @@ end end - function trees = fusiontrees(codomain, domain) - % Compute all allowed fusiontrees that connect domain and codomain. Only the - % trivial fusion tree is allowed, so this returns empty. - % - % Arguments - % --------- - % codomain, domain : :class:`GradedSpace` - % input spaces. - % - % Returns - % ------- - % trees : :class:`FusionTree` - % list of all allowed fusion trees. - - rank = [length(codomain) length(domain)]; - spaces = [codomain flip(domain)]; - - args = cell(2, sum(rank)); - for i = 1:size(args, 2) - args{1, i} = charges(spaces(i)); - args{2, i} = spaces(i).isdual; - end - - trees = FusionTree.new(rank, args{:}); - end - - %% Space manipulations function space = mtimes(space1, space2) % Fuse two spaces to a single space. % % Arguments % --------- - % space1, space2 : (1, 1) :class:`GradedSpace` + % space1, space2 : (1, 1) :class:`.GradedSpace` % input spaces. % % Returns % ------- - % space : (1, 1) :class:`GradedSpace` + % space : (1, 1) :class:`.GradedSpace` % fused space. space = prod([space1, space2]); end - function space = prod(spaces) + function space = prod(spaces, isdual) % Fuse a product space to a single space. % % Arguments % --------- - % spaces : (1, :) :class:`GradedSpace` - % Array of input spaces. + % spaces : (1, :) :class:`.GradedSpace` + % array of input spaces. % % Returns % ------- - % space : (1, 1) :class:`GradedSpace` - % Fused space which is isomorphic to the input product space. + % space : (1, 1) :class:`.GradedSpace` + % fused space which is isomorphic to the input product space. + + arguments + spaces + isdual = false + end if fusionstyle(spaces) == FusionStyle.Unique c = prod(charges(spaces), 1); @@ -254,7 +263,7 @@ newdimensions.degeneracies(i) = sum(d(idx)); end - space = GradedSpace(newdimensions, false); + space = GradedSpace.new(newdimensions, isdual); end @@ -264,12 +273,12 @@ % % Arguments % --------- - % spaces1, spaces2 : (1, :) :class:`GradedSpace` + % spaces1, spaces2 : (1, :) :class:`.GradedSpace` % input spaces, either of equal size or scalar. % % Returns % ------- - % bools : (1, :) logical + % bools : (1, :) :class:`logical` % flags that indicate if the element spaces are equal. if isempty(spaces1) && isempty(spaces2) @@ -277,7 +286,7 @@ return end - bools = [spaces1.isdual] == [spaces2.isdual]; + bools = [spaces1.dual] == [spaces2.dual]; if isscalar(spaces2) for i = 1:length(spaces1) @@ -298,12 +307,17 @@ end end + function bools = ne(spaces1, spaces2) + bools = ~(spaces1 == spaces2); + end + function bool = ge(space1, space2) bool = le(space2, space1); end function bool = le(space1, space2) - assert(isscalar(space1) && isscalar(space2)); + assert(isscalar(space1) && isscalar(space2), 'spaces:scalar', ... + 'method only defined for scalar inputs.'); [lia, locb] = ismember(charges(space1), charges(space2)); if ~all(lia) bool = false; @@ -330,81 +344,78 @@ end end - function bool = hascharge(spaces, charges) - mustBeEqualLength(spaces, charges); - bool = false(1, length(space)); - for i = 1:length(space) - bool(i) = any(space.charges{1} == charge(i)); - end - end - - function space = fuse(spaces) - space = spaces(1); - if length(spaces) == 1 - return - end + function space = plus(space, space2) + assert(isscalar(space) && isscalar(space2)); + assert(space.dual == space2.dual); - allcharges = combcharges(spaces, []).'; - degeneracies = computedegeneracies(spaces, [], allcharges); - - switch fusionstyle(allcharges) - case FusionStyle.Unique - fusedcharges = prod(allcharges, 2); - fusedbonds = prod(degeneracies, 2); - - [newcharges, ~, ic] = unique(fusedcharges); - newbonds = zeros(size(newcharges)); - for i = 1:length(newcharges) - newbonds(i) = sum(fusedbonds(ic == i)); - end - space = GradedSpace(newcharges, newbonds, false); - - otherwise - fusedbonds = prod(degeneracies, 2); - fusedcharges_cell = cell(size(allcharges, 1), 1); - fusedbonds_cell = cell(size(fusedcharges_cell)); - for i = 1:size(allcharges, 1) - [fusedcharges_cell{i}, N] = prod(allcharges(i, :)); - fusedbonds_cell{i} = fusedbonds(i) .* N; - end - - fusedbonds = horzcat(fusedbonds_cell{:}); - [newcharges, ~, ic] = unique(horzcat(fusedcharges_cell{:})); - newbonds = zeros(size(newcharges)); - for i = 1:length(newcharges) - newbonds(i) = sum(fusedbonds(ic == i)); - end - space = GradedSpace(newcharges, newbonds, false); - + c = charges(space2); + d = degeneracies(space2); + [lia, locb] = ismember(c, charges(space)); + for i = find(lia)' + space.dimensions.degeneracies(locb(i)) = d(i) + ... + space.dimensions.degeneracies(locb(i)); end + [space.dimensions.charges, p] = sort([space.dimensions.charges, ... + c(~lia)]); + space.dimensions.degeneracies = [space.dimensions.degeneracies, ... + d(~lia)]; + space.dimensions.degeneracies = space.dimensions.degeneracies(p); end end methods - function disp(spaces) - % Custom display of spaces. - if isscalar(spaces) - fprintf('%s GradedSpace of dimension %d:\n', ... - class(spaces.dimensions.charges), dims(spaces)); - title_str = strjust(pad(["isdual", "charges", "degeneracies"]), 'right'); - charge_str = strjust(pad([string(spaces.dimensions.charges) - string(spaces.dimensions.degeneracies)]), 'center'); - fprintf('\t%s:\t%s\n', title_str(1), string(spaces.isdual)); - fprintf('\t%s:\t%s\n', title_str(2), join(charge_str(1, :), char(9))); - fprintf('\t%s:\t%s\n', title_str(3), join(charge_str(2, :), char(9))); + function s = string(spaces, kwargs) + arguments + spaces + kwargs.IncludeType = true + kwargs.IncludeDetails = true + end + + if numel(spaces) > 1 + kwargs = namedargs2cell(kwargs); + s = arrayfun(@(x) string(x, kwargs{:}), spaces); return end - sz = size(spaces); - assert(length(sz) == 2); - dim_str = sprintf('%dx%d', sz(1), sz(2)); - fprintf('%s ProductSpace with elements:\n\n', dim_str); - for i = 1:length(spaces) - fprintf('%d.\t', i); - disp(spaces(i)); - fprintf('\n'); + dimstring = sprintf("%d", dims(spaces)); + if isdual(spaces), dimstring = dimstring + "*"; end + + if kwargs.IncludeType + typestring = name(spaces); + end + + if kwargs.IncludeDetails + chargestring = "(" + join(compose("%s => %d", ... + string(spaces.dimensions.charges).', ... + spaces.dimensions.degeneracies.'), ', ') + ")"; + end + + if kwargs.IncludeType && kwargs.IncludeDetails + s = sprintf("%s: %s %s", typestring, dimstring, chargestring); + elseif kwargs.IncludeType + s = sprintf("%s: %s", typestring, dimstring); + elseif kwargs.IncludeDetails + s = sprintf("%s %s", dimstring, chargestring); + else + s = dimstring; end end + + function complexspaces = ComplexSpace(gradedspaces) + d = num2cell(dims(gradedspaces)); + isdual = num2cell([gradedspaces.dual]); + args = [d; isdual]; + complexspaces = ComplexSpace(args{:}); + end + + function cartesianspaces = CartesianSpace(gradedspaces) + d = num2cell(dims(gradedspaces)); + cartesianspaces = CartesianSpace(d{:}); + end + + function s = name(spaces) + s = sprintf("%sSpace", name(spaces(1).dimensions.charges)); + end end diff --git a/src/tensors/spaces/SU2Space.m b/src/tensors/spaces/SU2Space.m new file mode 100644 index 0000000..55cf784 --- /dev/null +++ b/src/tensors/spaces/SU2Space.m @@ -0,0 +1,22 @@ +function V = SU2Space(charges, bonds, isdual) +% Convenience constructor for :math:`\mathrm{SU}(2)`-graded spaces. +% +% Arguments +% -------- +% charges : (1, :) :class:`int`-like +% vector of :math:`\mathrm{SU}(2)` charge labels. +% +% bonds : (1, :) :class:`int` +% degeneracy of each charge. +% +% isdual : :class:`logical` +% indicate if the space is dual. +arguments + charges + bonds + isdual = false +end + +V = GradedSpace.new(SU2(charges), bonds, isdual); + +end diff --git a/src/tensors/spaces/SumSpace.m b/src/tensors/spaces/SumSpace.m new file mode 100644 index 0000000..1f50ff1 --- /dev/null +++ b/src/tensors/spaces/SumSpace.m @@ -0,0 +1,195 @@ +classdef (InferiorClasses = {?GradedSpace, ?CartesianSpace, ?ComplexSpace}) SumSpace < AbstractSpace + % Direct sum structure for a tensor index. + + %% Constructors + methods + function spaces = SumSpace(subspaces) + % Construct a direct sum of vector spaces array of vector spaces. + % + % Repeating Arguments + % ------------------- + % subspace : :class:`.AbstractSpace` + % individual subspaces of sum space. + % + % Returns + % ------- + % spaces : :class:`.SumSpace` + % array of spaces representing a direct sum of vector spaces. + + arguments (Repeating) + subspaces + end + + if nargin == 0 || (nargin == 1 && isempty(subspaces{1})) + args = {}; + else + dual = cell(size(subspaces)); + for i = 1:length(subspaces) + dual{i} = isdual(subspaces{i}(1)); + assert(all(isdual(subspaces{i}) == dual{i}), 'Direct sum structure should have all dual or all regular spaces.'); + end + args = [subspaces; dual]; + end + + spaces = spaces@AbstractSpace(args{:}); + if (nargin == 1 && isempty(subspaces{1})) + spaces = spaces.empty(1, 0); + end + end + + function space = one(spaces) + space = SumSpace(arrayfun(@one, spaces(1).dimensions)); + end + + function space = infimum(space1, space2) + arguments + space1 SumSpace + space2 SumSpace + end + assert(isscalar(space1) && isscalar(space2)); + assert(isdual(space1) == isdual(space2)); + space = SumSpace(arrayfun(@infimum, space1.dimensions, space2.dimensions)); + end + end + + + %% Manipulations + methods + function spaces = conj(spaces) + for i = 1:length(spaces) + spaces(i).dual = ~spaces(i).dual; + spaces(i).dimensions = conj(subspaces(spaces(i))); + end + end + + function space = mtimes(space1, space2) + % Fuse two spaces to a single space. + % + % Arguments + % --------- + % space1, space2 : (1, 1) :class:`.SumSpace` + % input spaces. + % + % Returns + % ------- + % space : (1, 1) :class:`.SumSpace` + % fused space. + arguments + space1 SumSpace + space2 SumSpace + end + + space = SumSpace(sum(subspaces(space1)) * sum(subspaces(space2))); + end + end + + %% Utility + methods + function d = dims(spaces) + for i = length(spaces):-1:1 + d(i) = sum(dims(subspaces(spaces(i)))); + end + end + + function bools = eq(spaces1, spaces2) + assert(isequal(size(spaces1), size(spaces2))); + bools = false(size(spaces1)); + for i = 1:numel(bools) + bools(i) = all(subspaces(spaces1(i)) == subspaces(spaces2(i))); + end + end + + function bool = le(space1, space2) + arguments + space1 SumSpace + space2 SumSpace + end + + assert(isscalar(space1) && isscalar(space2)); + + bool = le(sum(subspaces(space1)), sum(subspaces(space2))); + end + + function s = subspaces(space, i) + assert(isscalar(space), ... + 'space:argerror', 'method only defined for scalar inputs.'); + s = space.dimensions; + if nargin > 1 + s = s(i); + end + end + + function n = nsubspaces(space) + n = zeros(size(space)); + for i = 1:numel(n) + n(i) = numel(space(i).dimensions); + end + end + + function [cod, dom] = slice(sumcod, sumdom, I) + assert(length(I) == length(sumcod) + length(sumdom)); + if isempty(sumcod) + cod = []; + else + for i = flip(1:length(sumcod)) + cod(i) = subspaces(sumcod(i), I(i)); + end + end + if isempty(sumdom) + dom = []; + else + for i = flip(1:length(sumdom)) + dom(i) = subspaces(sumdom(i), I(end+1-i)); + end + + end + end + + function disp(spaces) + s = settings; + shortformat = strcmp('short', s.matlab.commandwindow.NumericFormat.ActiveValue); + + if isscalar(spaces) + subsp = subspaces(spaces); + dimstr = num2str(sum(dims(subsp))); + if isdual(spaces), dimstr = dimstr + "*"; end + sz = size(subsp); + szstr = sprintf('%dx%d', sz(1), sz(2)); + fprintf('\t%s %s: %s\n', szstr, name(spaces), dimstr); + if ~shortformat + for i = 1:length(subsp) + fprintf('\t\t%d.\t%s\n', i, string(subsp(i), 'IncludeType', false)); + end + end + fprintf('\n'); + return + end + + sz = size(spaces); + assert(length(sz) == 2); + dim_str = sprintf('%dx%d', sz(1), sz(2)); + fprintf('\t%s Product %s:\n', dim_str, name(spaces)); + for i = 1:length(spaces) + subsp = subspaces(spaces(i)); + dimstr = num2str(sum(dims(subsp))); + if isdual(spaces(i)), dimstr = dimstr + "*"; end + sz = size(subsp); + szstr = sprintf('%dx%d', sz(1), sz(2)); + fprintf('\t\t%d.\t%s: %s\n', i, szstr, dimstr); + + if ~shortformat + for j = 1:length(subsp) + fprintf('\t\t\t%d.\t%s\n', j, ... + string(subsp(j), 'IncludeType', false)); + end + end + end + fprintf('\n'); + end + + function s = name(spaces) + s = sprintf("Sum%s", name(subspaces(spaces(1)))); + end + end +end + diff --git a/src/tensors/spaces/U1Space.m b/src/tensors/spaces/U1Space.m new file mode 100644 index 0000000..334d850 --- /dev/null +++ b/src/tensors/spaces/U1Space.m @@ -0,0 +1,22 @@ +function V = U1Space(charges, bonds, isdual) +% Convenience constructor for :math:`\mathrm{U}(1)`-graded spaces. +% +% Arguments +% -------- +% charges : (1, :) :class:`int`-like +% vector of :math:`\mathrm{U}(1)` charge labels. +% +% bonds : (1, :) :class:`int` +% degeneracy of each charge. +% +% isdual : :class:`logical` +% indicate if the space is dual. +arguments + charges + bonds + isdual = false +end + +V = GradedSpace.new(U1(charges), bonds, isdual); + +end diff --git a/src/tensors/spaces/Z2Space.m b/src/tensors/spaces/Z2Space.m new file mode 100644 index 0000000..cb522c3 --- /dev/null +++ b/src/tensors/spaces/Z2Space.m @@ -0,0 +1,22 @@ +function V = Z2Space(charges, bonds, isdual) +% Convenience constructor for :math:`Z_2`-graded spaces. +% +% Arguments +% -------- +% charges : (1, :) :class:`logical`-like +% vector of :math:`Z_2` charge labels. +% +% bonds : (1, :) :class:`int` +% degeneracy of each charge. +% +% isdual : :class:`logical` +% indicate if the space is dual. +arguments + charges + bonds + isdual = false +end + +V = GradedSpace.new(Z2(charges), bonds, isdual); + +end diff --git a/src/tensors/spaces/fZ2Space.m b/src/tensors/spaces/fZ2Space.m new file mode 100644 index 0000000..ceeb035 --- /dev/null +++ b/src/tensors/spaces/fZ2Space.m @@ -0,0 +1,22 @@ +function V = fZ2Space(charges, bonds, isdual) +% Convenience constructor for :math:`fZ_2`-graded spaces. +% +% Arguments +% -------- +% charges : (1, :) :class:`logical`-like +% vector of :math:`fZ_2` charge labels. +% +% bonds : (1, :) :class:`int` +% degeneracy of each charge. +% +% isdual : :class:`logical` +% indicate if the space is dual. +arguments + charges + bonds + isdual = false +end + +V = GradedSpace.new(fZ2(charges), bonds, isdual); + +end diff --git a/src/utility/Options.m b/src/utility/Options.m index 2a65273..39c12b6 100644 --- a/src/utility/Options.m +++ b/src/utility/Options.m @@ -1,9 +1,15 @@ classdef Options % Various package settings. - methods (Static) function bool = CacheEnabled(bool) + % Enable cache. + % + % Usage + % ----- + % :code:`bool = Options.CacheEnabled(bool)` sets the cache enabling to :code:`bool`. + % + % :code:`bool = Options.CacheEnabled(bool)` returns the current cache enabling. persistent isenabled if nargin > 0 isenabled = bool; @@ -13,6 +19,24 @@ if isempty(isenabled), isenabled = true; end bool = isenabled; end + + function bool = Debug(bool) + % Enable cache. + % + % Usage + % ----- + % :code:`bool = Options.Debug(bool)` sets the debug mode to :code:`bool`. + % + % :code:`bool = Options.Debug(bool)` returns the current debug mode. + persistent dodebug + if nargin > 0 + dodebug = bool; + return + end + + if isempty(dodebug), dodebug = false; end + bool = dodebug; + end end end diff --git a/src/utility/Verbosity.m b/src/utility/Verbosity.m new file mode 100644 index 0000000..35e972a --- /dev/null +++ b/src/utility/Verbosity.m @@ -0,0 +1,22 @@ +classdef Verbosity < uint8 + % Verbosity level enumeration class. + % + % Levels: + % + % - off (0): No information + % - warn (1): Information on failure + % - conv (2): Convergence information + % - iter (3): Information about each iteration + % - detail (4): Detailed information about each iteration + % - diagnostics (255): All possible information + + enumeration + off (0) % No information + warn (1) % Information on failure + conv (2) % Convergence information + iter (3) % Information about each iteration + detail (4) % Detailed information about each iteration + diagnostics (intmax('uint8')) % all possible information + end +end + diff --git a/src/utility/between.m b/src/utility/between.m new file mode 100644 index 0000000..9425644 --- /dev/null +++ b/src/utility/between.m @@ -0,0 +1,11 @@ +function x = between(x1, x, x2) +% Restrict :code:`x` to lie between :code:`x1` and :code:`x2` + +assert(x1 <= x2, 'range', 'x1 should be smaller than or equal to x2'); +if x < x1 + x = x1; +elseif x > x2 + x = x2; +end + +end \ No newline at end of file diff --git a/src/utility/clear_path.m b/src/utility/clear_path.m new file mode 100644 index 0000000..41b1a89 --- /dev/null +++ b/src/utility/clear_path.m @@ -0,0 +1,20 @@ +function clear_path(filename) + + +if isfile(filename) + [filepath, name, ext] = fileparts(filename); + + pat = "_backup" + digitsPattern; + if endsWith(name, pat) + number = extractAfter(name, "_backup"); + newname = replace(name, pat, "_backup" + (double(number) + 1)); + else + newname = name + "_backup1"; + end + newfilename = fullfile(filepath, newname + ext); + clear_path(newfilename); + movefile(filename, newfilename); +end + + +end diff --git a/src/utility/colors.m b/src/utility/colors.m new file mode 100644 index 0000000..446de07 --- /dev/null +++ b/src/utility/colors.m @@ -0,0 +1,16 @@ +function y = colors(i) +% Periodically cycle through standard Matlab colors. + +y=[0 0.4470 0.7410 + 0.8500 0.3250 0.0980 + 0.9290 0.6940 0.1250 + 0.4940 0.1840 0.5560 + 0.4660 0.6740 0.1880 + 0.3010 0.7450 0.9330 + 0.6350 0.0780 0.1840 + 0.2500 0.2500 0.2500]; + +i = 1 + mod(i - 1, size(y, 1)); +y = y(i, :); + +end \ No newline at end of file diff --git a/src/utility/dim2str.m b/src/utility/dim2str.m new file mode 100644 index 0000000..4d1b693 --- /dev/null +++ b/src/utility/dim2str.m @@ -0,0 +1,7 @@ +function s = dim2str(sz) +% Convert size array to string output. + +s = regexprep(mat2str(sz), {'\[', '\]', '\s+'}, {'', '', 'x'}); + +end + diff --git a/src/utility/diracdelta.m b/src/utility/diracdelta.m new file mode 100644 index 0000000..93f40b2 --- /dev/null +++ b/src/utility/diracdelta.m @@ -0,0 +1,13 @@ +function d = diracdelta(sz) +% Construct delta tensor of given size. Note that all dimensions must have the same length. + +assert(all(sz == sz(1))) +d = zeros(sz); + +subs = repmat(1:sz(1), length(sz), 1).'; +idx = sub2ind_(sz, subs); + +d(idx) = 1; + +end + diff --git a/src/utility/indices/contractinds.m b/src/utility/indices/contractinds.m index 627586f..726e387 100644 --- a/src/utility/indices/contractinds.m +++ b/src/utility/indices/contractinds.m @@ -1,7 +1,8 @@ function [dimA, dimB] = contractinds(ia, ib) -% contractinds - Find the contracted dimensions. -% [dimA, dimB] = contractinds(ia, ib) -% locates the repeated indices. +% Find the contracted dimensions. +% +% :code:`[dimA, dimB] = contractinds(ia, ib)` +% locates the repeated indices in two vectors of integers. ind = find(ia(:) == ib).' - 1; dimA = mod(ind, length(ia)) + 1; diff --git a/src/utility/indices/mod1.m b/src/utility/indices/mod1.m index 3c0207a..2b86986 100644 --- a/src/utility/indices/mod1.m +++ b/src/utility/indices/mod1.m @@ -1,17 +1,17 @@ function m = mod1(x, y) -% Modulus after division, counting from 1:y. +% Modulus after division, counting from :code:`1:y`. % % Arguments % --------- -% x : int +% x : :class:`int` % numerator. % -% y : int +% y : :class:`int` % divisor. % % Returns % ------- -% m : int +% m : :class:`int` % remainder after division, where a value of 0 is replaced with y. m = mod(x-1, y) + 1; diff --git a/src/utility/indices/next.m b/src/utility/indices/next.m new file mode 100644 index 0000000..cfa0afb --- /dev/null +++ b/src/utility/indices/next.m @@ -0,0 +1,16 @@ +function j = next(i, total) +% Give the next index in a cyclic loop. +% +% Usage +% ----- +% :code:`j = next(i, total)` +% gives the :math:`i + 1`'th index, but loops back to 1 when :math:`j > \text{total}`. +% +% See Also +% -------- +% :func:`.prev` + +j = mod(i, total) + 1; + +end + diff --git a/src/utility/indices/prev.m b/src/utility/indices/prev.m new file mode 100644 index 0000000..3489dd7 --- /dev/null +++ b/src/utility/indices/prev.m @@ -0,0 +1,15 @@ +function j = prev(i, total) +% Give the previous index in a cyclic loop. +% +% Usage +% ----- +% :code:`j = prev(i, total)` +% gives the :math:`i - 1`'th index, but loops back to total when :math:`j < 1`. +% +% See Also +% -------- +% :func:`.next` + +j = mod(i - 2, total) + 1; + +end diff --git a/src/utility/indices/rankrange.m b/src/utility/indices/rankrange.m index 8751470..153bc01 100644 --- a/src/utility/indices/rankrange.m +++ b/src/utility/indices/rankrange.m @@ -1,6 +1,5 @@ function r = rankrange(rank) -%RANKRANGE Summary of this function goes here -% Detailed explanation goes here +% Convert tensor rank into a contiguous range of dimensions. r = [1:rank(1) rank(1) + (rank(2):-1:1)]; diff --git a/src/utility/indices/traceinds.m b/src/utility/indices/traceinds.m index 8340e66..6d75ffe 100644 --- a/src/utility/indices/traceinds.m +++ b/src/utility/indices/traceinds.m @@ -1,4 +1,8 @@ function [dimA1, dimA2] = traceinds(ia) +% Find the traced dimensions. +% +% :code:`[dimA1, dimA2] = traceinds(ia)` +% locates the repeated indices in a vectors of integers. ind = find(ia(:) == ia & ~tril(true(length(ia)))).' - 1; dimA1 = mod(ind, length(ia)) + 1; diff --git a/src/utility/indices/treeindsequence.m b/src/utility/indices/treeindsequence.m index 28fd144..d570059 100644 --- a/src/utility/indices/treeindsequence.m +++ b/src/utility/indices/treeindsequence.m @@ -3,22 +3,28 @@ % % Usage % ----- -% ```t = treeindsequence(n)``` +% :code:`t = treeindsequence(n)` % % Arguments % --------- -% n : int +% n : :class:`int` % number of external edges % % Returns % ------- -% t : int +% t : :class:`int` % total number of edges % -% Examples -% -------- -% ```treeindsequence(0:4)``` -% ans = [0 1 2 4 6] +% Example +% ------- +% +% .. code-block:: matlab +% +% >> treeindsequence(0:4) +% +% ans = +% +% 0 1 2 4 6 n = max(n, 2 * n - 2); diff --git a/src/utility/indices/unique1.m b/src/utility/indices/unique1.m index e2621af..f5cb5d8 100644 --- a/src/utility/indices/unique1.m +++ b/src/utility/indices/unique1.m @@ -1,7 +1,10 @@ function inds = unique1(inds) -% unique1 - Find the elements that appear exactly once. -% inds = unique1(inds) -% deletes all elements that appear more than once. +% Find the elements that appear exactly once. +% +% Usage +% ----- +% :code:`inds = unique1(inds)` +% deletes all elements that appear more than once. inds = inds(sum(inds(:) == inds) == 1); diff --git a/src/utility/jl2mat.m b/src/utility/jl2mat.m new file mode 100644 index 0000000..dd1ef29 --- /dev/null +++ b/src/utility/jl2mat.m @@ -0,0 +1,43 @@ +function obj = jl2mat(obj) +% Recursively convert a Julia object back into something read by TensorTrack. + +if isstruct(obj) + fns = fieldnames(obj); + for i = 1:numel(obj) + for fn = fns' + obj(i).(fn{1}) = jl2mat(obj(i).(fn{1})); + end + end + if isfield(obj, 'classname') + obj = str2obj(obj); + end +elseif iscell(obj) + obj = cellfun(@jl2mat, obj, 'UniformOutput', false); +end + +end + +function o = str2obj(s) + +if numel(s) > 1 + for i = numel(s):-1:1 + o(i) = str2obj(s(i)); + end + o = reshape(o, size(s)); + return +end + +if ismember('Data', fieldnames(s)) + o = feval(s.classname, s.Data); +else + o = feval(s.classname); +end + +for name = fieldnames(s)' + if strcmp(name{1}, 'classname') || strcmp(name{1}, 'Data') + continue; + end + o.(name{1}) = s.(name{1}); +end + +end \ No newline at end of file diff --git a/src/utility/linalg/contract.m b/src/utility/linalg/contract.m index 7deb1e0..8da895a 100644 --- a/src/utility/linalg/contract.m +++ b/src/utility/linalg/contract.m @@ -11,23 +11,23 @@ % % Repeating Arguments % ------------------- -% tensors : :class:`Tensor` +% tensors : :class:`.Tensor` % list of tensors that constitute the vertices of the network. % -% indices : int -% list of indices that define the links and contraction order, using ncon-like syntax. +% indices : (1, :) :class:`int` +% list of indices that define the links and contraction order, using `ncon-like syntax `_. % % Keyword Arguments % ----------------- -% Conj : (1, :) logical +% Conj : (1, :) :class:`logical` % optional list to flag that tensors should be conjugated. % -% Rank : (1, 2) int +% Rank : (1, 2) :class:`int` % optionally specify the rank of the resulting tensor. % % Returns % ------- -% C : :class:`Tensor` or numeric +% C : :class:`.Tensor` or :class:`numeric` % result of the tensor network contraction. % TODO contraction order checker, order specifier. @@ -40,33 +40,46 @@ arguments kwargs.Conj (1, :) logical = false(size(tensors)) kwargs.Rank = [] + kwargs.Debug = false + kwargs.CheckOptimal = false end assert(length(kwargs.Conj) == length(tensors)); -for i = 1:length(tensors) - if length(indices{i}) > 1 - assert(length(unique(indices{i})) == length(indices{i}), ... - 'Tensors:TBA', 'Traces not implemented.'); +if kwargs.CheckOptimal + legcosts = zeros(2, 0); + for i = 1:length(indices) + legcosts = [legcosts [indices{i}; size(tensors{i}, 1:length(indices{i}))]]; + end + legcosts = unique(legcosts.', 'rows'); + + currentcost = contractcost(indices, legcosts); + [sequence, cost] = netcon(indices, 1, 1, currentcost, 1, legcosts); + + if cost < currentcost + warning('suboptimal contraction order.\n optimal: %s', ... + num2str(sequence)); end end +for i = 1:length(tensors) + [i1, i2] = traceinds(indices{i}); + tensors{i} = tensortrace(tensors{i}, i1, i2); + indices{i}([i1 i2]) = []; +end + +debug = kwargs.Debug; + % Special case for single input tensor if nargin == 2 - [~, order] = sort(indices{1}, 'descend'); C = tensors{1}; - if isnumeric(C) + if kwargs.Conj, C = conj(C); end + + if ~isempty(indices{1}) + [~, order] = sort(indices{1}, 'descend'); C = permute(C, order); - if kwargs.Conj - C = conj(C); - end - else - if kwargs.Conj - C = permute(C', order(length(order):-1:1), kwargs.Rank); - else - C = permute(C, order, kwargs.Rank); - end end + return end @@ -76,11 +89,14 @@ tree = generatetree(partialtrees, contractindices); % contract all subtrees -[A, ia, ca] = contracttree(tensors, indices, kwargs.Conj, tree{1}); -[B, ib, cb] = contracttree(tensors, indices, kwargs.Conj, tree{2}); +[A, ia, ca] = contracttree(tensors, indices, kwargs.Conj, tree{1}, debug); +[B, ib, cb] = contracttree(tensors, indices, kwargs.Conj, tree{2}, debug); % contract last pair [dimA, dimB] = contractinds(ia, ib); + +if debug, contractcheck(A, ia, ca, B, ib, cb); end + C = tensorprod(A, B, dimA, dimB, ca, cb, 'NumDimensionsA', length(ia)); ia(dimA) = []; ib(dimB) = []; ic = [ia ib]; @@ -88,57 +104,7 @@ % permute last tensor if ~isempty(ic) && length(ic) > 1 [~, order] = sort(ic, 'descend'); - if isnumeric(C) - C = permute(C, order); - else - if isempty(kwargs.Rank) - kwargs.Rank = [length(order) 0]; - end - C = permute(C, order, kwargs.Rank); - end -end - -end - -function tree = generatetree(partialtrees, contractindices) -if length(partialtrees) == 1 - tree = partialtrees{1}; - return -end - -if all(cellfun('isempty', contractindices)) % disconnected network - partialtrees{end - 1} = partialtrees(end - 1:end); - partialtrees(end) = []; - contractindices(end) = []; -else - tocontract = min(horzcat(contractindices{:})); - tinds = find(cellfun(@(x) any(tocontract == x), contractindices)); - assert(length(tinds) == 2); - partialtrees{tinds(1)} = partialtrees(tinds); - partialtrees(tinds(2)) = []; - contractindices{tinds(1)} = unique1(horzcat(contractindices{tinds})); - contractindices(tinds(2)) = []; -end - -tree = generatetree(partialtrees, contractindices); -end - -function [C, ic, cc] = contracttree(tensors, indices, conjlist, tree) - -if isnumeric(tree) - C = tensors{tree}; - ic = indices{tree}; - cc = conjlist(tree); - return + C = permute(C, order); end -[A, ia, ca] = contracttree(tensors, indices, conjlist, tree{1}); -[B, ib, cb] = contracttree(tensors, indices, conjlist, tree{2}); -[dimA, dimB] = contractinds(ia, ib); -C = tensorprod(A, B, dimA, dimB, ca, cb, 'NumDimensionsA', length(ia)); - -ia(dimA) = []; -ib(dimB) = []; -ic = [ia ib]; -cc = false; end diff --git a/src/utility/linalg/contractcheck.m b/src/utility/linalg/contractcheck.m new file mode 100644 index 0000000..67d6608 --- /dev/null +++ b/src/utility/linalg/contractcheck.m @@ -0,0 +1,16 @@ +function contractcheck(A, ia, ca, B, ib, cb) + +Aspaces = space(A); +if ca, Aspaces = conj(Aspaces); end +Bspaces = space(B); +if cb, Bspaces = conj(Bspaces); end + +[dimA, dimB] = contractinds(ia, ib); + +for i = 1:length(dimA) + assert(Aspaces(dimA(i)) == conj(Bspaces(dimB(i))), 'tensors:SpaceMismatch', ... + 'Invalid index %d:\n\t%s\n\tis incompatible with\n\t%s', ... + ia(dimA(i)), string(Aspaces(dimA(i))), string(Bspaces(dimB(i)))); +end + +end \ No newline at end of file diff --git a/src/utility/linalg/contractcost.m b/src/utility/linalg/contractcost.m new file mode 100644 index 0000000..1a7b0b7 --- /dev/null +++ b/src/utility/linalg/contractcost.m @@ -0,0 +1,73 @@ +function cost = contractcost(indices, legCosts) + +cost = 0; + +allInds = horzcat(indices{:}); +numCont = max(allInds); + +table = zeros(numCont, 2); +for ii = 1:length(indices) + for jj = indices{ii}(indices{ii} > 0) + if table(jj, 1) == 0 + table(jj, 1) = ii; + else + table(jj, 2) = ii; + end + end +end + + +%% Do contractions +ctr = 1; +contlist = 1:numCont; + +while ~isempty(contlist) + i1 = table(contlist(1), 1); + i2 = table(contlist(1), 2); + + if i1 + i2 == 0 + contlist(1) = []; + continue; + else + assert(i1 ~= i2, 'Tensor:isOptimalContract', 'Traces are not implemented.'); + end + + labels1 = indices{i1}; + labels2 = indices{i2}; + + [pos1, pos2] = contractinds(labels1, labels2); + unc1 = 1:length(labels1); unc1(pos1) = []; + unc2 = 1:length(labels2); unc2(pos2) = []; + contracting = labels1(pos1); + + % cost is dim(unc1) * dim(pos1) * dim(unc2) + dims1 = zeros(1, length(unc1)); + for ii = 1:length(unc1) + label = labels1(unc1(ii)); + dims1(ii) = legCosts(legCosts(:, 1) == label, 2); + end + dims2 = zeros(1, length(pos1)); + for ii = 1:length(pos1) + label = labels1(pos1(ii)); + dims2(ii) = legCosts(legCosts(:, 1) == label, 2); + end + dims3 = zeros(1, length(unc2)); + for ii = 1:length(unc2) + label = labels2(unc2(ii)); + dims3(ii) = legCosts(legCosts(:, 1) == label, 2); + end + + cost = cost + prod([dims1 dims2 dims3]); + + % update remaining contractions + indices{i1} = [indices{i1}(unc1) indices{i2}(unc2)]; + indices(i2) = []; + table(table == i2) = i1; + table(table > i2) = table(table > i2) - 1; + contlist = contlist(~ismember(contlist, contracting)); + + ctr = ctr + 1; +end + + +end diff --git a/src/utility/linalg/contracttree.m b/src/utility/linalg/contracttree.m new file mode 100644 index 0000000..4ead6a4 --- /dev/null +++ b/src/utility/linalg/contracttree.m @@ -0,0 +1,25 @@ +function [C, ic, cc] = contracttree(tensors, indices, conjlist, tree, debug) + +if isnumeric(tree) + C = tensors{tree}; + ic = indices{tree}; + cc = conjlist(tree); + return +end + +[A, ia, ca] = contracttree(tensors, indices, conjlist, tree{1}, debug); +[B, ib, cb] = contracttree(tensors, indices, conjlist, tree{2}, debug); +[dimA, dimB] = contractinds(ia, ib); + +if debug + contractcheck(A, ia, ca, B, ib, cb); +end + +C = tensorprod(A, B, dimA, dimB, ca, cb, 'NumDimensionsA', length(ia)); + +ia(dimA) = []; +ib(dimB) = []; +ic = [ia ib]; +cc = false; + +end diff --git a/src/utility/linalg/eigsolve.m b/src/utility/linalg/eigsolve.m new file mode 100644 index 0000000..9c1ae78 --- /dev/null +++ b/src/utility/linalg/eigsolve.m @@ -0,0 +1,114 @@ +function varargout = eigsolve(A, v, howmany, sigma, options) +% Find a few eigenvalues and eigenvectors of an operator. +% +% Usage +% ----- +% :code:`[V, D, flag] = eigsolve(A, v, howmany, sigma, kwargs)` +% +% :code:`D = eigsolve(A, v, ...)` +% +% Arguments +% --------- +% A : :class:`matrix` or :class:`function_handle` +% A square matrix. +% A function handle which implements one of the following, depending on sigma: +% +% - :code:`A \ x`, if `sigma` is 0 or 'smallestabs' +% - :code:`(A - sigma * I) \ x`, if sigma is a nonzero scalar +% - :code:`A * x`, for all other cases +% +% v : :class:`vector` +% initial guess for the eigenvector. If A is a :class:`.Tensor`, this defaults +% to a random complex :class:`.Tensor`, for function handles this is a required +% argument. +% +% howmany : :class:`int` +% amount of eigenvalues and eigenvectors that should be computed. By default +% this is 1, and this should not be larger than the total dimension of A. +% +% sigma : :class:`char` or :class:`numeric` +% selector for the eigenvalues, should be either one of the following: +% +% - 'largestabs', 'lm': default, eigenvalues of largest magnitude +% - 'largestreal', 'lr': eigenvalues with largest real part +% - 'largestimag', 'li': eigenvalues with largest imaginary part. +% - 'smallestabs', 'sm': default, eigenvalues of smallest magnitude +% - 'smallestreal', 'sr': eigenvalues with smallest real part +% - 'smallestimag', 'si': eigenvalues with smallest imaginary part. +% - numeric : eigenvalues closest to sigma. +% +% Keyword Arguments +% ----------------- +% Tol : :class:`double` +% tolerance of the algorithm. +% +% Algorithm : :class:`char` +% choice of eigensolver algorithm. Currently there is a choice between the use +% of Matlab's buitin `eigs` specified by the identifiers 'eigs' or +% 'KrylovSchur', or the use of a custom Arnolid algorithm specified by +% the identifier 'Arnoldi'. +% +% MaxIter : :class:`int` +% maximum number of iterations, 100 by default. +% +% KrylovDim : :class:`int` +% number of vectors kept in the Krylov subspace. +% +% IsSymmetric : :class:`logical` +% flag to speed up the algorithm if the operator is symmetric, false by +% default. +% +% Verbosity : :class:`.Verbosity` +% Level of output information, by default nothing is printed if `flag` is +% returned, otherwise only warnings are given, defaults to :code:`Verbosity.warn`. +% +% Returns +% ------- +% V : (1, howmany) :class:`vector` +% vector of eigenvectors. +% +% D : :class:`numeric` +% vector of eigenvalues if only a single output argument is asked, diagonal +% matrix of eigenvalues otherwise. +% +% flag : :class:`int` +% if flag = 0 then all eigenvalues are converged, otherwise not. + +arguments + A + v + howmany = 1 + sigma = 'lm' + + options.Algorithm {mustBeMember(options.Algorithm, ... + {'eigs', 'KrylovSchur', 'Arnoldi'})} = 'Arnoldi' + + options.Tol = eps(underlyingType(v))^(3/4) + options.MaxIter = 100 + options.KrylovDim = 20 + options.DeflateDim + options.ReOrth = 2 + options.NoBuild + options.Verbosity = Verbosity.warn + options.IsSymmetric logical = false +end + +switch options.Algorithm + + case {'Arnoldi'} + alg_opts = rmfield(options, {'Algorithm', 'IsSymmetric'}); + kwargs = namedargs2cell(alg_opts); + alg = Arnoldi(kwargs{:}); + [varargout{1:nargout}] = eigsolve(alg, A, v, howmany, sigma); + + case {'eigs', 'KrylovSchur'} + alg_opts = rmfield(options, ... + {'Algorithm', 'DeflateDim', 'ReOrth', 'NoBuild', 'IsSymmetric'}); + kwargs = namedargs2cell(alg_opts); + alg = KrylovSchur(kwargs{:}); + [varargout{1:nargout}] = eigsolve(alg, A, v, howmany, sigma, ... + 'IsSymmetric', options.IsSymmetric); + +end + +end diff --git a/src/utility/linalg/generatetree.m b/src/utility/linalg/generatetree.m new file mode 100644 index 0000000..1333de7 --- /dev/null +++ b/src/utility/linalg/generatetree.m @@ -0,0 +1,30 @@ +function tree = generatetree(partialtrees, contractindices) + +if length(partialtrees) == 1 + tree = partialtrees{1}; + return +end + +if all(cellfun('isempty', contractindices)) % disconnected network + partialtrees{end - 1} = partialtrees(end - 1:end); + partialtrees(end) = []; + contractindices(end) = []; +else + tocontract = min(horzcat(contractindices{:})); + tinds = find(cellfun(@(x) any(tocontract == x), contractindices)); + + if length(tinds) ~= 2 + celldisp(contractindices); + error('contract:indices', ... + 'contracted indices should appear exactly twice.\n(%d)', ... + tocontract); + end + partialtrees{tinds(1)} = partialtrees(tinds); + partialtrees(tinds(2)) = []; + contractindices{tinds(1)} = unique1(horzcat(contractindices{tinds})); + contractindices(tinds(2)) = []; +end + +tree = generatetree(partialtrees, contractindices); + +end diff --git a/src/utility/linalg/isapprox.m b/src/utility/linalg/isapprox.m index 91ea4c2..f2de936 100644 --- a/src/utility/linalg/isapprox.m +++ b/src/utility/linalg/isapprox.m @@ -1,4 +1,23 @@ function bool = isapprox(A, B, tol) +% Verify whether two arrays are approximately equal, based on their Euclidean distance. +% +% Arguments +% --------- +% A, B : :class:`numeric` +% input arrays of the same size. +% +% Keyword Arguments +% ----------------- +% AbsTol : :class:`double` +% absolute tolerance. +% +% RelTol : :code:`double` +% relative tolerance. +% +% Returns +% ------- +% bool : :code:`logical` +% true if :code:`A` and :code:`B` are approximately equal. arguments A @@ -8,6 +27,7 @@ tol.AbsTol = 0 end -assert(all(size(A) == size(B)), 'Incompatible sizes'); -bool = distance(A, B) <= max(tol.AbsTol, tol.RelTol * max(norm(A(:)), norm(B(:)))); +% assert(all(size(A) == size(B)), 'Incompatible sizes'); +bool = distance(A, B) <= max(tol.AbsTol, tol.RelTol * ... + max(norm(reshape(A, 1, [])), norm(reshape(B, 1, [])))); end diff --git a/src/utility/linalg/isisometry.m b/src/utility/linalg/isisometry.m index a4fc99e..f8026a0 100644 --- a/src/utility/linalg/isisometry.m +++ b/src/utility/linalg/isisometry.m @@ -3,24 +3,24 @@ % % Arguments % --------- -% A : numeric +% A : :class:`numeric` % input matrix. % -% side : 'left', 'right' or 'both' -% check if A' * A == I, A * A' == I, or both by default. +% side : :class:`char`, 'left', 'right' or 'both' +% check if :code:`A' * A == I`, :code:`A * A' == I`, or both by default. % % Keyword Arguments % ----------------- -% AbsTol : double +% AbsTol : :class:`double` % absolute tolerance % -% RelTol : double +% RelTol : :code:`double` % relative tolerance % % Returns % ------- -% bool : logical -% true if A is an isometry. +% bool : :code:`logical` +% true if :code:`A` is an isometry. arguments A diff --git a/src/utility/linalg/leftnull.m b/src/utility/linalg/leftnull.m index c547045..483dffa 100644 --- a/src/utility/linalg/leftnull.m +++ b/src/utility/linalg/leftnull.m @@ -1,20 +1,21 @@ function N = leftnull(A, alg, atol) -% Compute the left nullspace of a matrix, such that N' * A = 0. +% Compute the left nullspace of a matrix, such that :code:`N' * A == 0`. % % Arguments % --------- -% A : numeric +% A : :class:`numeric` % input matrix. % -% alg : 'qr' or 'svd' +% alg : :class:`char`, 'qr' or 'svd' % choice of algorithm, default 'svd'. % -% atol : double -% absolute tolerance for the null space, defaults to max(size(A)) * eps(max(svd(A))). +% atol : :class:`double` +% absolute tolerance for the null space, defaults to +% :code:`max(size(A)) * eps(max(svd(A)))`. % % Returns % ------- -% N : numeric +% N : :class:`numeric` % orthonormal basis for the orthogonal complement of the support of the row space of A. arguments diff --git a/src/utility/linalg/leftorth.m b/src/utility/linalg/leftorth.m index 28bd781..82b00fe 100644 --- a/src/utility/linalg/leftorth.m +++ b/src/utility/linalg/leftorth.m @@ -1,26 +1,42 @@ function [Q, R] = leftorth(A, alg) - - +% Factorize a matrix into an orthonormal basis `Q` and remainder `R`, such that +% :code:`A = Q * R`. +% +% Usage +% ----- +% :code:`[Q, R] = leftorth(A, alg)` +% +% Arguments +% --------- +% A : :class:`numeric` +% input matrix to factorize. +% +% alg : :class:`char` or :class:`string` +% selection of algorithms for the decomposition: +% +% - 'qr' produces an upper triangular remainder R +% - 'qrpos' corrects the diagonal elements of R to be positive. +% - 'ql' produces a lower triangular remainder R +% - 'qlpos' corrects the diagonal elements of R to be positive. +% - 'polar' produces a Hermitian and positive semidefinite R. +% - 'svd' uses a singular value decomposition. +% +% Returns +% ------- +% Q : :class:`numeric` +% orthonormal basis matrix +% +% R : :class:`numeric` +% remainder matrix, depends on selected algorithm. % TODO have a look at https://github.com/iwoodsawyer/factor - switch alg case 'qr' [Q, R] = qr(A, 0); case 'qrpos' - [Q, R] = qr(A, 0); - if isrow(Q) - Q = Q * sign(R(1)); - R = R * sign(R(1)); - else - D = diag(R); - D(abs(D) < 1e-12) = 1; - D = sign(D); - Q = Q .* D'; - R = D .* R; - end + [Q, R] = qrpos(A, 0); case 'ql' [Q, R] = qr(flip(A, 2), 0); diff --git a/src/utility/linalg/qrpos.m b/src/utility/linalg/qrpos.m new file mode 100644 index 0000000..d6ba3fd --- /dev/null +++ b/src/utility/linalg/qrpos.m @@ -0,0 +1,16 @@ +function [Q, R] = qrpos(A, varargin) +% Positive orthogonal-triangular decomposition. + +[Q, R] = qr(A, varargin{:}); +if isrow(Q) + Q = Q * sign(R(1)); + R = R * sign(R(1)); +else + D = diag(R); + D(abs(D) < 1e-12) = 1; + D = sign(D); + Q = Q .* D'; + R = D .* R; +end + +end diff --git a/src/utility/linalg/rightnull.m b/src/utility/linalg/rightnull.m index f891242..0ad7412 100644 --- a/src/utility/linalg/rightnull.m +++ b/src/utility/linalg/rightnull.m @@ -1,21 +1,23 @@ function N = rightnull(A, alg, atol) -% Compute the right nullspace of a matrix, such that A * N' = 0. +% Compute the right nullspace of a matrix, such that :code:`A * N' == 0`. % % Arguments % --------- -% A : numeric +% A : :class:`numeric` % input matrix. % -% alg : 'lq' or 'svd' +% alg : :class:`char`, 'lq' or 'svd' % choice of algorithm, default 'svd'. % -% atol : double -% absolute tolerance for the null space, defaults to max(size(A)) * eps(max(svd(A))). +% atol : :class:`double` +% absolute tolerance for the null space, defaults to +% :code:`max(size(A)) * eps(max(svd(A)))`. % % Returns % ------- -% N : numeric -% orthonormal basis for the orthogonal complement of the support of the column space of A. +% N : :class:`numeric` +% orthonormal basis for the orthogonal complement of the support of the column space of +% :code:`A`. arguments A diff --git a/src/utility/linalg/rightorth.m b/src/utility/linalg/rightorth.m index ca2ac50..3e54e2d 100644 --- a/src/utility/linalg/rightorth.m +++ b/src/utility/linalg/rightorth.m @@ -1,7 +1,33 @@ function [R, Q] = rightorth(A, alg) - - - +% Factorize a matrix into an orthonormal basis `Q` and remainder `L`, such that +% :code:`A = L * Q`. +% +% Usage +% ----- +% :code:`[R, Q] = rightorth(A, alg)` +% +% Arguments +% --------- +% A : :class:`numeric` +% input matrix to factorize. +% +% alg : :class:`char` or :class:`string` +% selection of algorithms for the decomposition: +% +% - 'rq' produces an upper triangular remainder R +% - 'rqpos' corrects the diagonal elements of R to be positive. +% - 'lq' produces a lower triangular remainder R +% - 'lqpos' corrects the diagonal elements of R to be positive. +% - 'polar' produces a Hermitian and positive semidefinite R. +% - 'svd' uses a singular value decomposition. +% +% Returns +% ------- +% R : :class:`numeric` +% remainder matrix, depends on selected algorithm. +% +% Q : :class:`numeric` +% orthonormal basis matrix. % TODO have a look at https://github.com/iwoodsawyer/factor diff --git a/src/utility/linalg/tensorprod.m b/src/utility/linalg/tensorprod.m index 679f77d..16f0c3b 100644 --- a/src/utility/linalg/tensorprod.m +++ b/src/utility/linalg/tensorprod.m @@ -1,18 +1,25 @@ function C = tensorprod(A, B, dimA, dimB, cA, cB, options) -% tensorprod - Tensor products between two tensors. -% C = tensorprod(A, B, dimA, dimB) -% returns the tensor product of tensors A and B. The arguments dimA and dimB are -% vectors that specify which dimensions to contract in A and B. The size of the -% output tensor is the size of the uncontracted dimensions of A followed by the size -% of the uncontracted dimensions of B. +% Tensor products between two tensors. % -% C = tensorprod(A, B) -% returns the outer product of tensors A and B. This is equivalent to the previous -% syntax with dimA = dimB = []. +% Usage +% ----- +% :code:`C = tensorprod(A, B, dimA, dimB)` +% +% returns the tensor product of tensors :code:`A` and :code:`B`. The arguments :code:`dimA` +% and :code:`dimB` are vectors that specify which dimensions to contract in :code:`A` and +% :code:`B`. The size of the +% output tensor is the size of the uncontracted dimensions of :code:`A` followed by the size +% of the uncontracted dimensions of :code:`B`. % -% C = tensorprod(_, NumDimensionsA=ndimsA) -% optionally specifies the number of dimensions in tensor A in addition to combat the -% removal of trailing singleton dimensions. +% :code:`C = tensorprod(A, B)` +% +% returns the outer product of tensors :code:`A` and :code:`B`. This is equivalent to the +% previous syntax with :code:`dimA = dimB = []`. +% +% :code:`C = tensorprod(_, NumDimensionsA=ndimsA)` +% +% optionally specifies the number of dimensions in tensor :code:`A` in addition to combat the +% removal of trailing singleton dimensions. arguments A @@ -24,12 +31,12 @@ options.NumDimensionsA = ndims(A) end -persistent version -if isempty(version), version = ~isMATLABReleaseOlderThan("R2022a", "release", 1); end -if version - C = builtin('tensorprod', dimA, dimB, 'NumDimensionsA', options.NumDimensionsA); - return -end +% persistent version +% if isempty(version), version = ~isMATLABReleaseOlderThan("R2022a", "release", 1); end +% if version +% C = builtin('tensorprod', dimA, dimB, 'NumDimensionsA', options.NumDimensionsA); +% return +% end szA = size(A, 1:options.NumDimensionsA); diff --git a/src/utility/linalg/tensortrace.m b/src/utility/linalg/tensortrace.m index 35a1f77..5fe2727 100644 --- a/src/utility/linalg/tensortrace.m +++ b/src/utility/linalg/tensortrace.m @@ -1,28 +1,30 @@ -function C = tensortrace(A, ia) -% tensortrace - Compute the (partial) trace of a tensor. -% [C, ic] = tensortrace(A, ia) -% traces over the indices that appear twice in ia. +function C = tensortrace(A, i1, i2) +% Compute the (partial) trace of a tensor. % -% [C, ic] = tensortrace(A, ia, ic) -% optionally specifies the output indices' order. - -arguments - A - ia -end +% Usage +% ----- +% +% :code:`[C, ic] = tensortrace(A, i1)` +% +% traces over the indices that appear twice in :code:`i1`. +% +% :code:`[C, ic] = tensortrace(A, i1, i2)` +% +% optionally specifies the output indices' order. -[dimA1, dimA2] = traceinds(ia); +if isempty(i1) && isempty(i2), C = A; return; end +assert(length(i1) == length(i2), 'invalid indices'); -szA1 = size(A, dimA1); -szA2 = size(A, dimA2); +szA1 = size(A, i1); +szA2 = size(A, i2); assert(all(szA1 == szA2)); E = reshape(eye(prod(szA1)), [szA1 szA2]); indsA = -(1:ndims(A)); -indsA(dimA1) = 1:length(dimA1); -indsA(dimA2) = (1:length(dimA2)) + length(dimA1); -C = contract(A, indsA, E, [1:length(dimA1) (1:length(dimA2)) + length(dimA1)]); +indsA(i1) = 1:length(i1); +indsA(i2) = (1:length(i2)) + length(i1); +C = contract(A, indsA, E, [1:length(i1) (1:length(i2)) + length(i1)]); end diff --git a/src/utility/mat2jl.m b/src/utility/mat2jl.m new file mode 100644 index 0000000..dd30f7e --- /dev/null +++ b/src/utility/mat2jl.m @@ -0,0 +1,26 @@ +function obj = mat2jl(obj) +% Recursively convert an object into something that can be read by Julia (GlueKit) + +if isobject(obj) && ~strcmp(class(obj), "string") + name = class(obj); + orig = warning('off', 'MATLAB:structOnObject'); + obj = arrayfun(@struct, obj); + warning(orig); + for i = 1:numel(obj) + obj(i).classname = name; + end +end + +if isstruct(obj) + fns = fieldnames(obj); + for i = 1:numel(obj) + for fn = fns' + obj(i).(fn{1}) = mat2jl(obj(i).(fn{1})); + end + end +elseif iscell(obj) + obj = cellfun(@mat2jl, obj, 'UniformOutput', false); +end + +end + diff --git a/src/utility/memsize.m b/src/utility/memsize.m index ebdc278..ebe1e73 100644 --- a/src/utility/memsize.m +++ b/src/utility/memsize.m @@ -1,4 +1,9 @@ function [bytes, unit] = memsize(in, unit) +% Check memory size of object in prefered unit ('GB', 'MB', 'KB' or 'B'). +% +% Usage +% ----- +% :code:`[bytes, unit] = memsize(in, unit)` if isa(in, 'containers.Map') warning('off', 'MATLAB:structOnObject'); @@ -13,7 +18,7 @@ props = properties(in); bytes = 0; for ii = 1:length(props) - bytes = bytes + memsize(in.(thisprop), 'B'); + bytes = bytes + memsize({in.(props{ii})}, 'B'); end end diff --git a/src/utility/netcon.m b/src/utility/netcon.m new file mode 100644 index 0000000..0c127d4 --- /dev/null +++ b/src/utility/netcon.m @@ -0,0 +1,1324 @@ +function [sequence, cost] = netcon(legLinks,verbosity,costType,muCap,allowOPs,legCosts) +% Finds most efficient way to contract a tensor network +% v2.00 by Robert N. C. Pfeifer 2014 +% Contact: rpfeifer.public@gmail.com, robert.pfeifer@mq.edu.au +% +% Usage +% ----- +% :code:`[sequence, cost] = netcon(legLinks, verbosity, costType, muCap, allowOPs, legCosts)` +% +% Arguments +% --------- +% legLinks +% abelling of tensor indices. +% +% Optional Arguments +% ------------------ +% verbosity +% 0: Quiet. 1: State final result. 2: State intermediate and final results. 3: Also display object sizes during pairwise contractions. (2) +% costType +% 1: Absolute value. 2: Multiple and power of chi. (2) +% muCap +% Initially restrict search to sequences having a cost of muCap (if costType==1) or +% O(X^muCap) (if costType==2). This value will increment automatically if required, and it +% is recommended that it be left at the default value of 1. (1) +% allowOPs +% Allow contraction sequences including outer products: 0/false: No. 1/true: Yes. (true) +% legCosts +% For costType==1: nx2 table. A row reading [a b] assigns a dimension of b to index a. +% Default: 2 for all legs. For costType==2: nx3 table. A row reading [a b c] assigns a +% dimension of bX^c to index a, for unspecified X. Default: 1X^1 for all legs. + +arguments + legLinks + verbosity = 2 % 0: No displayed output. 1: Display final result. 2: Display progress reports. + costType = 2 % 1: Absolute value. 2: Multiple and power of chi. + muCap = 1 + allowOPs = 1 + legCosts = [] +end + +% Benchmarking examples +% --------------------- +% 3:1 1D MERA: +% tic;netcon({[-1 1 2 3],[2 4 5 6],[1 5 7 -3],[3 8 4 9],[6 9 7 10],[-2 8 11 12],[10 11 12 -4]},0,2,1,1);toc +% All in MATLAB, no OPs: 0.041s +% All in MATLAB, with OPs: 0.054s +% Using C++, no OPs: 0.0019s +% Using C++, with OPs: 0.0019s +% +% 9:1 2D MERA: +% tic;netcon({[1 4 5 6 7 8],[2 9 10 11 12 13],[3 14 15 16 17 18],[-6 9 23 24 25 26],[-5 5 19 20 21 22],[8 14 27 28 29 30],[12 15 31 32 33 34],[22 25 28 31 35 36 37 38],[-4 20 23 35 39 40 41 42],[42 36 37 38 43 44 45 46],[41 24 44 26 47 48],[19 40 21 43 49 50],[27 45 29 30 51 52],[46 32 33 34 53 54],[-2 -3 39 49 47 55],[4 50 6 7 51 56],[48 10 11 53 13 57],[52 54 16 17 18 58],[55 56 57 58 -1 1 2 3]},0,2,1,1);toc +% All in MATLAB, no OPs: 5.4s +% All in MATLAB, with OPs: 6.1s +% Using C++, no OPs: 0.066s +% Using C++, with OPs: 0.069s +% +% 4:1 2D MERA: +% tic;netcon({[64 72 73 19 22],[65 74 75 23 76],[66 77 20 79 28],[67 21 24 29 34],[68 25 78 35 80],[69 81 30 83 84],[70 31 36 85 86],[71 37 82 87 88],[-5 19 20 21 1 2 4 5],[22 23 24 25 3 26 6 27],[28 29 30 31 7 8 32 33],[34 35 36 37 9 38 39 40],[1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18],[10 11 13 14 41 42 43 44],[12 26 15 27 47 48 49 50],[16 17 32 33 53 54 55 56],[18 38 39 40 60 61 62 63],[-2 -3 -4 41 89],[72 73 42 47 90],[74 75 48 76 45],[77 43 79 53 46],[44 49 54 60 51],[50 78 61 80 52],[81 55 83 84 57],[56 62 85 86 58],[63 82 87 88 59],[89 90 45 46 51 52 57 58 59 -1 64 65 66 67 68 69 70 71]},0,2,1,1);toc +% All in MATLAB, no OPs: 2486s (41.4m) +% All in MATLAB, with OPs: 3919.6s (65.3m) +% Using C++, no OPs: 36.12s +% Using C++, with OPs: 36.49s + + +% Changes in v2.00: +% ----------------- +% Changed algorithm to breadth-first search instead of dynamic programming. +% Made handling of subnetworks iterative, not recursive. +% Made displayed output more user-friendly for networks composed of disjoint subnetworks. +% Added warning, not error, for tensors or subnetworks of trivial dimension (i.e. just a number). +% Added more vigorous exclusion of unnecessary outer products. + +% Changes in v1.01: +% ----------------- +% Improved structure of code. +% Fixed bug returning wrong cost when trivial indices present and non-consecutive numbering of positive indices. +% Removed keepTriv option. +% Fixed omission of trailing zeros in sequence when network is disjoint and all contractions are traces. +% Corrected helptext. + + +% Check input data (and strip trivial indices from legLinks) +% ================ +if ~isempty(legCosts) + [trivialindices legCosts legLinks] = checkInputData(legLinks,verbosity,costType,muCap,allowOPs,legCosts); +else + [trivialindices legCosts legLinks] = checkInputData(legLinks,verbosity,costType,muCap,allowOPs); +end + +% Divide network into disjoint subnets +% ==================================== +subnetlist = zeros(1,numel(legLinks)); +subnetcounter = 1; +while any(subnetlist==0) + flags = findSubnetIncluding(legLinks,find(subnetlist==0,1,'first')); + subnetlist(flags) = subnetcounter; + subnetcounter = subnetcounter + 1; +end +subnetcounter = subnetcounter-1; + +% Evaluate contraction sequences for disjoint subnets +% =================================================== +sequence = cell(1,subnetcounter); +cost = cell(1,subnetcounter); +freelegs = cell(1,subnetcounter); +donetrace = false(1,subnetcounter); +for a=1:subnetcounter + if verbosity > 0 + disp(' '); + if subnetcounter > 1 + disp(['Subnet ' num2str(a) ':']); + end + if verbosity > 1 + disp(['Tensors: ' unpaddednum2str(find(subnetlist==a))]); + end + end + [sequence{a} cost{a} freelegs{a} donetrace(a)] = netcon_getsubnetcost(legLinks(subnetlist==a),verbosity,costType,muCap,legCosts,allowOPs); +end +donetrace = any(donetrace); + +% Perform outer products of subnets, add costs, and merge sequences +% ================================================================= +[sequence cost] = performOPs(sequence,cost,freelegs,legCosts,costType,verbosity); +sequence = [sequence trivialindices]; % Append tracing over trivial indices on final object + +% Display result +% ============== +if verbosity>0 + if ~isempty(trivialindices) && verbosity > 1 + disp(' '); + disp(['Restore summed trivial indices: ' unpaddednum2str(trivialindices)]); + end + disp(' '); + if verbosity > 1 && subnetcounter > 1 + disp('Entire network:'); + end + netcon_displayresult(sequence,cost,donetrace,costType); +end +end + +function [sequence cost] = performOPs(sequence,cost,freelegs,legCosts,costType,verbosity) +% Renumber freelegs by position and trim index label list off legCosts +for a=1:numel(freelegs) + for b=1:numel(freelegs{a}) + freelegs{a}(b) = find(legCosts(:,1)==freelegs{a}(b)); + end +end +legCosts = legCosts(:,2:end); +% Perform outer products +if costType==2 + tensormulsize = ones(size(freelegs)); + tensorchisize = zeros(size(freelegs)); + for a=1:numel(freelegs) + for b=1:numel(freelegs{a}) + tensormulsize(a) = tensormulsize(a)*legCosts(freelegs{a}(b),1); + tensorchisize(a) = tensorchisize(a)+legCosts(freelegs{a}(b),2); + end + end + if any(tensormulsize==1 & tensorchisize==0) && numel(freelegs)>1 + if sum(tensormulsize==1 & tensorchisize==0)==1 + if verbosity > 0 + disp(' '); + end + warning('netcon:trivialsubnet',['Subnet ' unpaddednum2str(find(tensormulsize==1 & tensorchisize==0)) ' reduces to a single number. Since this subnet is not the entire network, contraction sequence may not be optimal.']); + else + if verbosity > 0 + disp(' '); + end + warning('netcon:trivialsubnet',['More than one subnet reduces to a single number: Contraction sequence may not be optimal. List of subnets reducing to a single number: ' unpaddednum2str(find(tensormulsize==1 & tensorchisize==0))]); + end + end + while numel(tensorchisize)>1 + % Sort tensors + ptr = 1; + while (ptrtensorchisize(ptr+1) || (tensorchisize(ptr)==tensorchisize(ptr+1) && tensormulsize(ptr)>tensormulsize(ptr+1)) + t = tensorchisize(ptr+1); tensorchisize(ptr+1) = tensorchisize(ptr); tensorchisize(ptr) = t; + t = tensormulsize(ptr+1); tensormulsize(ptr+1) = tensormulsize(ptr); tensormulsize(ptr) = t; + t = sequence{ptr}; sequence{ptr} = sequence{ptr+1}; sequence{ptr+1} = t; + t = cost{ptr}; cost{ptr} = cost{ptr+1}; cost{ptr+1} = t; + ptr = ptr - 1; + if ptr==0 + ptr = 2; + end + else + ptr = ptr + 1; + end + end + % Combine smallest two tensors + outerproductmul = tensormulsize(1) * tensormulsize(2); + outerproductpower = tensorchisize(1) + tensorchisize(2); + if numel(cost{1})1 + % Sort tensors + tensorsize = sort(tensorsize,'ascend'); + % Combine smallest two tensors + outerproduct = tensorsize(1) * tensorsize(2); + cost{1} = cost{1} + outerproduct; + tensorsize = [outerproduct tensorsize(3:end)]; + cost{1} = cost{1} + cost{2}; + sequence{1} = [sequence{1} sequence{2}]; + cost = cost([1 3:end]); + sequence = sequence([1 3:end]); + end +end +sequence = sequence{1}; +sequence = [sequence zeros(1,numel(freelegs)-1)]; % Add outer products between disjoint subnets to the contraction sequence +cost = cost{1}; +end + +function [sequence cost negindices donetrace] = netcon_getsubnetcost(legLinks,verbosity,costType,muCap,legCosts,allowOPs) +% Performs any traces on single tensors +% Checks if network is trivial +% If the network is not trivial, invokes netcon_nondisj to evaluate cost for the network + +% Get index lists for this subnet and trim legCosts +% ================================================= +[posindices negindices] = getIndexLists(cell2mat(legLinks)); + +% Any contraction to do? +% ====================== +if numel(legLinks)<2 + % List of tensors has only one entry + % ================================== + if ~isempty(posindices) + % One tensor, with traces - do them, then go to "Finished contracting" + if costType==2 + cost = []; + else + cost = 0; + end + sequence = posindices; + donetrace = true; + if verbosity > 0 + disp('Tracing costs only'); + end + % Go to "Finished contracting" + else + % Nothing to do + sequence = []; + if costType==2 + cost = []; + else + cost = 0; + end + donetrace = false; + if verbosity > 0 + disp('Network is trivial'); + end + % One tensor, no traces - go to "Finished contracting" + end +else + % >1 tensor, tracing and/or contraction to do + % =========================================== + % Make positive indices consecutive + % Make negative indices consecutive following on from positive indices + % Truncate cost table and remove indexing column + % (Note original labelling for legs may be recovered from posindices and negindices) + [legLinks legCosts] = reprocessLegLinks(legLinks,posindices,negindices,legCosts); + + % Determine any traces (legs with both ends on a single tensor) and eliminate + % =========================================================================== + [tracedindices donetrace legLinks legCosts linkposindices] = eliminateTraces(legLinks,posindices,legCosts); + + if isempty(linkposindices) + % No positive indices after tracing + sequence = posindices(tracedindices); + if costType==2 + cost = []; + else + cost = 0; + end + else + % Find best sequence + % ================== + % Invoke sequence finder for traceless non-disjoint network + for a=1:numel(legLinks) + legLinks{a} = int32(abs(legLinks{a})); + end + legCosts = double(legCosts); + verbosity = double(verbosity); + costType = double(costType); + muCap = double(muCap); + allowOPs = double(allowOPs); + posindices = double(posindices); + tracedindices = int32(tracedindices); + [sequence cost] = netcon_nondisj(legLinks,legCosts,verbosity,costType,muCap,allowOPs,posindices,tracedindices); +% try +% [sequence cost] = netcon_nondisj_cpp(legLinks,legCosts,verbosity,costType,muCap,allowOPs,posindices,tracedindices); +% catch ME +% if isequal(ME.identifier,'MATLAB:UndefinedFunction') +% [sequence cost] = netcon_nondisj(legLinks,legCosts,verbosity,costType,muCap,allowOPs,posindices,tracedindices); +% else +% rethrow(ME); +% end +% end + % Reinsert indices traced at the beginning of the contraction process + sequence = [tracedindices sequence]; + % Translate sequence back into user's original leg labels + sequence(sequence>0) = posindices(sequence(sequence>0)); + end +end +end + +function [tracedindices donetrace legLinks legCosts posindices] = eliminateTraces(legLinks,posindices,legCosts) +tracedindices = []; +donetrace = false; +% For each tensor: +for a=1:numel(legLinks) + % - Find all tracing legs on that tensor, note the indices, and delete them. + % - Tracing can be performed over all traced indices on a tensor simultaneously by combining them - no need to sort + b = 1; + while b + legLinks{a}(legLinks{a}==legLinks{a}(b)) = []; + else + b = b + 1; + end + end +end +% Then: +if ~isempty(tracedindices) + donetrace = true; + % - Delete removed rows from leg cost list + legCosts(tracedindices,:) = []; + % - Renumber all positive and negative legs consecutively again + renumindices = 1:numel(posindices); + renumindices(tracedindices) = []; + posindices(tracedindices) = []; + for a=1:numel(legLinks) + for b=1:numel(legLinks{a}) + if legLinks{a}(b)>0 + legLinks{a}(b) = find(renumindices==legLinks{a}(b)); + else + legLinks{a}(b) = legLinks{a}(b) + numel(tracedindices); + end + end + end +end +end + +function [legLinks legCosts] = reprocessLegLinks(legLinks,posindices,negindices,legCosts) +% Renumber all positive and negative indices consecutively +% ======================================================== +for a=1:numel(legLinks) + for b=find(legLinks{a}>0) + legLinks{a}(b) = find(posindices==legLinks{a}(b)); + end + for b=find(legLinks{a}<0) + legLinks{a}(b) = -find(negindices==legLinks{a}(b))-numel(posindices); + end +end + +% Assemble truncated cost table +% ============================= +for a=size(legCosts,1):-1:1 + if ~any([posindices negindices]==legCosts(a,1)) + legCosts(a,:) = []; + end +end +legCosts = legCosts(:,2:end); +end + +function cost = addCosts(cost1,cost2,costType) +if costType==1 + cost = cost1 + cost2; +else + cost = zeros(1,max([numel(cost1) numel(cost2)])); + cost(1:numel(cost1)) = reshape(cost1,1,numel(cost1)); + cost(1:numel(cost2)) = cost(1:numel(cost2)) + reshape(cost2,1,numel(cost2)); +end +end + +function flags = findSubnetIncluding(legLinks,pos) +new = true; +flags = zeros(1,numel(legLinks)); % 0: May not be in subnet +flags(pos) = 1; % 1: Follow connections off this tensor +while new + new = false; + fromlist = find(flags==1); + flags(flags==1) = 2; % 2: Have followed all connections from this tensor + for checkfrom = fromlist + for checkto = find(flags==0) + for x = 1:numel(legLinks{checkfrom}) + if any(legLinks{checkto}==legLinks{checkfrom}(x)) + flags(checkto) = true; + new = true; + end + end + end + end +end +flags = flags~=0; +end + +function [trivialindices legCosts legLinks] = checkInputData(legLinks,verbosity,costType,muCap,allowOPs,legCosts) +% Check data sizes +if ~iscell(legLinks) + error('legLinks must be a cell array') +end +if numel(legLinks)==0 + error('legLinks may not be empty') +end +if ~isnumeric(verbosity) || ~any([0 1 2 3]==verbosity) + error('verbosity must be 0, 1, 2 or 3') +end +if ~isnumeric(muCap) || numel(muCap)~=1 || ~isreal(muCap) || muCap<=0 + error('muCap must be a real positive number'); +end +if ~isnumeric(costType) || ~any([1 2]==costType) + error('costType must be either 1 or 2') +end +if (~isnumeric(allowOPs) && ~islogical(allowOPs)) || ~any([0 1]==allowOPs) + error('allowOPs must be either 0 or 1'); +end +for a=1:numel(legLinks) + if ~isnumeric(legLinks{a}) + error('Entries in legLinks must be numeric arrays') + end +end +if size(legLinks,1)~=1 || size(legLinks,2)~=numel(legLinks) + error('Array of index connections (legLinks) has incorrect dimension - should be 1xn') +end +for a=1:numel(legLinks) + if size(legLinks{a},1)~=1 || size(legLinks{a},2)~=numel(legLinks{a}) + error(['legLinks entry ' num2str(a) ' has wrong dimension - should be 1xn']); + end + if isempty(legLinks{a}) + error(['Empty list of indices on tensor ' num2str(a)]) + end +end +allindices = cell2mat(legLinks); +if any(allindices==0) + error('Zero entry in index list') +elseif any(imag(allindices)~=0) + error('Complex entry in index list') +elseif any(int32(allindices)~=allindices) + error('Non-integer entry in legLinks'); +end +posindices = sort(allindices(allindices>0),'ascend'); +negindices = sort(allindices(allindices<0),'descend'); +if ~isempty(posindices) + % Test all positive indices occur exactly twice + if mod(numel(posindices),2)~=0 + maxposindex = posindices(end); + posindices = posindices(1:end-1); + end + flags = (posindices(1:2:numel(posindices))-posindices(2:2:numel(posindices)))~=0; + if any(flags) + errorpos = 2*find(flags~=0,1,'first')-1; + if errorpos>1 && posindices(errorpos-1)==posindices(errorpos) + error(['Error in index list: Index ' num2str(posindices(errorpos)) ' appears more than twice']); + else + error(['Error in index list: Index ' num2str(posindices(errorpos)) ' only appears once']); + end + end + if exist('maxposindex','var') + if posindices(end)==maxposindex + error(['Error in index list: Index ' num2str(maxposindex) ' appears more than twice']); + else + error(['Error in index list: Index ' num2str(maxposindex) ' only appears once']); + end + end + posindices = posindices(1:2:numel(posindices)); % List of all positive indices, sorted ascending, each appearing once. + flags = posindices(1:end-1)==posindices(2:end); + if any(flags) + errorpos = find(flags,1,'first'); + error(['Error in index list: Index ' num2str(posindices(errorpos)) ' appears more than twice']); + end +else + posindices = []; +end +% Test all negative indices occur exactly once +flags = negindices(1:end-1)==negindices(2:end); +if any(flags) + errorpos = find(flags,1,'first'); + error(['Error in index list: Index ' num2str(negindices(errorpos)) ' appears more than once']); +end + +% Check leg size data +trivialindices = []; +if ~exist('legCosts','var') + if costType==1 + % Create basic leg costs list (all 2) + legCosts = [[posindices.';negindices.'] 2*ones(numel(posindices)+numel(negindices),1)]; + else + % Create basic leg costs list (all Chi^1) + legCosts = [[posindices.';negindices.'] ones(numel(posindices)+numel(negindices),2)]; + end +else + if ~isnumeric(legCosts) + error('legCosts must be numeric') + end + % Check valid leg costs list, & process + if ndims(legCosts)>2 || any(legCosts(:,1)==0) || size(legCosts,2)~=2+(costType==2) || any(legCosts(:,2)<=0) + error('Bad index dimensions list (error in legCosts: type ''help netcon'' for specifications)') + end + if costType==2 + if any(legCosts(:,3)<0) + error('For index dimensions of the form aX^b, values of b less than 0 are not supported') + end + if any(legCosts(:,2)<=0) + error('For index dimensions of the form aX^b, values of a less than or equal to 0 are not supported') + end + else + if any(legCosts(:,2)<=0) + error('Index dimensions less than or equal to 0 are not supported') + end + end + [t1 ix1] = sort(legCosts(legCosts(:,1)>0,1)); + [t2 ix2] = sort(legCosts(legCosts(:,1)<0,1),'descend'); + t1 = reshape(t1,1,[]); + t2 = reshape(t2,1,[]); + flag = t1(1:end-1)==t1(2:end); + if any(flag) + error(['Dimension of index ' num2str(t1(find(flag,1))) ' specified twice']) + end + if ~(isempty(t1) && isempty(posindices)) % Necessary as a 1x0 matrix is not the same as a 0x0 matrix + if ~isequal(t1,posindices) + error(['Dimension not specified for all positive indices. Supplied: ' num2str(t1) ' Required: ' num2str(posindices)]) + end + end + flag = t2(1:end-1)==t2(2:end); + if any(flag) + error(['Dimension of index ' num2str(t2(find(flag,1))) ' specified twice']) + end + if ~(isempty(t2) && isempty(negindices)) + if ~isequal(t2,negindices) + error(['Dimension not specified for all negative indices. Supplied: ' num2str(t2) ' Required: ' num2str(negindices)]) + end + end + + % Store list of any summed trivial indices to be stripped + if costType==1 + trivialindices = legCosts(legCosts(:,2)==1,1); + else + trivialindices = legCosts(legCosts(:,2)==1 & legCosts(:,3)==0,1); + end + trivialindices = reshape(sort(trivialindices(trivialindices>0),'descend'),1,[]); + + % Strip trivial indices + for a=numel(trivialindices):-1:1 + for b=1:numel(legLinks) + if sum(legLinks{b}==trivialindices(a))==2 + trivialindices(a) = []; % Do not strip trivial single-tensor traces (not that this actually matters either way) + else + legLinks{b}(legLinks{b}==trivialindices(a)) = []; + end + end + end + if ~isempty(trivialindices) && verbosity > 1 + disp(' '); + disp(['Ignore summed trivial indices: ' unpaddednum2str(trivialindices)]); + end + + % Order leg costs list + t1 = legCosts(legCosts(:,1)>0,2:end); + t1 = t1(ix1,:); + t2 = legCosts(legCosts(:,1)<0,2:end); + t2 = t2(ix2,:); + legCosts = [[posindices.';negindices.'] [t1;t2]]; +end +end + +function [posindices negindices] = getIndexLists(allindices) +posindices = sort(allindices(allindices>0),'ascend'); +negindices = sort(allindices(allindices<0),'descend'); +posindices = posindices(1:2:end); +end + +function netcon_displayresult(sequence,cost,donetrace,costType) +% Displays nicely-formatted final output +t = ['Best sequence: ' unpaddednum2str(sequence)]; +if isempty(sequence) + t = [t '']; +end +disp(t); +if isempty(cost) || isequal(cost,0) + if donetrace + disp('Cost: Tracing costs only'); + else + disp('Cost: 0'); + end +else + t = 'Cost: '; + if costType==2 + for a=numel(cost):-1:2 + t = [t num2str(cost(a)) 'X^' num2str(a-1) ' + ']; %#ok + end + t = [t num2str(cost(1)) 'X^0']; + else + t = [t num2str(cost)]; + end + if donetrace + t = [t ' + tracing costs']; + end + disp(t); +end +end + +function str = unpaddednum2str(row) +str = []; +for a=row(1:end-1) + str = [str num2str(a) ' ']; %#ok +end +if ~isempty(row) + str = [str num2str(row(end))]; +end +end + +% Functions used in the pure-MATLAB version only: + +function [sequence cost] = netcon_nondisj(legLinks,legCosts,verbosity,costType,muCap,allowOPs,posindices,tracedindices) +% Set up initial data +% =================== +numtensors = numel(legLinks); +if costType==1 + zerocost = 0; +else + zerocost = []; +end +allowOPs = (allowOPs==1); + +% This code uses the nomenclature of Appendix G, in which a tensor which may be contracted with the result of an outer product [e.g. C in Fig.4(a)] is denoted X. + +% Structure of "objects": objects{numElements}{positionInList}{legFlags,tensorFlags,sequenceToBuild,costToBuild,isOP,OPmaxdim,allIn} +% legFlags: Legs present on object +% tensorFlags: Tensor constituents of object +% sequenceToBuild: Cheapest identified contraction sequence for constructing this object +% costToBuild: Minimum identified cost of constructing this object +% isOP: Indicates whether the contraction which yielded this object was an outer product +% OPmaxdim: If isOP==true, then OPmaxdim gives the dimension of the largest constituent tensor +% allIn: If this tensor is an object X which may be contracted with an outer product, allIn is the dimension of the tensor which contributed no external legs to X, i.e. xi_C in Fig.5(c). + +objects = cell(1,numtensors); +objects{1} = cell(1,numtensors); +tensorflags = false(1,numtensors); +numleglabels = size(legCosts,1); +legflags = false(1,numleglabels); + +% ### Create lists used in enforcing Sec. II.B.2.c (structure of tensor X contractable with an outer product) +tensorXlegs = {}; +tensorXflags = []; +tensorXdims = {}; +% ### End of creating lists + +for a=1:numtensors + tensorflags(a) = true; + legflags(abs(legLinks{a})) = true; + objects{1}{a} = {legflags,tensorflags,[],zerocost,false,[],zeros(1,costType)}; + + % ### Set up initial list data used in enforcing Sec. II.B.2.c + if allowOPs + [tensorXlegs tensorXdims tensorXflags] = addToTensorXlist(tensorXlegs,tensorXdims,tensorXflags,legflags,Inf*ones(1,costType),costType,true); + end + % ### End of setting up linked list data + + tensorflags(a) = false; + legflags(abs(legLinks{a})) = false; +end + +% ### Set up initial list data used in enforcing Sec. II.B.2.c (continued) +tensorXflags = zeros(1,numel(tensorXflags)); +% ### End of setting up initial linked list data (continued) + +for a=2:numtensors + objects{a} = {}; +end + +newobjectflags = cell(1,numtensors); +newobjectflags{1} = true(1,numtensors); + +oldmuCap = 0; +newmuCap = Inf; + +done = false; +while ~done + if verbosity>0 + if oldmuCap~=muCap + if costType==1 + disp(['Looking for solutions with maximum cost of ' num2str(muCap)]); + else + disp(['Looking for solutions of cost O(X^' num2str(muCap) ')']); + end + end + end + for numInObjects = 2:numtensors + if verbosity > 2 + disp(['Pairwise contractions (AB) involving ' num2str(numInObjects) ' fundamental tensors:']); + end + for numInPieceOne = 1:floor(numInObjects/2) + numInPieceTwo = numInObjects - numInPieceOne; + if verbosity > 2 + disp(['A contains ' num2str(numInPieceOne) ', B contains ' num2str(numInPieceTwo)'.']); + pause(0.01); + end + if numel(objects{numInPieceOne})>0 && numel(objects{numInPieceTwo})>0 + + % Iterate over pairings: Iterate over object 1 + for a = 1:numel(objects{numInPieceOne}) + + % Get data for object 1 + obj1data = objects{numInPieceOne}{a}; + % obj1data: legs, tensors, seqToBuild, costToBuild, isOP, maxdimOP, allIn + legs1 = obj1data{1}; + tensorsIn1 = obj1data{2}; + seq1 = obj1data{3}; + costToBuild1 = obj1data{4}; + isOP1 = obj1data{5}; + OPmaxdim1 = obj1data{6}; + allIn1 = obj1data{7}; + isnew1 = newobjectflags{numInPieceOne}(a); + + % Iterate over pairings: Iterate over object 2 + if numInPieceOne == numInPieceTwo + obj2list = a+1:numel(objects{numInPieceTwo}); + else + obj2list = 1:numel(objects{numInPieceTwo}); + end + for b = obj2list + + % Check object 1 and object 2 don't share any common tensors (which would then appear twice in the resulting network) + if ~any(objects{numInPieceOne}{a}{2} & objects{numInPieceTwo}{b}{2}) + + % Get data for object 2 + obj2data = objects{numInPieceTwo}{b}; + % obj2data: legs, tensors, seqToBuild, costToBuild, isOP, maxdimOP, allIn + legs2 = obj2data{1}; + tensorsIn2 = obj2data{2}; + seq2 = obj2data{3}; + costToBuild2 = obj2data{4}; + isOP2 = obj2data{5}; + OPmaxdim2 = obj2data{6}; + allIn2 = obj2data{7}; + isnew2 = newobjectflags{numInPieceTwo}(b); + + commonlegs = legs1 & legs2; % Common legs + freelegs = xor(legs1,legs2); + freelegs1 = legs1 & freelegs; + freelegs2 = legs2 & freelegs; + commonlegsflag = any(commonlegs); + + isOK = allowOPs || commonlegsflag; % Exclude outer products if allowOPs is not set + + thisTensorXflag = -1; + % ### Enforce Sec. II.B.2.b,c,d (only perform outer product if there is a known tensor X with appropriate structure; only contract resulting object in another outer product or with an appropriate tensor X; enforce index dimension constraints) + if isOK && ~commonlegsflag + % It's an outer product. Check if a suitable tensor X exists yet to contract with this outer product [Fig.5(c) & Eq.(25)]. + if oldmuCap == muCap + % Pass for new X's + if isnew1 || isnew2 + % Using a new object - allowed to contract with old and new X's + % Note - the while/end construction is faster than using "find" + Xstart = 1; + Xend = 1; + while Xend<=numel(tensorXflags) && tensorXflags(Xend)~=2; + Xend = Xend + 1; + end + else + % Made from old objects - only allowed to contract with new X's. Already had a chance to contract with old X's + % on the previous pass so repeating these is unnecessary. + Xstart = 1; + while Xstart<=numel(tensorXflags) && tensorXflags(Xstart)~=1; + Xstart = Xstart + 1; + end + Xend = Xstart; + while Xend<=numel(tensorXflags) && tensorXflags(Xend)==1; + Xend = Xend + 1; + end + end + else + % Old X's only on this pass + Xstart = 1; + Xend = 1; + while Xend<=numel(tensorXflags) && tensorXflags(Xend)==0; + Xend = Xend + 1; + end + end + for x = Xstart:Xend-1 + if all(tensorXlegs{x}(freelegs)) + % IIB2c: xi_C > xi_A (25) + if isGreaterThan_sd(tensorXdims{a},getProdLegDims(freelegs,legCosts,costType),costType) + % IIB2b: xi_C > xi_D && xi_C > xi_E: (16) + if isGreaterThan_sd(getProdLegDims(tensorXlegs{x}&~freelegs,legCosts,costType),getProdLegDims(freelegs1,legCosts,costType),costType) + if isGreaterThan_sd(getProdLegDims(tensorXlegs{x}&~freelegs,legCosts,costType),getProdLegDims(freelegs2,legCosts,costType),costType) + thisTensorXflag = tensorXflags(x); + break; + end + end + end + end + end + isOK = thisTensorXflag~=-1; + end + + % If either constituent is the result of an outer product, check that it is being contracted with an appropriate tensor + % [either this is a contraction over all indices, or this is an outer product with a tensor of larger total dimension than + % either constituent of the previous outer product, and Eqs. (16), (25), and (26) are satisfied]. + if isOK && (isOP1 || isOP2) + % Post-OP. This contraction only allowed if it is also an outer product, or if only one object is an outer product, it involves all indices on that tensor, and the other object satisfies the relevant conditions. + isOK = xor(isOP1,isOP2) || ~commonlegsflag; % If contracting over common indices, only one object may be an outer product + if isOK + if commonlegsflag + % This contraction is not itself an outer product + % Conditions on outer product object: + if isOP1 + % Check all-indices condition: + if isequal(commonlegs,legs1) + % Check free legs on contracting tensor are larger than summing legs going to each component of outer product [Eq. (16)] + isOK = isGreaterThan_sd(getProdLegDims(freelegs,legCosts,costType),OPmaxdim1,costType); % IIB2b: xi_C > xi_D, xi_C > xi_E (16) + else + isOK = false; + end + else + % Check all-indices condition: + if isequal(commonlegs,legs2) + % Check free legs on contracting tensor are larger than summing legs going to each component of outer + % product [Eq. (16)] + isOK = isGreaterThan_sd(getProdLegDims(freelegs,legCosts,costType),OPmaxdim2,costType); % IIB2b: xi_C > xi_D, xi_C > xi_E (16) + else + isOK = false; + end + end + % Conditions on X: Ensure X is fundamental or acceptably-constructed (note: structure is checked by requiring + % non-zero value of allIn) + if isOK + if isOP1 + % Tensor 2 is X + if numInPieceTwo > 1 + % Tensor 2 is not fundamental + % Check tensor 2 is constructed in an acceptable fashion [Fig. 5(c) and Eqs. (25) and (26)] + isOK = isGreaterThan_sd(allIn2,getProdLegDims(freelegs,legCosts,costType),costType); % IIB2c: xi_C > xi_D (26) + if isOK + isOK = isGreaterThan_sd(allIn2,getProdLegDims(freelegs1,legCosts,costType),costType); % IIB2c: xi_C > xi_A (25) + end + end + else + % Tensor 1 is X + if numInPieceOne > 1 + % Tensor 1 is not fundamental + % Check tensor 1 is constructed in an acceptable fashion [Fig. 5(c) and Eqs. (25) and (26)] + isOK = isGreaterThan_sd(allIn1,getProdLegDims(freelegs,legCosts,costType),costType); % IIB2c: xi_C > xi_D (26) + if isOK + isOK = isGreaterThan_sd(allIn1,getProdLegDims(freelegs2,legCosts,costType),costType); % IIB2c: xi_C > xi_A (25) + end + end + end + end + else + % This contraction is an outer product. If either constituent is an outer product, check that both tensors + % within that object are not larger than the third tensor with which they are now being contracted. + if isOP1 + isOK = ~isGreaterThan_sd(OPmaxdim1,getProdLegDims(freelegs2,legCosts,costType),costType); % IIB2b: xi_C >= xi_A, xi_C >= xi_B (20) + end + if isOK && isOP2 + isOK = ~isGreaterThan_sd(OPmaxdim2,getProdLegDims(freelegs1,legCosts,costType),costType); % IIB2b: xi_C >= xi_A, xi_C >= xi_B (20) + end + end + end + end + % ### End of enforcing Sec. II.B.2.b,c,d (only perform outer product if there is a known tensor X with appropriate structure; only contract resulting object in another outer product or with an appropriate tensor X; enforce index dimension constraints) + + % If contraction is not prohibited, check cost is acceptable (<=muCap and, if not involving new objects, >oldmuCap) + if isOK + % If constructing an outer product which may contract with a new X, do not exclude on basis of low cost: Hence + % isnew1||isnew2||thisTensorXflag>0 + [newCost isOK] = getBuildCost(freelegs,commonlegs,legCosts,costType,oldmuCap,muCap,isnew1||isnew2||thisTensorXflag>0,costToBuild1,costToBuild2); + if ~isOK + newmuCap = min([newmuCap newCost]); + end + end + + % If cost is OK, compare with previous best known cost for constructing this object + if isOK + % Get involved tensors + tensorsInNew = tensorsIn1 | tensorsIn2; + % Find if previously constructed + objectPtr = 0; + for x=1:numel(objects{numInObjects}) + if isequal(objects{numInObjects}{x}{2},tensorsInNew) + objectPtr = x; + break; + end + end + isnew = objectPtr==0; + if isnew + % Is a new construction + objectPtr = numel(objects{numInObjects})+1; + else + % Compare new cost with best-so-far cost for construction of this object + isOK = isLessThan(newCost,objects{numInObjects}{objectPtr}{4},costType); + end + + % ### If appropriate, update tensorXlist (list of tensors which can be contracted with objects created by outer product) + if allowOPs + if isOK + % New tensor or new best cost + E_is_2 = ~any(freelegs2) && any(freelegs1); + if (~any(freelegs1) && any(freelegs2)) || E_is_2 + % New best sequence consistent with Fig.5(c). + % Determine the value of allIn, which corresponds to xi_C. (This is used in determining valid tensors X to contract with outer products). + if E_is_2 + allIn = getProdLegDims(legs2,legCosts,costType); + else + allIn = getProdLegDims(legs1,legCosts,costType); + end + % Add to tensor X list for outer products (or if already there, update the value of allIn): + if isnew + if isGreaterThan_sd(dimSquared(allIn,costType),getProdLegDims(freelegs,legCosts,costType),costType) % Enforce Eq.(27) + [tensorXlegs tensorXdims tensorXflags] = addToTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs,allIn,costType,isnew); + end + else + if isGreaterThan_sd(dimSquared(allIn,costType),getProdLegDims(freelegs,legCosts,costType),costType) % Enforce Eq.(27) + [tensorXlegs tensorXdims tensorXflags] = addToTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs,allIn,costType,isnew,~isequal(objects{numInObjects}{objectPtr}{7},zeros(1,costType))); + elseif ~isequal(objects{numInObjects}{objectPtr}{7},zeros(1,costType)) % Only need to invoke removeFromTensorXList if there might actually be an entry + [tensorXlegs tensorXdims tensorXflags] = removeFromTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs); + end + end + else + % This tensor is not an eligible tensor X for an outer product: Store a dummy value in allIn to indicate this + allIn = zeros(1,costType); + % Best cost and not consistent with Fig.5(c): Ensure does not appear in tensorXlist. Active removal only + % required if object is not new, and previous best sequence was consistent with Fig.5(c), so allIn is not a + % dummy on the old entry. + if ~isnew && ~isequal(objects{numInObjects}{objectPtr}{7},zeros(1,costType)) + [tensorXlegs tensorXdims tensorXflags] = removeFromTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs); + end + end + elseif isequal(newCost,objects{numInObjects}{objectPtr}{4}) + % Equal-best cost to a known sequence for the same tensor + if ~isequal(objects{numInObjects}{objectPtr}{7},zeros(1,costType)) + % Previous best sequence was consistent with Fig.5(c) so tensor may appear in the provisional environments list + E_is_2 = ~any(freelegs2) && any(freelegs1); + if (~any(freelegs1) && any(freelegs2)) || E_is_2 + % Determine the value of allIn, which corresponds to xi_C in Fig.5(c). + if E_is_2 + allIn = getProdLegDims(legs2,legCosts,costType); + else + allIn = getProdLegDims(legs1,legCosts,costType); + end + % If smaller than previous value, update the value of allIn: + if isGreaterThan_sd(objects{numInObjects}{objectPtr}{7},allIn,costType) + if isGreaterThan_sd(dimSquared(allIn,costType),getProdLegDims(freelegs,legCosts,costType),costType) % Enforce Eq.(27) + [tensorXlegs tensorXdims tensorXflags] = updateTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs,allIn); + else + [tensorXlegs tensorXdims tensorXflags] = removeFromTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs); + end + end + else + % Found best-equal sequence not consistent with Fig.5(c) + [tensorXlegs tensorXdims tensorXflags] = removeFromTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs); + end + %else: There already exists a best-known-cost sequence for the tensor which is not consistent with Fig.5(c), and isOK=false. Tensor does not appear in tensorXlist. No need to assign allIn. Now returning to start of main loop. + end + % else: Sequence is not capable of updating tensorXlist (not better cost, not equal cost). Also, isOK=false. No need to assign allIn. Now returning to start of main loop. + end + else + % Not doing outer products. Store a dummy value in allIn, which is never used. + allIn = []; + end + % ### Done updating tensorXlist (list of tensors which can be contracted with objects created by outer product) + end + + if isOK + % Either no previous construction, or this one is better + if ~any(commonlegs) + % ### This construction is an outer product. Note dimension of larger of the two participating tensors in newmaxdim. (This is used in enforcing index-dimension-related constraints.) + newmaxdim = getProdLegDims(legs1,legCosts,costType); + newmaxdim2 = getProdLegDims(legs2,legCosts,costType); + if isGreaterThan_sd(newmaxdim2,newmaxdim,costType) + newmaxdim = newmaxdim2; + end + % ### End recording dimension of larger of the two participating tensors in newmaxdim. + thisIsOP = true; + newseq = [seq1 seq2 0]; + else + % This construction is not an outer product. + if isOP1 + newseq = [seq2 seq1 find(commonlegs)]; + else + newseq = [seq1 seq2 find(commonlegs)]; + end + thisIsOP = false; + % ### This construction is not an outer product. Therefore store a dummy value in maxdim. (For outer products, maxdim records the dimension of the larger participating tensor, to assist in enforcing index-dimension-related constraints.) + newmaxdim = []; + % ### End storing dummy value in maxdim + end + + % Update objects{} with this construction + objects{numInObjects}{objectPtr} = {freelegs,tensorsInNew,newseq,newCost,thisIsOP,newmaxdim,allIn}; + % ### Note 1: If this tensor has the structure of Fig.5(c) and so is capable of being contracted with an outer product object, |E| is recorded in allIn (otherwise this is a dummy value). + % ### Note 2: If this tensor is constructed by outer product, the dimension of the largest participating tensor is recorded in newmaxdim. (This is used in enforcing index-dimension-related constraints.) Otherwise, this is a dummy value. + + % Flag as a new construction + newobjectflags{numInObjects}(objectPtr) = true; + + % If top level, display result + if numInObjects == numtensors + % ### If a valid contraction sequence has been found, there is no need to perform any contraction sequence more expensive than this. Set muCap accordingly. + if costType==1 + muCap = newCost; + else + muCap = numel(newCost)-1; + end + % ### Done setting muCap accordingly. + if verbosity > 1 + displayInterimCostAndSequence(newCost,newseq,costType,posindices,tracedindices); + end + end + end + end + end + end + end + end + end + + % ### Finished searching if an object has been constructed which contains all tensors, and no new outer products have been enabled on the last pass (all(tensorXflags<2)==true, indicating that there are no new entries in the list of tensors which can be contracted with outer products). + done = numel(objects{numtensors})~=0 && (all(tensorXflags<2) || ~allowOPs); % Final object has been constructed, and if outer products are allowed, also no new X's have recently been constructed + + if ~done + if all(tensorXflags<2) + % ### All X tensors have been present for an entire pass, so all permitted outer products at this cost have already been constructed. + % Increment muCap, update oldmuCap + if costType==1 + if newmuCap < muCap * max([min(legCosts) 2]) + newmuCap = muCap * max([min(legCosts) 2]); + end + end + oldmuCap = muCap; + muCap = newmuCap; + newmuCap = Inf; + else + % ### New X tensors generated this pass (some tensor X flags==2). Do another pass with same cost limit, to construct newly-allowed objects (i.e. sequences of affordable cost including at least one new outer product). + % ### This is achieved by updating oldmuCap only. Now only outer products and contractions involving newly-created tensors will satisfy mu_0 < mu <= muCap. + oldmuCap = muCap; + end + % Clear all new object flags + for a=1:numel(newobjectflags) + newobjectflags{a} = false(1,numel(newobjectflags{a})); + end + % ### Update tensor X flags (2 -> 1 -> 0): + % ### 2: Newly created this pass becomes + % ### 1: Created last pass; allow construction of cheap objects which contract with this, as they may have previously been excluded due to lack of a valid tensor X. 1 becomes... + % ### 0: Old tensor X. Standard costing rules apply. + % ### Delete redundant entries in tensorXlist (e.g. if A has a subset of the legs on B, and an equal or lower value of allIn (i.e. |E| in Fig.5(c))) + [tensorXlegs tensorXdims tensorXflags] = mergeTensorXlist(tensorXlegs,tensorXdims,tensorXflags,costType); + % ### Done updating tensor X flags + else + if numel(objects{numtensors})~=1 + error('Inappropriate number of objects containing all tensors!') + end + end +end + +% Extract final result +sequence = int32(objects{numtensors}{1}{3}); +cost = objects{numtensors}{1}{4}; +end + +function [newCost isOK] = getBuildCost(freelegs,commonlegs,legCosts,costType,oldmuCap,muCap,isnew,costToBuild1,costToBuild2) +% Get fusion cost +allLegs = freelegs | commonlegs; +isOK = true; +if costType==1 + % Is cost too high (>muCap)? + newCost = prod(legCosts(allLegs)) + costToBuild1 + costToBuild2; + if newCost > muCap + isOK = false; + end + + % Is cost too low (not made from new objects, and <=oldmuCap: This construction has been done before) + if isOK && ~isnew + if newCost <= oldmuCap + isOK = false; + newCost = Inf; + end + end +else + % Is cost too high (>muCap)? + fusionpower = sum(legCosts(allLegs,2)); + if fusionpower > muCap + isOK = false; + newCost = fusionpower; + end + + % Is cost too low (not made from new objects, and <=oldmuCap: This construction has been done before) + if isOK && ~isnew + if fusionpower <= oldmuCap + isOK = false; + newCost = Inf; + end + end + + % If cost OK, determine total cost of construction + if isOK + newCostLen = max([numel(costToBuild1) numel(costToBuild2) fusionpower+1]); + newCost = zeros(1,newCostLen); + newCost(1:numel(costToBuild1)) = costToBuild1; + if ~isempty(costToBuild2) + newCost(1:numel(costToBuild2)) = newCost(1:numel(costToBuild2)) + costToBuild2; + end + newCost(fusionpower+1) = newCost(fusionpower+1) + prod(legCosts(allLegs,1)); + end +end +end + +function flag = isLessThan(cost1,cost2,costType) +% Compares two full network costs +if costType==1 + flag = cost1 < cost2; +else + if numel(cost1) < numel(cost2) + flag = true; + elseif numel(cost1) > numel(cost2) + flag = false; + else + flag = false; + for a = numel(cost2):-1:1 + if cost1(a) < cost2(a) + flag = true; + break; + elseif cost1(a) > cost2(a) + break; + end + end + end +end +end + +function flag = isGreaterThan_sd(cost1,cost2,costType) +% Compares two single-index costs +if costType==1 + flag = cost1 > cost2; +else + flag = false; + if cost1(2) > cost2(2) + flag = true; + elseif cost1(2) == cost2(2) + if cost1(1) > cost2(1) + flag = true; + end + end +end +end + +function dim = getProdLegDims(freelegs,legCosts,costType) +if costType==1 + dim = prod(legCosts(freelegs)); +else + dim = [prod(legCosts(freelegs,1)) sum(legCosts(freelegs,2))]; +end +end + +function dim = dimSquared(dim,costType) +if costType==1 + dim = dim*dim; +else + dim = [dim(1)*dim(1) dim(2)*2]; +end +end + +function displayInterimCostAndSequence(cost,seq,costType,posindices,tracedindices) +% Displays cost and sequence +dispseq = [tracedindices seq]; +dispseq(dispseq>0) = posindices(dispseq(dispseq>0)); +disp(['Sequence: ' unpaddednum2str(dispseq)]); +if costType==1 + t = ['Cost: ' num2str(cost)]; +else + t = 'Cost: '; + for a = numel(cost):-1:2 + t = [t num2str(cost(a)) 'X^' num2str(a-1) ' + ']; %#ok + end + t = [t num2str(cost(1)) 'X^0']; +end +if ~isempty(tracedindices) + t = [t ' + tracing costs']; +end +disp(t); +end + +function [tensorXlegs tensorXdims tensorXflags] = addToTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs,allIn,costType,isnew,oldMayHaveEntry) +% Constructed a new tensor for tensorXlist, or a known tensor at same or better cost. +% If legs exactly match a known non-provisional entry, consider as a possible tighter bound on allIn. +% Otherwise, consider for provisional list if not made redundant by any non-provisional entries. +% Add to provisional list if not made redundant by any non-provisional entries. +consider = true; +ptr = find(tensorXflags==1,1,'last'); +for a=ptr:-1:1 + if isequal(tensorXlegs{a},freelegs) + % If legs exactly match a non-provisional entry, update value of allIn. + % If allIn for this entry just got increased, associated constraints have been relaxed. Flag this updated entry as provisional to trigger another pass. + if isGreaterThan_sd(allIn,tensorXdims{a},costType) + tensorXdims{a} = allIn; + tensorXflags(a) = 2; + % Flags are always in ascending order: Move re-flagged entry to end of list + tensorXdims = tensorXdims([1:a-1 a+1:end a]); + tensorXlegs = tensorXlegs([1:a-1 a+1:end a]); + tensorXflags = tensorXflags([1:a-1 a+1:end a]); + else + tensorXdims{a} = allIn; + end + consider = false; + break; + else + % Check to see if made redundant by existing non-provisional entry + if all(tensorXlegs{a}(freelegs)) && ~isGreaterThan_sd(allIn,tensorXdims{a},costType) + % All legs in freelegs are in overlap with tensorXlegs{a}, and dimension of absorbed tensor is not greater: Proposed new entry is redundant. + % (Greater dimension is allowed as it would mean more permissive bounds for a subset of legs) + consider = false; + if ~isnew + if oldMayHaveEntry + % This tensor: Excluded from list. + % Previous, higer-cost contraction sequence may have successfully made an entry. Remove it. + [tensorXlegs tensorXdims tensorXflags] = removeFromTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs); + end + end + break; + end + end +end +if consider + % Tensor not excluded after comparison with non-provisional entries: + % If legs exactly match another provisional entry, new submission is a cheaper sequence so keep only the new value of allIn. + % (This is the same subnetwork but with a better cost, and possibly a different value of \xi_C.) + if ~isnew + if oldMayHaveEntry + for a=ptr+1:numel(tensorXlegs) + if isequal(tensorXlegs{a},freelegs) + tensorXdims{a} = allIn; + consider = false; + break; + end + end + end + end + % Otherwise: This is a new tensorXlist entry + if consider + tensorXlegs{end+1} = freelegs; + tensorXflags(end+1) = 2; + tensorXdims{end+1} = allIn; + end +end +end + +function [tensorXlegs tensorXdims tensorXflags] = updateTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs,allIn) +% Found new contraction sequence for an existing entry, but with a smaller value of allIn. Update the stored value to this value. +% (This is the same subnetwork, but the \xi_C constraint has been tightened.) +for a=numel(tensorXlegs):-1:1 + if isequal(freelegs,tensorXlegs{a}) + tensorXdims{a} = allIn; + break; + end +end +% If the original entry was provisional and redundant, this one is too. No match will be found (as the redundant tensor was never recorded), and that's OK. +% If the original entry was not provisional and redundant, it has now been updated. +end + +function [tensorXlegs tensorXdims tensorXflags] = removeFromTensorXlist(tensorXlegs,tensorXdims,tensorXflags,freelegs) +% Remove this tensor from tensorXlist. +% Cannot restrict checking just to provisional portion, because might need to remove a confirmed tensor's data from the list in the scenario described in footnote 2. +for a=numel(tensorXlegs):-1:1 + if isequal(freelegs,tensorXlegs{a}) + tensorXlegs(a) = []; + tensorXflags(a) = []; + tensorXdims(a) = []; + break; + end +end +end + +function [tensorXlegs tensorXdims tensorXflags] = mergeTensorXlist(tensorXlegs,tensorXdims,tensorXflags,costType) +% Merge provisional tensors into main list and decrease all nonzero flags by one. +% For each provisional entry in turn, check if it makes redundant or is made redundant by any other provisional entries, and if it makes redundant any +% non-provisional entries. If so, delete the redundant entries. + +% Decrease non-zero tensorXflags +tensorXflags(tensorXflags>0) = tensorXflags(tensorXflags>0)-1; + +ptr = find(tensorXflags==1,1,'first'); +for a=numel(tensorXlegs):-1:ptr + % Permit provisional tensors to be eliminated by other provisional tensors (elimination by non-provisional tensors was done earlier) + for b=[ptr:a-1 a+1:numel(tensorXlegs)] + if all(tensorXlegs{b}(tensorXlegs{a})) && ~isGreaterThan_sd(tensorXdims{a},tensorXdims{b},costType) % All legs in tensorXlegs{a} overlap with tensorXlegs{b}, and dimension of absorbed tensor is not greater: tensorXlegs{a} is redundant. + tensorXlegs(a) = []; + tensorXflags(a) = []; + tensorXdims(a) = []; + break; + end + end +end + +for a=ptr-1:-1:1 + % Now permit non-provisional tensors to be eliminated by provisional tensors, as these are now confirmed + for b=ptr:numel(tensorXlegs) + if all(tensorXlegs{b}(tensorXlegs{a})) && ~isGreaterThan_sd(tensorXdims{a},tensorXdims{b},costType) % All legs in tensorXlegs{a} overlap with tensorXlegs{b}, and dimension of absorbed tensor is not greater: tensorXlegs{a} is redundant. + tensorXlegs(a) = []; + tensorXflags(a) = []; + tensorXdims(a) = []; + break; + end + end +end +end diff --git a/src/utility/permutations/invperm.m b/src/utility/permutations/invperm.m index a18dc13..9922649 100644 --- a/src/utility/permutations/invperm.m +++ b/src/utility/permutations/invperm.m @@ -1,7 +1,11 @@ function invp = invperm(p) -% invp - Compute the inverse permutation. -% invp = invperm(p) -% computes the permutation that satisfies invp(p) = 1:length(p). +% Compute the inverse of an integer permutation. +% +% Usage +% ----- +% :code:`invp = invperm(p)` +% +% computes the permutation that satisfies :code:`invp(p) = 1:length(p)`. invp(p) = 1:length(p); diff --git a/src/utility/permutations/iscircperm.m b/src/utility/permutations/iscircperm.m new file mode 100644 index 0000000..0c1ce18 --- /dev/null +++ b/src/utility/permutations/iscircperm.m @@ -0,0 +1,12 @@ +function bool = iscircperm(p) + +bool = true; +for i = 1:length(p) + if isequal(circshift(p, i), 1:length(p)) + return + end +end +bool = false; + +end + diff --git a/src/utility/permutations/isperm.m b/src/utility/permutations/isperm.m index 4a1a635..5f6617a 100644 --- a/src/utility/permutations/isperm.m +++ b/src/utility/permutations/isperm.m @@ -1,6 +1,9 @@ function bool = isperm(p) -% isperm - Check if a vector is a permutation. -% bool = isperm(p) +% Check if a vector is a permutation. +% +% Usage +% ----- +% :code:`bool = isperm(p)` bool = all(sort(p) == 1:length(p)); diff --git a/src/utility/permutations/perm2swap.m b/src/utility/permutations/perm2swap.m index 4cd3075..b873c59 100644 --- a/src/utility/permutations/perm2swap.m +++ b/src/utility/permutations/perm2swap.m @@ -3,14 +3,14 @@ % % Arguments % --------- -% p : int +% p : :class:`int` % permutation vector. % % Returns % ------- -% s : int -% list of swaps that compose into the permutation vector, where `i` indicates a swap -% between indices `i` and `i+1`. +% s : :class:`int` +% list of swaps that compose into the permutation vector, where :code:`i` indicates a swap +% between indices :code:`i` and :code:`i+1`. N = length(p); s = []; diff --git a/src/utility/plot_schmidtspectrum.m b/src/utility/plot_schmidtspectrum.m new file mode 100644 index 0000000..c63c911 --- /dev/null +++ b/src/utility/plot_schmidtspectrum.m @@ -0,0 +1,59 @@ +function [ax] = plot_schmidtspectrum(S, ax, kwargs) + +arguments + S + ax = [] + kwargs.SymmetrySort = true + kwargs.ExpandQdim = false + kwargs.Marker = '.' +end + + if isempty(ax) + figure; + ax = gca; + end + + if kwargs.Marker == '.' + kwargs.MarkerSize = 10; + else + kwargs.MarkerSize = 6; + end + + %hold(ax, 'off'); + [svals, charges] = matrixblocks(S); + if kwargs.ExpandQdim + for i = 1:length(svals) + svals{i} = reshape(repmat(svals{i}, 1, qdim(charges(i))), [], 1); + end + end + ctr = 0; + labels = arrayfun(@string, charges, 'UniformOutput', false); + lengths = cellfun(@length, svals); + ticks = cumsum(lengths); + if kwargs.SymmetrySort + for i = 1:length(svals) + semilogy(ax, ctr+(1:lengths(i)), svals{i}, 'Marker', kwargs.Marker, 'MarkerSize', kwargs.MarkerSize, 'Color', colors(i)); + if i == 1, hold(ax, 'on'); end + ctr = ctr + lengths(i); + end + set(ax, 'Xtick', ticks, 'fontsize', 10, ... + 'XtickLabelRotation', 60, 'Xgrid', 'on'); + else + [~, p] = sort(vertcat(svals{:}), 'descend'); + p = invperm(p); + for i = 1:length(svals) + semilogy(ax, p(ctr+(1:lengths(i))), svals{i}, 'Marker', kwargs.Marker, 'MarkerSize', kwargs.MarkerSize, 'Color', colors(i)); + if i == 1, hold(ax, 'on'); end + ctr = ctr + lengths(i); + end + + end + legend(ax, arrayfun(@(x) line([0,1],[0,0],'Color', colors(x), 'Marker', '.', 'MarkerSize', 10, 'linestyle', 'none'),1:numel(labels)), labels, 'Location', 'Best') + set(ax, 'TickLabelInterpreter', 'latex'); + xlim(ax, [1 - 1e-8 ticks(end) + 1e-8]); + hold off + + linkaxes(ax, 'y'); + +end + diff --git a/src/utility/randc.m b/src/utility/randc.m index cb0bbdd..d44c00d 100644 --- a/src/utility/randc.m +++ b/src/utility/randc.m @@ -14,18 +14,18 @@ % % Arguments % --------- -% m, n, p, ... : int +% m, n, p, ... : :class:`int` % integers defining the size of the output array. % % classname : :class:`char` -% datatype of the array, default :class:`double`. +% datatype of the array, default :code:`'double'`. % -% Y : numeric +% Y : :class:`numeric` % create an array of the same class as Y. % % Returns % ------- -% R : numeric +% R : :class:`numeric` % complex pseudorandom values, real and imaginary part distributed from the uniform % distribution. diff --git a/src/utility/randnc.m b/src/utility/randnc.m index 5d884bc..c69d4f0 100644 --- a/src/utility/randnc.m +++ b/src/utility/randnc.m @@ -14,18 +14,18 @@ % % Arguments % --------- -% m, n, p, ... : int +% m, n, p, ... : :class:`int` % integers defining the size of the output array. % % classname : :class:`char` -% datatype of the array, default :class:`double`. +% datatype of the array, default :code:`'double'`. % -% Y : numeric +% Y : :class:`numeric` % create an array of the same class as Y. % % Returns % ------- -% R : numeric +% R : :class:`numeric` % complex pseudorandom values, real and imaginary part distributed from the normal % distribution. diff --git a/src/utility/safesave.m b/src/utility/safesave.m new file mode 100644 index 0000000..c3c671c --- /dev/null +++ b/src/utility/safesave.m @@ -0,0 +1,14 @@ +function safesave(filename, variable) +% Safe wrapper around save. +% Clears path and makes backup of existing files, and creates directories if not existing. + +[filepath, name, ext] = fileparts(filename); +if ~isdir(filepath) + mkdir(filepath); +else + clear_path(filename); +end + +save(filename, '-struct', 'variable'); + +end diff --git a/src/utility/simulsort.m b/src/utility/simulsort.m index 107354e..e99c8b7 100644 --- a/src/utility/simulsort.m +++ b/src/utility/simulsort.m @@ -19,18 +19,18 @@ % % Keyword Arguments % ----------------- -% Dimension : int +% Dimension : :class:`int` % determine the dimension along which to sort. This behaves similarly to SORT, by default: % - for vectors, sorts the elements % - for matrices, sorts each column % - for N-D arrays, sorts along the first non-singleton dimension. % -% Direction : 'ascend' or 'descend' +% Direction : :class:`char`, 'ascend' or 'descend' % specify the sorting direction, defaults to 'ascend'. % % Returns % ------- -% I : int +% I : :class:`int` % permutation vector that brings the input arrays into sorted order. % % array1, array2, ... @@ -77,7 +77,7 @@ end else for k = length(arrays)-1:-1:1 - for i = 1:size(I, 2) + for i = 1:size(I, 1) varargout{k}(i, :) = arrays{k}(i, I(i, :)); end end @@ -113,7 +113,7 @@ % permute index for i = 1:size(I, 1) - I(i, :) = I(i, I_(:, i)); + I(i, :) = I(i, I_(i, :)); end end end diff --git a/src/utility/simulsortrows.m b/src/utility/simulsortrows.m index 0be7897..150e89e 100644 --- a/src/utility/simulsortrows.m +++ b/src/utility/simulsortrows.m @@ -1,33 +1,34 @@ function [I, varargout] = simulsortrows(arrays, kwargs) % Simultaneous sorting of several input arrays by row. -% Sorts the rows such that equal rows of array{i} appear sorted by the rows of array{i+1}. +% +% Sorts the rows such that equal rows of array{i} appear sorted by the rows of array{i+1}. % % This is achieved by sorting rows from end to front, making use of the fact that SORTROWS % is stable and thus will not mess up the order of later rows when earlier rows are equal. % % Usage % ----- -% [I, array1, array2, ...] = simulsortrows(array1, array2, ..., kwargs) +% :code:`[I, array1, array2, ...] = simulsortrows(array1, array2, ..., kwargs)` % -% Arguments -% --------- +% Repeating arguments +% ------------------- % array1, array2, ... % arrays of equal size that need to be sorted. These can be of any type that supports % SORTROWS. % % Keyword Arguments % ----------------- -% Col : int +% Col : :class:`int` % vector of indices that specifies the columns used for sorting. % -% Direction : 'ascend' or 'descend' +% Direction : :class:`char`, 'ascend' or 'descend' % specify the sorting direction. You can also specify a different direction for each % column by using a cell array of 'ascend' and 'descend' of the same size as Col, such % that corresponding elements are sorted ascending or descending. % % Returns % ------- -% I : int +% I : :class:`int` % permutation vector that brings the input arrays into rowsorted order. % % array1, array2, ... diff --git a/src/utility/simulunique.m b/src/utility/simulunique.m index 27a2e6a..adf65f6 100644 --- a/src/utility/simulunique.m +++ b/src/utility/simulunique.m @@ -1,5 +1,5 @@ function varargout = simulunique(varargin) -%SIMULUNIQUE Set unique over multiple arrays. +% Set unique over multiple arrays. nlhs = max(1, nargout); diff --git a/src/utility/sparse/SparseArray.m b/src/utility/sparse/SparseArray.m new file mode 100644 index 0000000..0428552 --- /dev/null +++ b/src/utility/sparse/SparseArray.m @@ -0,0 +1,1186 @@ +classdef SparseArray + % Class for multi-dimensional sparse arrays. + % + % Limited to arrays with a total number of elements of at most 2^48-1. + + %% Properties + properties (Access = private) + var = sparse([]) % (:, 1) double + sz = [] % (1, :) + end + + + %% Constructors + methods + function a = SparseArray(varargin) + % Create a sparse array. + % + % Usage + % ----- + % :code:`a = SparseArray(subs, vals, sz)` + % uses the rows of :code:`subs` and :code:`vals` to generate a sparse array + % :code:`a` of size :code:`sz = [m1 m2 ... mn]`. :code:`subs` is a + % :code:`p` x :code:`n` array specifying the subscripts of the nonzero values + % to be inserted into :code:`a`. The k-th row of :code:`subs` specifies the + % subscripts for the k-th value in :code:`vals`. + % The argument :code:`vals` may be scalar, in which case it is expanded to be + % the same length as :code:`subs`, i.e., it is equivalent to + % :code:`vals * (p, 1)`. + % In the case of duplicate subscripts in :code:`subs`, the corresponding + % values are added. + % + % :code:`a = SparseArray` + % Empty constructor. + % + % :code:`a = SparseArray(b)` + % Copies/converts :code:`b` if it is a :class:`.SparseArray`, a dense array or + % a sparse matrix. + % + % :code:`a = SparseArray(b, sz)` + % Copies/converts :code:`b` if it is a :class:`.SparseArray`, a dense array or + % a sparse matrix, and sets the size of :code:`a` to :code:`sz` + % + % Example + % ------- + % .. code-block:: matlab + % + % >> subs = [1 1 1; 1 1 3; 2 2 2; 4 4 4; 1 1 1; 1 1 1]; + % >> vals = [0.5; 1.5; 2.5; 3.5; 4.5; 5.5]; + % >> sz = [4 4 4]; + % >> a = SparseArray(subs, vals, sz) %<-- sparse 4x4x4 + + if (nargin == 0) || ((nargin == 1) && isempty(varargin{1})) + % Empty constructor + return; + + elseif nargin == 1 + % Single argument + + source = varargin{1}; + + switch(class(source)) + + % copy constructor + case 'SparseArray' + a.var = source.var; + a.sz = source.sz; + + % sparse matrix, dense array + case {'numeric', 'logical', 'double'} + a.var = reshape(source, [], 1); + if ~issparse(source) + a.var = sparse(a.var); + end + a.sz = size(source); + + otherwise + error('sparse:UnsupportedConstructor', 'Unsupported use of SparseArray constructor.'); + + end + + + elseif nargin == 2 + % Two arguments + + var = varargin{1}; + sz = varargin{2}(:).'; + if isempty(sz) + sz = size(var); + end + if numel(sz) < 2 + sz = [sz, 1]; + end + if numel(var) ~= prod(sz) + error('sparse:ArgumentError', 'Incompatible size vector and input array.'); + end + + switch(class(var)) + + % copy constructor + case 'SparseArray' + a.var = var.var; + a.sz = sz; + + % sparse matrix, dense array + case {'numeric', 'logical', 'double'} + a.var = reshape(var, [], 1); + if ~issparse(var) + a.var = sparse(a.var); + end + a.sz = sz; + + otherwise + error('sparse:UnsupportedConstructor', 'Unsupported use of SparseArray constructor.'); + + end + + + elseif nargin == 3 + % Three arguments + subs = varargin{1}; + vals = varargin{2}; + sz = varargin{3}(:)'; + if numel(sz) < 2 + sz = [sz, 1]; + end + + ind = sub2ind_(sz, subs); + a.var = sparse(ind, ones(size(ind)), vals, prod(sz), 1); + a.sz = sz; + + else + error('sparse:UnsupportedConstructor', 'Unsupported use of SparseArray constructor.'); + end + + end + + end + + + %% Methods + methods + + function a = abs(a) + % Absolute value. + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % b : :class:`.SparseArray` + % output array. + a.var = abs(a.var); + end + + function a = chop(a, tol) + % Set all nonzero values in :class:`SparseArray` who's absolute value is below + % a given threshold to zero. + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % tol : :class:`double` , optional + % threshold tolerance for absolute values of entries, defaults to + % :code:`1e-15`. + % + % Returns + % ------- + % b : :class:`.SparseArray` + % sparse array with entries of absolute value below :code:`tol` set to zero. + arguments + a + tol = 1e-15 + end + + [r, c, v] = find(a.var); + drop = abs(v) < tol; + a.var(r(drop), c(drop)) = 0; + end + + function a = conj(a) + % Complex conjugate. + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % b : :class:`.SparseArray` + % output array. + a.var = conj(a.var); + end + + function a = ctranspose(a) + % Complex conjugate transpose. + % + % Only defined for 2D sparse arrays. + % + % Usage + % ----- + % :code:`b = ctranspose(a)` + % + % :code:`b = a'` + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % b : :class:`.SparseArray` + % output array. + assert(ismatrix(a), 'sparse:RankError', 'ctranspose is only defined for 2D sparse arrays.'); + a = permute(conj(a), [2, 1]); + end + + function d = diag(a) + assert(ismatrix(a), 'sparse:RankError', 'diag is only defined for 2D sparse arrays.'); + d = diag(reshape(a.var, a.sz)); + end + + function disp(a) + nz = nnz(a); + + if nz == 0 + fprintf('all-zero %s of size %s\n', class(a), ... + dim2str(a.sz)); + return + end + + fprintf('%s of size %s with %d nonzeros:\n', class(a), ... + dim2str(a.sz), nz); + + if (nz > 1000) + r = input('Big array, do you want to print all nonzeros? (y/n) ', 's'); + if ~strcmpi(r, 'y'), return, end + end + + [subs, ~, vals] = find(a); + spc = floor(log10(max(double(subs), [], 1))) + 1; + fmt_subs = sprintf('%%%du,', spc(1:end-1)); + fmt_subs = sprintf('%s%%%du', fmt_subs, spc(end)); + fmt = sprintf('\t(%s)%%s\n', fmt_subs); + S = evalc('disp(vals)'); % abuse builtin display + S = splitlines(S); + if contains(S{1}, '*') % big numbers + fprintf('%s\n', S{1}); + S = S(2:end); + end + for i = 1:nz + fprintf(fmt, subs(i,:), S{i}); + end + end + + function varargout = eig(a, varargin) + % Find eigenvalues and eigenvectors of a 2D sparse array. + % + % See Also + % -------- + % `Documentation for builtin Matlab eig `_. + assert(ismatrix(a), 'sparse:RankError', 'eig is only defined for 2D sparse arrays.'); + [varargout{1:nargout}] = eig(reshape(a.var, a.sz), varargin{:}); + varargout = cellfun(@maybesparse_, varargout, 'UniformOutput', false); + end + + function varargout = eigs(a, varargin) + % Find a few eigenvalues and eigenvectors of a 2D sparse array. + % + % See Also + % -------- + % `Documentation for builtin Matlab eigs `_. + assert(ismatrix(a), 'sparse:RankError', 'eigs is only defined for 2D sparse arrays.'); + [varargout{1:nargout}] = eigs(reshape(a.var, a.sz), varargin{:}); + varargout = cellfun(@maybesparse_, varargout, 'UniformOutput', false); + end + + function [subs, idx, vals] = find(a) + % Find subscripts of nonzero elements in a sparse array. + % + % Usage + % ----- + % :code:`idx = find(a)` + % + % :code:`[subs, idx, vals] = find(a)` + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array + % + % Returns + % ------- + % subs : (:, :) :class:`int` + % subscripts of nonzero array entries. + % + % idx : (:, 1) :class:`int` + % linear indices of nonzero array entries. + % + % var : (:, 1) :class:`double` + % values of nonzero array entries. + [idx, ~, vals] = find(a.var); + if nargout == 1 + subs = idx; + return + end + subs = ind2sub_(a.sz, idx); + end + + function d = full(a) + % Convert a sparse array to a dense array. + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % d : :class:`double` + % dense output array. + d = reshape(full(a.var), a.sz); + end + + function a = groupind(a, g) + % Group (block) specified sets of contiguous indices. + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % g : (1, :) :class:`int` + % list of number of contiguous indices to be grouped in each index of the + % output tensor. + % + % Returns + % ------- + % b : :class:`.SparseArray` + % output array with grouped indices. + % + % Example + % ------- + % .. code-block:: matlab + % + % >> a = SparseArray.random([2, 3, 4, 5, 6], .1); + % >> b = groupind(a, [3, 2]); %<-- sparse 24x30 + assert(sum(g) == ndims(a), 'sparse:InvalidGrouping', 'Invalid grouping.') + r = zeros(1, length(g)); + offset = 0; + for i = 1:length(g) + r(i) = prod(a.sz(1+offset:g(i)+offset)); + offset = offset + g(i); + end + a = reshape(a, r); + end + + function a = imag(a) + % Complex imaginary part of sparse array. + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % b : :class:`.SparseArray` + % output array with real entries corresponding to the imaginary part of the + % entries of :code:`a`. + a.var = imag(a.var); + end + + function bool = ismatrix(a) + bool = ndims(a) == 2; + end + + function bool = isnumeric(~) + % Determine whether input is numeric. + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % bool : :class:`logical` + % defaults to :code:`true` for :class:`SparseArray`. + bool = true; + end + + function bool = isscalar(~) + % Determine whether input is scalar. + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % bool : :class:`logical` + % defaults to :code:`false` for :class:`.SparseArray`. + bool = false; + end + + function bool = isrow(a) + % Determine whether input is row vector. + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % bool : :class:`logical` + bool = ismatrix(a) && size(a, 1) == 1; + end + + function bool = isstruct(~) + bool = false; + end + + function bool = istriu(a) + bool = istriu(spmatrix(a)); + end + + function c = ldivide(a, b) + % Elementwise left division for sparse arrays. + % + % Usage + % ----- + % :code:`ldivide(a, b)` + % + % :code:`a .\ b` + % + % where :code:`a` or :code:`b` is a :class:`SparseArray`. :code:`a`and :code:`b` + % must have the same size, unless one is a scalar. + % + % Arguments + % --------- + % a, b : :class:`.SparseArray` or :class:`double` + % input arrays to be divided. + % + % Returns + % ------- + % c : :class:`.SparseArray` + % elementwise left division of :code:`b` by :code:`a`. + c = rdivide(b, a); + end + + function c = minus(a, b) + % Elementwise subtraction for sparse arrays. + % + % Usage + % ----- + % :code:`minus(a, b)` + % + % :code:`a - b` + % + % where :code:`a`or :code:`b` is a :class:`.SparseArray`. :code:`a`and :code:`b` + % must have the same size, unless one of the is scalar. Scalar can be subtracted + % from a sparse array of any size, resulting in a dense array. + % + % Arguments + % --------- + % a, b : :class:`.SparseArray` or :class:`double` + % intput arrays. + % + % Returns + % ------- + % c : :class:`.SparseArray` or :class:`double` + % output array. + % + % Example + % ------- + % .. code-block:: matlab + % + % >> a = SparseArray.random([4 3 2], .1); + % >> b = SparseArray.random([4 3 2], .1); + % >> a - b %<-- sparse + % >> a - 5 %<-- dense + % >> a - 0 %<-- dense + % >> a - full(a) %<-- dense + c = plus(a, -b); + end + + function c = mrdivide(a, b) + % Matrix right division for sparse arrays. + % + % Usage + % ----- + % :code:`mrdivide(a, b)` + % + % :code:`a / b` + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % intput array. + % + % b : :class:`double` + % scalar to divide by. + % + % Returns + % ------- + % c : :class:`.SparseArray` + % output array. + % + % Example + % ------- + % .. code-block:: matlab + % + % >> a = SparseArray.random([4 3 2], .1); + % >> a / 3 %<-- sparse + assert(isscalar(b), 'sparse:ScalarDivide', 'SparseArray.mrdivide only supports the scalar case.') + c = a; + c.var = c.var/b; + return; + end + + function c = mtimes(a, b) + % Matrix multiplication for 2D sparse arrays. + % + % Usage + % ----- + % :code:`mtimes(a, b)` + % + % :code:`a * b` + % + % where :code:`a` or :code:`b` is a :class:`.SparseArray`. + % + % See Also + % -------- + % `mtimes `_ + if isscalar(a) + c = b; + c.var = a * c.var; + return + elseif isscalar(b) + c = a; + c.var = b * c.var; + return + end + + sz = [size(a,1) size(b,2)]; + + if isa(a, 'SparseArray') + a = reshape(a.var, a.sz); + end + + if isa(b, 'SparseArray') + b = reshape(b.var, b.sz); + end + + % always return sparse array; TODO: make sure this doesn't break things + c = SparseArray(a * b, sz); + end + + function n = ndims(t) + % Number of dimensions of a sparse array. + % + % Note that unlike the `builtin Matlab behavior ` + % trailing singleton dimensions are not ignored. + % + % Example + % ------- + % .. code-block:: matlab + % + % >> a = SparseArray.random([4 3 1], .1); + % >> ndims(a) %<-- returns 3 + n = size(t.sz, 2); + end + + function n = nnz(a) + % Number of nonzero elements in sparse array. + n = nnz(a.var); + end + + function nz = nonzeros(a) + % Returns a full column vector of the nonzero elements of :code:`a`. + [~, ~, nz] = find(a); + end + + function nrm = norm(a) + % Frobenius norm of a sparse array. + nrm = norm(a.var); + end + + function n = numArgumentsFromSubscript(varargin) + n = 1; + end + + function n = numel(a) + % Number of elements in a sparse array. + n = prod(size(a)); + end + + function c = outer(a, b) + % Outer product of two sparse arrays. + % + % Warning + % ------- + % Not :code:`kron`. + varC = kron(b.var, a.var); % ordering is counterintuitive here + c = SparseArray(varC, [size(a), size(b)]); + end + + function a = permute(a, order) + % Permute sparse array dimensions. + if length(a.sz) < length(order) + a = reshape(a, [a.size, ones(1,length(order)-length(a.size))]); + end + if issorted(order) % don't rebuild t if it was a trivial permute + return + end + sz1 = a.sz; + sz2 = sz1(order); + [idx, ~, v] = find(a.var); + idx2 = sub2ind_(sz2, ind2sub_(sz1, idx, order)); + a = SparseArray(sparse(idx2, ones(size(idx2)), v, numel(a.var), 1), sz2); + end + + function c = plus(a, b) + % Elementwise addition for sparse arrays. + % + % Usage + % ----- + % + % :code:`plus(a, b)` + % + % :code:`a + b` + % + % where :code:`a` and :code:`b` must have the same size, + % unless one is a scalar. A scalar can be added to a sparse array of any size. + % + % Arguments + % --------- + % a, b : :class:`.SparseArray` or :class:`double` + % input arrays. + % + % Returns + % ------- + % c : :class:`.SparseArray` + % output array. + % + % Example + % ------- + % .. code-block:: matlab + % + % >> a = SparseArray.random([4 3 2], .1); + % >> a + b %<-- sparse + % >> a + 5 %<-- dense + % >> a + 0 %<-- dense + % >> a + full(a) %<-- dense + if (isa(a, 'double') && ~issparse(a)) || (isa(b, 'double') && ~issparse(b)) + c = full(a) + full(b); + return; + end + c = SparseArray(a); b = SparseArray(b); + c.var = c.var + b.var; + end + + function a = power(a, b) + % Elementwise power for sparse array. + assert(isscalar(b), 'sparse:NonScalarPower', 'SparseArray only supports power with a scalar.') + a.var = a.var.^b; + end + + function varargout = qr(a, varargin) + % Orthogonal-triangular decomposition for a sparse array. + % + % See Also + % -------- + % `Documentation for builtin Matlab qr `_. + assert(ismatrix(a), 'sparse:RankError', 'qr is only defined for 2D sparse arrays.'); + [varargout{1:nargout}] = qr(reshape(a.var, a.sz), varargin{:}); + varargout = cellfun(@maybesparse_, varargout, 'UniformOutput', false); + end + + function [Q, R] = qrpos(a, varargin) + % Positive orthogonal-triangular decomposition for a sparse array. + assert(ismatrix(a), 'sparse:RankError', 'qrpos is only defined for 2D sparse arrays.'); + [Q, R] = qr(a, varargin{:}); + if isrow(Q) + Q = Q * sign(R(1)); + R = R * sign(R(1)); + else + D = diag(R); + D(abs(D) < 1e-10) = 1; + D = sign(D); + Q = Q * diag(D); % TODO: implement broadcasting for .* + R = diag(D) * R; + end + end + + function c = rdivide(a, b) + % Elementwise right division for sparse arrays. + % + % Usage + % ----- + % :code:`rdivide(a, b)` + % + % :code:`a ./ b` + % + % where :code:`a` or :code:`b` is a :class:`.SparseArray`. :code:`a`and + % :code:`b` must have the same size, unless one is a scalar. + % + % Arguments + % --------- + % a, b : :class:`.SparseArray` or :class:`double` + % input arrays to be divided. + % + % Returns + % ------- + % c : :class:`.SparseArray` + % elementwise left division of :code:`a` by :code:`b`. + % + % Example + % ------- + % .. code-block:: matlab + % + % >> a = SparseArray.random([4 3 2], .1); + % >> a ./ 5 %<-- sparse + % >> 5 ./ a %<-- dense + % >> a ./ full(a) %<-- sparse + % >> full(a) ./ a %<-- dense + if isa(a, 'SparseArray') + c = a; + if isa(b, 'SparseArray') + c.var = c.var ./ b.var; + else + c.var = a.var ./ b(:); + end + elseif isa(b, 'SparseArray') + c = a ./ full(b); + end + end + + function a = real(a) + % Complex real part of sparse array. + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % b : :class:`.SparseArray` + % output array with real entries corresponding to the real part of the + % entries of :code:`a`. + a.var = real(a.var); + end + + function a = reshape(a, varargin) + % Reshape sparse array. + if nargin == 2 + a.sz = varargin{1}; + else + sz = ones(1, numel(varargin)); + I = []; + for i = 1:numel(varargin) + if isempty(varargin{i}) + I = i; + else + sz(i) = varargin{i}; + end + end + sz(I) = prod(a.sz) / prod(sz); + a.sz = sz; + end + end + + function a = sign(a) + % Signum function. + a.var = sign(a.var); + end + + function varargout = size(a, i) + if nargin == 1 + sz = a.sz; + else + sz = ones(1, max(i)); + sz(1:length(a.sz)) = a.sz; + sz = sz(i); + end + + if nargout <= 1 + varargout = {sz}; + else + varargout = num2cell(sz); + end + end + + function b = sparse(a) + % Convert 2D sparse array to a sparse matrix. + assert(ismatrix(a), 'sparse:NotMatrix', sprintf('Cannot convert array of rank %d to sparse matrix.', ndims(a))) + b = reshape(a.var, a.size); + end + + function b = spmatrix(a) + % Convert 2D sparse array to a sparse matrix. + % + % See Also + % -------- + % :code:`sparse` + b = sparse(a); + end + + function a = squeeze(a) + % Remove singleton dimensions from a sparse array. + % + % Usage + % ----- + % :code:`b = squeeze(a)` + % + % returns a sparse array :code:`b` with the same elements as :code:`a` but + % with all the singleton dimensions removed. + % + % Example + % ------- + % .. code-block:: matlab + % + % >> squeeze(SparseArray.random([2, 1, 3], 0.5)) %<-- returns a 2 x 3 SparseArray + % >> squeeze(SparseArray([1, 1, 1], 1, [1, 1, 1])) %<-- returns a scalar + + if sum(a.sz > 1) == 0 + a = full(a.var); + return + end + + % always give n x 1 SparseArray in case of only 1 non-singleton dimension, + % consistent with class constructor + a.sz = [a.size(a.size>1), ones(1, 2-sum(a.size>1))]; + end + + function a = subsasgn(a, s, rhs) + % Subscripted assignment for sparse array. + assert(strcmp(s(1).type, '()'), 'sparse:index', 'only () indexing allowed'); + + if length(s(1).subs) > 1 % non-linear indexing + assert(length(s(1).subs) == size(a.sz, 2), 'sparse:index', ... + 'number of indexing indices must match tensor size.'); + assert(all(a.sz >= cellfun(@max, s(1).subs)), 'sparse:bounds', ... + 'out of bounds assignment disallowed'); + s(1).subs = {sub2ind(a.sz, s(1).subs{:})}; + end + + rhs = full(rhs); + + [I, ~, V] = find(a.var); + [lia, locb] = ismember(s(1).subs{1}, I); + newI = vertcat(I, s(1).subs{1}(~lia)); + newJ = ones(size(newI)); + V(locb(lia)) = rhs(lia); + newV = vertcat(V, rhs(~lia)); + + a.var = sparse(newI, newJ, newV, ... + size(a.var, 1), size(a.var, 2)); + end + + function a_sub = subsref(a, s) + % Subscripted reference for a sparse array. + % + % Usage + % ----- + % :code:`a_sub = a(i)` + % linear indexing for SparseArray with only one non-singleton dimension, + % returns a scalar. + % + % :code:`a_sub = a(i1, i2, ..., iN)` + % where each :code:`in` is an integer index, returns a scalar. + % + % :code:`a_sub = a(R1, R2, ..., RN)` + % where each :code:`Rn` is either a colon, an array of integers representing a + % slice in this dimension or a logical array representing the logical indexing + % of this dimension. Returns a sparse array. + % + % :code:`a_sub = a(S)` + % where :code:`S` is a :code:`1` x :code:`n` array of integer subscripts (for + % dimensions that are indexed) or zeroes (for dimensions that are not + % indexed). Returns a sparse array. + % + % Example + % ------- + % .. code-block:: matlab + % + % >> a = SparseArray([4, 4, 4; 2, 2, 1; 2, 3, 2], [3; 5; 1], [4, 4, 4]); + % >> a(1, 2, 1) %<-- returns zero + % >> a(4, 4, 4) %<-- returns 3 + % >> a(2, :, :) %<-- returns a 1 x 4 x 4 sparse array + % >> a([0, 4, 0]) %<-- returns a 4 x 1 x 4 sparse array + % >> a([4, 4, 0]) %<-- returns a 1 x 1 x 4 sparse array + + switch s(1).type + + case '()' % regular subscript indexing + + % linear subscript indexing with only one non-singleton dimension + if (numel(s(1).subs) == 1 && numel(s(1).subs{1}) == 1 && sum(size(a)~=1) == 1) + subs = a.size; + subs(subs~=1) = s(1).subs{1}; + a_sub = full(a.var(sub2ind_(a.size, subs))); + + % regular multi-dimensional subscript indexing + elseif (numel(s(1).subs) == ndims(a)) + + % xtract the subdimensions to be extracted from t + sbs = s(1).subs; + + % Error check that range is valid + okcolon = cellfun(@(x) ischar(x) && x == ':', sbs); + okrange = cellfun(@(x) isnumeric(x) && isreal(x) && ... + ~any(isnan(x)) && ~any(isinf(x)) && ... + isequal(x,round(x)) && all(x > 0), sbs); + oklogical = arrayfun(@(i) islogical(sbs{i}) && ... + length(sbs{i}) == a.size(i), 1:ndims(a)); + if ~all(okcolon | okrange | oklogical) + error('Invalid subscript.'); + end + + % simple case: all dimensions indexed + indexed = cellfun(@isnumeric, sbs) & cellfun(@(x) length(x) == 1, sbs); + if all(indexed) + a_sub = full(a.var(sub2ind_(a.size, [sbs{:}]))); + return; + end + + % convert logical indexing to integer ranges for easy handling + for i = find(oklogical) + sbs{i} = find(sbs{i}); + end + + % extract subscripts and values of nonzero elements + [subs, ~, nz] = find(a); + + % some trickery to do actual slicing + f = true(size(nz)); % auxiliary array of booleans + new_sz = a.sz; % initialize size of sliced tensor + for i = fliplr(1:ndims(a)) + if strcmp(sbs{i}, ':') + continue; % do nothing if not sliced + end + A = sbs{i}; + if length(A) ~= length(unique(A)) + error('Repeated index in position %i.', i); + end + if ~isempty(subs) + % fast intersect for this dimension + B = subs(:, i); + P = false(max(max(A),max(B))+1,1); + P(A+1) = true; + % throw away anything that has no intersect with the slice in this dimension + f = and(f, P(B+1)); + % relabel subscripts in this dimension to match slice + % order + [~, ~, temp] = unique([A(:); subs(f, i)], 'stable'); + subs(f, i) = temp(length(A)+1:end); + end + % adjust size in this dimension + new_sz(i) = length(A); + end + if isempty(subs) + a_sub = SparseArray([], [], new_sz); + else + a_sub = SparseArray(subs(f, :), nz(f), new_sz); + end + + return; + + % hacky indexing using an array + elseif (numel(s(1).subs) == 1) && (numel(s(1).subs{1}) == ndims(a)) + + % extract the subdimensions to be extracted from t + sbs = s(1).subs{1}; + + % error check that range is valid: each subscript must be a nonnegative integer + if ~ (isnumeric(sbs) && isreal(sbs) && ~any(isnan(sbs)) && ~any(isinf(sbs)) ... + && all(sbs >= 0)) + error('Invalid subscript.'); + end + + indexed = sbs ~= 0; + + if all(indexed) % if all dimensions are indexed with integer, return scalar + a_sub = full(a.var(sub2ind_(a.size, sbs))); + return; + end + + new_sz = a.size; + new_sz(indexed) = 1; % set indexed dimensions to 1 + + [subs, ~, nz] = find(a); + good_ind = all(subs(:, indexed) == sbs(indexed), 2); + good_subs = subs(good_ind, :); + good_vals = nz(good_ind); + good_subs(:, indexed) = 1; % set indexed subscripts to 1 for new subs + + a_sub = SparseArray(good_subs, good_vals, new_sz); % construct output + return; + + else + error('sparse:InvalidIndexing', 'Incorrect indexing into SparseArray.') + end + + otherwise + % only overload parentheses indexing + a_sub = builtin('subsref', a, s); + end + end + + function s = sum(a) + % Sum of all elements of a SparseArray. + s = full(sum(a.var)); + end + + function varargout = svd(a, varargin) + % Singular value decomposition. + error('Not supported for sparse arrays.') + end + + function varargout = svds(a, varargin) + % Find a few singular values and vectors. + % + % See Also + % -------- + % `Documentation for builtin Matlab svds `_. + assert(ismatrix(a), 'sparse:RankError', 'svds is only defined for 2D sparse arrays.'); + [varargout{1:nargout}] = svds(reshape(a.var, a.sz), varargin{:}); + varargout = cellfun(@maybesparse_, varargout, 'UniformOutput', false); + end + + function c = times(a, b) + % Array multiplication for sparse tensors. + % + % Usage + % ----- + % :code:`c = times(a, b)` + % + % :code:`a .* b` + % + % where :code:`a` or :code:`b` is a sparse array. :code:`a` and :code:`b` must + % have the same size, unless one is a scalar. A scalar can be be multiplied by + % a sparse array of any size. + % + % Example + % ------- + % .. code-block:: matlab + % + % >> a = SparseArray.random([4 3 2], .1); + % >> b = SparseArray.random([4 3 2], .1); + % >> a .* b %<-- sparse + % >> a .* 5 %<-- sparse + % >> a .* 0 %<-- sparse + % >> a .* full(a) %<-- sparse + if isscalar(b) || ~isa(b, 'SparseArray') + c = SparseArray(a.var .* b(:), a.sz); + elseif isscalar(a) || ~isa(a, 'SparseArray') + c = SparseArray(b.var .* a(:), b.sz); + else + c = SparseArray(b.var .* a.var, b.sz); + end + end + + function a = transpose(a) + % Transpose. + % + % Only defined for 2D sparse arrays. + % + % Usage + % ----- + % :code:`b = transpose(a)` + % + % :code:`b = a.'` + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % b : :class:`.SparseArray` + % output array. + assert(ismatrix(a), 'sparse:RankError', 'ctranspose is only defined for 2D sparse arrays.'); + a = permute(a, [2, 1]); + end + + function a = uminus(a) + % Unary minus. + % + % Usage + % ----- + % :code:`b = uminus(a)` + % + % :code:`b = -a` + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % b : :class:`.SparseArray` + % output array. + a.var = -a.var; + end + + function a = uplus(a) + % Unary plus. + % + % Usage + % ----- + % :code:`b = uplus(a)` + % + % :code:`b = +a` + % + % Arguments + % --------- + % a : :class:`.SparseArray` + % input array. + % + % Returns + % ------- + % b : :class:`.SparseArray` + % output array. + return; + end + + function jl = mat2jl(a) + jl = struct('classname', 'SparseArray'); + jl.sz = mat2jl(a.sz); + jl.var = mat2jl(a.var); + end + end + + methods (Static) + function a = delta(numinds, inddim) + % Create delta- (ghz-) array with given number of indices and index dimension. + % + % Arguments + % --------- + % numinds : :class:`int` + % number of indices of delta-array. + % + % inddim : :class:`int` + % dimension of each index of delta-array. + % + % Returns + % ------- + % a : :class:`.SparseArray` + % output delta-array. + a = SparseArray(repmat(1:inddim, numinds, 1)', 1, repmat(inddim, 1, numinds)); + end + + function a = zeros(sz) + a = SparseArray([], [], sz); + end + + function a = random(sz, density) + % Create a random complex sparse array. + % + % Arguments + % --------- + % sz : (1, :) :class:`int` + % size of the sparse array + % + % density : :class:`double` + % density of nonzero elements (0 < :code:`density` < 1) + a = SparseArray([], [], sz); + a.var = sprandn(numel(a), 1, density); + idx = find(a.var); + a.var(idx) = a.var(idx) + 1i * randn(size(a.var(idx))); + end + end + +end diff --git a/src/utility/sparse/maybesparse_.m b/src/utility/sparse/maybesparse_.m new file mode 100644 index 0000000..d5c0436 --- /dev/null +++ b/src/utility/sparse/maybesparse_.m @@ -0,0 +1,6 @@ +function a = maybesparse_(a) +if issparse(a) + a = SparseArray(a); +end +end + diff --git a/src/utility/swapvars.m b/src/utility/swapvars.m index f1c6352..5a53f00 100644 --- a/src/utility/swapvars.m +++ b/src/utility/swapvars.m @@ -1,7 +1,10 @@ function [B, A] = swapvars(A, B) -% swapvars - Exchange two variables. -% [a, b] = swapvars(a, b) -% stores the value of a in b, and the value of b in a. +% Exchange two variables. +% +% Usage +% ----- +% :code:`[a, b] = swapvars(a, b)` +% stores the value of :code:`a` in :code:`b`, and the value of :code:`b` in :code:`a`. end diff --git a/src/utility/time2str.m b/src/utility/time2str.m new file mode 100644 index 0000000..500c996 --- /dev/null +++ b/src/utility/time2str.m @@ -0,0 +1,45 @@ +function timeStr = time2str(time, unit, precision) +% Returns string version of a duration in seconds. +% +% Usage +% ----- +% :code:`timeStr = time2str(time, unit, precision)` + +if nargin < 3, precision = 1; end + + +if nargin == 1 || isempty(unit) + if time >= 3600 + unit = 'h'; + elseif time >= 60 + unit = 'm'; + else + unit = 's'; + end +end + + +switch unit + case 'h' + time = time / 3600; + + case 'm' + time = time / 60; + + case 's' + + case 'ms' + time = time * 1000; + + case 'mus' + time = time * 1000000; + +end + + +timeStr = sprintf('%0.*f%s', precision, time, unit); + + +end + + diff --git a/src/utility/uninit/uninit.mexw64 b/src/utility/uninit/uninit.mexw64 new file mode 100644 index 0000000..7b27d45 Binary files /dev/null and b/src/utility/uninit/uninit.mexw64 differ diff --git a/src/utility/validations/mustBeEqualLength.m b/src/utility/validations/mustBeEqualLength.m index 6359ddd..b8ffbde 100644 --- a/src/utility/validations/mustBeEqualLength.m +++ b/src/utility/validations/mustBeEqualLength.m @@ -1,10 +1,11 @@ function mustBeEqualLength(a, b) -% mustBeEqualLength - Validate that the inputs are of equal length. -% mustBeEqualLength(a, b) throws an error if length(a) ~= length(b) +% Validate that the inputs are of equal length. +% +% :code:`mustBeEqualLength(a, b)` throws an error if :code:`length(a) ~= length(b)` % -% Class support: -% All classes that define these methods: -% length +% Note +% ---- +% Supported by all classes that define these methods: :code:`length` if length(a) ~= length(b) throwAsCaller(createValidatorException('TensorTrack:validators:mustBeEqualLength')); diff --git a/src/utility/validations/mustBeSorted.m b/src/utility/validations/mustBeSorted.m index 492014e..d6fec5c 100644 --- a/src/utility/validations/mustBeSorted.m +++ b/src/utility/validations/mustBeSorted.m @@ -1,10 +1,11 @@ function mustBeSorted(A) -% mustBeSorted - Validate that value is sorted. -% mustBeSorted(A) throws an error if A is not sorted. +% Validate that input is sorted. +% +% :code:`mustBeSorted(A)` throws an error if :code:`A` is not sorted. % -% Class support: -% All classes that define these methods: -% issorted +% Note +% ---- +% Supported by all classes that define these methods: :code:`issorted` if ~issorted(A) throwAsCaller(createValidatorException('TensorTrack:validators:mustBeSorted')); diff --git a/src/utility/validations/mustBeUnique.m b/src/utility/validations/mustBeUnique.m index 3a11e51..466d0d0 100644 --- a/src/utility/validations/mustBeUnique.m +++ b/src/utility/validations/mustBeUnique.m @@ -1,11 +1,11 @@ function mustBeUnique(A) -% mustBeUnique - Validate that there are no repeated values. -% mustBeUnique(A) throws an error if A contains repeated values. +% Validate that there are no repeated values. % -% Class support: -% All classes that define these methods: -% unique -% length +% :code:`mustBeUnique(A)` throws an error if :code:`A` contains repeated values. +% +% Note +% ---- +% Supported all classes that define these methods: :code:`unique`, :code:`length` if length(A) ~= length(unique(A)) throwAsCaller(createValidatorException('validators:mustBeUnique')); diff --git a/src/utility/wigner/Wigner3j.m b/src/utility/wigner/Wigner3j.m index 0a49dda..835e3af 100644 --- a/src/utility/wigner/Wigner3j.m +++ b/src/utility/wigner/Wigner3j.m @@ -1,5 +1,5 @@ function wig = Wigner3j(j1,j2,j3,m1,m2,m3,ifs,ifcb) -% Computes the Wigner $3j$ symbols +% Computes the Wigner :math:`3j` symbols % % .. math:: % @@ -49,14 +49,15 @@ % % ifcb % if exists and is true, switches to computing the Clebsch-Gordan coefficients instead of -% the $3j$-symbols (default :code:`false`) +% the :math:`3j`-symbols (default :code:`false`) % % Returns % ------- % W -% the resulting values of the 3j-symbols (:code:`ifcb=false`) or the Clebsch-Gordan -% coefficients (:code:`ifcb=true`) in either numeric double-precision or symbolic form -% (see the input parameter :code:`ifs`); array of the same size as $j_1$. +% the resulting values of the :math:`3j`-symbols (:code:`ifcb=false`) or the +% Clebsch-Gordan coefficients (:code:`ifcb=true`) in either numeric double-precision or +% symbolic form (see the input parameter :code:`ifs`); array of the same size as +% :math:`j_1`. % % % Notes @@ -71,7 +72,7 @@ % d. all the "numeric" algorithms switch automatically to the symbolic computations % as soon as the numeric overflow occurs, thereby improving the accuracy % but slowing down the computations for some big quantum numbers -% e. in cases with at least one of the $j$ quantum numbers <=2, the explicit accurate +% e. in cases with at least one of the :math:`j` quantum numbers <=2, the explicit accurate % equations are applied in all the algorithms ensuring the highest possible accuracy % f. for relatively small quantum numbers (up to ~20) all the algorithms provide % approximately equivalent results with a reasonably high accuracy diff --git a/src/utility/wigner/Wigner6j.m b/src/utility/wigner/Wigner6j.m index a14d7dc..e02f986 100644 --- a/src/utility/wigner/Wigner6j.m +++ b/src/utility/wigner/Wigner6j.m @@ -1,5 +1,5 @@ function wig = Wigner6j(j1,j2,j3,j4,j5,j6,ifs,ifcb) -% Computes the Wigner $$6j$$ symbols +% Computes the Wigner :math:`6j` symbols % % .. math:: % @@ -48,14 +48,14 @@ % % ifcb % if exists and is true, switches to computing the coupling matrix elements instead of the -% $6j$-symbols (default :code:`false`) +% :math:`6j`-symbols (default :code:`false`) % % Returns % ------- % W -% the resulting values of the 3j-symbols (:code:`ifcb=false`) or the Clebsch-Gordan +% the resulting values of the :math:`6j`-symbols (:code:`ifcb=false`) or the coupling matrix elements % coefficients (:code:`ifcb=true`) in either numeric double-precision or symbolic form -% (see the input parameter :code:`ifs`); array of the same size as $j_1$. +% (see the input parameter :code:`ifs`); array of the same size as :math:`j_1`. % % Notes % ----- @@ -69,7 +69,7 @@ % d. all the "numeric" algorithms switch automatically to the symbolic computations % e. as soon as the numeric overflow occurs, thereby improving the accuracy % but slowing down the computations for some big quantum numbers -% f. in cases with at least one of the $j$ quantum numbers <=2, the explicit accurate +% f. in cases with at least one of the :math:`j` quantum numbers <=2, the explicit accurate % equations are applied in all the algorithms ensuring the highest possible accuracy % g. for relatively small quantum numbers (up to ~20) all the algorithms provide % approximately equivalent results with a reasonably high accuracy diff --git a/test/TestAlgorithms.m b/test/TestAlgorithms.m new file mode 100644 index 0000000..7254f43 --- /dev/null +++ b/test/TestAlgorithms.m @@ -0,0 +1,156 @@ +classdef TestAlgorithms < matlab.unittest.TestCase + % Unit tests for algorithms + + properties (TestParameter) + alg = {... + Vumps('which', 'smallestreal', 'maxiter', 5), ... + IDmrg('which', 'smallestreal', 'maxiter', 5), ... + Vumps2('which', 'smallestreal', 'maxiter', 6), ... + IDmrg2('which', 'smallestreal', 'maxiter', 5) ... + } + symm = {'Z1', 'Z2'} + end + methods (Test) + function test2dIsing(tc, alg, symm) + alg.which = 'largestabs'; + for unitcell = 1:3 + if unitcell == 1 && (isa(alg, 'Vumps2') || isa(alg, 'IDmrg2')) + continue; + end + + E0 = 2.5337 ^ unitcell; + mpo1 = statmech2dIsing('Symmetry', symm); + + if strcmp(symm, 'Z1') + mps1 = initialize_mps(mpo1, CartesianSpace.new(12)); + else + mps1 = initialize_mps(mpo1, GradedSpace.new(Z2(0, 1), [6 6], false)); + end + + mpo = mpo1; + mps = mps1; + for i = 2:unitcell + mpo = [mpo mpo1]; + mps = [mps mps1]; + end + + [gs, lambda] = fixedpoint(alg, mpo, mps); + tc.verifyEqual(expectation_value(gs, mpo, gs), E0, 'RelTol', 1e-2); + + alg_expand = Expand('which', alg.which, 'schmidtcut', 1e-7, 'finalize', Vomps('maxiter', 10)); + gs2 = changebonds(alg_expand, mpo, mps); + tc.verifyGreaterThan(abs(fidelity(gs, gs2)), 0.85^unitcell) + end + end + + function test1dIsing_ordered(tc) + J = 1; + h = 0.5; + e0 = quantum1dIsing_energy(J, h); + d0 = @(k) quantum1dIsing_dispersion(k, 'J', J, 'h', h); + D = 20; + momenta = [0 pi 0.5]; + + for L = 1:3 + %% No symmetry +% H = repmat(quantum1dIsing('h', h, 'J', J), 1, L); +% +% vspace = arrayfun(@(w) CartesianSpace.new(D + w), 1:L, ... +% 'UniformOutput', false); +% gs = initialize_mps(H, vspace{:}); +% +% % Groundstate algorithms +% gs = fixedpoint(Vumps('which', 'smallestreal', 'maxiter', 5, ... +% 'alg_eigs', Arnoldi('maxiter', 100, 'krylovdim', 20)), ... +% H, gs); +% tc.assertEqual(expectation_value(gs, H), e0 * L, 'RelTol', 1e-2); +% +% % Excitation algorithms +% for k = momenta +% qp = InfQP.randnc(gs, gs, k); +% [~, mu] = excitations(QPAnsatz(), H, qp); +% tc.assertEqual(mu, d0(k/L), 'RelTol', 1e-3, ... +% sprintf('qp failed at momentum %.2f', k)); +% end +% + %% Z2 + H = repmat(quantum1dIsing('h', h, 'J', J, 'Symmetry', 'Z2'), 1, L); + + vspace = Z2Space([0, 1], [D D] / 2, false); + gs = initialize_mps(H, vspace); + + % Groundstate algorithms + gs = fixedpoint(Vumps('which', 'smallestreal', 'maxiter', 5), ... + H, gs); + tc.assertEqual(expectation_value(gs, H), e0 * L, 'RelTol', 1e-2); + + % Excitation algorithms + for k = momenta + qp = InfQP.randnc(gs, gs, k, Z2(1)); + [~, mu] = excitations(QPAnsatz(), H, qp); + tc.assertEqual(mu, d0(k / L), 'RelTol', 1e-3, ... + sprintf('qp failed at momentum %.2f', k)); + fprintf('ok for %.2f\n', k); + end +% + %% fZ2 + H = repmat(quantum1dIsing('h', h, 'J', J, 'Symmetry', 'fZ2'), 1, L); + + vspace = fZ2Space([0, 1], [D D] / 2, false); + gs = initialize_mps(H, vspace); + + % Groundstate algorithms + gs = fixedpoint(Vumps('which', 'smallestreal', 'maxiter', 5), ... + H, gs); + tc.assertEqual(expectation_value(gs, H), e0 * L, 'RelTol', 1e-2); + + % Excitation algorithms + for k = momenta + qp = InfQP.randnc(gs, gs, k, fZ2(1)); + [~, mu] = excitations(QPAnsatz(), H, qp); + tc.assertEqual(mu, d0(k / L), 'RelTol', 1e-3, ... + sprintf('qp failed at momentum %.2f', k)); + end + end + end + + function test1dSSH(tc) + load('ssh.mat', 'E0', 'gap', 'H_ssh'); + D = 16; + vspace = fZ2Space([0 1], [D D] / 2, false); + gs = initialize_mps(H_ssh, vspace); + + % Groundstate algorithms + gs = fixedpoint(Vumps('which', 'smallestreal', 'maxiter', 50, 'verbosity', Verbosity.detail), ... + H_ssh, gs); + tc.assertEqual(expectation_value(gs, H_ssh), E0, 'RelTol', 1e-2); + + % Excitation algorithms + k = 0; + qp = InfQP.randnc(gs, gs, k, fZ2(0)); + [~, mu] = excitations(QPAnsatz(), H, qp); + tc.assertEqual(mu, gap, 'RelTol', 1e-3, ... + sprintf('qp failed at momentum %.2f', k)); + end + + function test1dHeisenberg(tc) + alg = Vumps('which', 'smallestreal', 'maxiter', 100); + + mpo = quantum1dHeisenberg('Spin', 1, 'Symmetry', 'SU2'); + vspace1 = GradedSpace.new(SU2(2:2:6), [5 5 1], false); + mps = initialize_mps(mpo, vspace1); + + [gs_mps] = fixedpoint(alg, mpo, mps); + lambda = expectation_value(gs_mps, mpo); + tc.verifyEqual(lambda, -1.401, 'RelTol', 1e-2); + + p = pi; + charge = SU2(3); + qp = InfQP.randnc(gs_mps, gs_mps, p, charge); + + [qp, mu] = excitations(QPAnsatz(), mpo, qp); + tc.verifyEqual(mu, 0.4104, 'AbsTol', 1e-2); + end + end +end + diff --git a/test/TestCharge.m b/test/TestCharge.m index 2dda531..d9ca1ff 100644 --- a/test/TestCharge.m +++ b/test/TestCharge.m @@ -12,11 +12,14 @@ 'fZ2', fZ2([false true]), ... 'Z4', ZN(4, 0:3), ... 'U1', U1(-2:2), ... + 'fU1', fU1(-2:2), ... 'O2', O2([0 0 1 2], [0 1 2 2]), ... 'SU2', SU2(1:4), ... + 'fSU2', fSU2(1:4), ... 'A4', A4(1:4), ... 'SU3', SUN([1 0 0], [2 0 0], [3 0 0]), ... - 'Z2xU1', ProductCharge(Z2(0, 0, 0, 1, 1, 1), U1(-1, 0, 1, -1, 0, 1))... + 'Z2xU1', ProductCharge(Z2(0, 0, 0, 1, 1, 1), U1(-1, 0, 1, -1, 0, 1)), ... + 'U1xSU2', ProductCharge(U1(-1, -1, 0, 0, 1, 1), SU2(1, 2, 1, 2, 1 ,2))... ) end diff --git a/test/TestCtmrg.m b/test/TestCtmrg.m new file mode 100644 index 0000000..40b312f --- /dev/null +++ b/test/TestCtmrg.m @@ -0,0 +1,76 @@ +classdef TestCtmrg < matlab.unittest.TestCase + % Unit tests for infinite matrix product operators. + + properties (TestParameter) + pspace = struct('fermion', GradedSpace.new(fZ2(0, 1), [1 1], false)) + vspace = struct('fermion', GradedSpace.new(fZ2(0, 1), [1 1], false)) + chispace = struct('fermion', GradedSpace.new(fZ2(0, 1), [10 10], false)) + alg = {Ctmrg('tol', 1e-6)} + %other symms TBA + %larger unit cells TBA + end + + methods (Test, ParameterCombination='sequential') + function testEnvironments(tc, pspace, vspace, chispace) + peps = UniformPeps(PepsTensor.randnc(pspace, vspace, vspace)); + envs = CtmrgEnvironment(peps, peps, chispace); + %check matching spaces TBA + end + + function testConvergence(tc, pspace, vspace, chispace, alg) + maxloops = 10; + i = 0; + err = 1; + while err > alg.tol && i < maxloops + i = i+1; + peps = UniformPeps(PepsTensor.randnc(pspace, vspace, vspace)); + envs = CtmrgEnvironment(peps, peps, chispace); + [envs, norm, err] = fixedpoint(alg, peps, peps, envs); + end + assert(i < maxloops, "Convergence seems to fail for simple random PEPS.") + %check left move TBA + %check norm TBA + end + + function testPWave(tc) + %initialize + pspace = GradedSpace.new(fZ2(0, 1), [1 1], false); + vspace = pspace; + chispace = GradedSpace.new(fZ2(0, 1), [10 10], false); + alg = Ctmrg('tol', 1e-6, 'miniter', 1, 'maxiter', 200); + %fill PEPS as PWave from Gaussian + mblocks{1} = [-0.4084 - 0.5139i -0.1033 + 0.1492i -0.7413 - 1.1574i 0.5924 + 0.6552i ... + -0.0660 - 0.1002i -0.1678 - 0.3237i -0.3739 + 0.9139i 0.6497 + 0.8648i]; + mblocks{2} = [ 0.5454 + 0.6080i -0.3003 - 0.2482i 0.3430 - 0.1734i -0.2851 - 0.2658i ... + -0.4272 + 0.5267i 0.4567 + 0.5106i -0.6708 - 0.1306i 0.2184 - 0.1094i]; + A = PepsTensor.zeros(pspace, vspace, vspace); + peps = UniformPeps(PepsTensor(A.var.fill_matrix(mblocks))); + A = peps.A{1}.var; + %run ctmrg + maxloops = 10; + i = 0; + err = 1; + while err > alg.tol && i < maxloops + i = i+1; + envs = CtmrgEnvironment(peps, peps, chispace); + [envs, norm, err] = fixedpoint(alg, peps, peps, envs); + end + assert(i < maxloops, "Convergence seems to fail for PWave example.") + %local density comparison to Gaussian + O = Tensor(pspace, pspace); + O.var.var{2} = 1; + GL = contract(envs.corners{1,1,1}, [1 -4], envs.edges{4,1,1}, [2 -2 -3 1], envs.corners{4,1,1}, [-1 2]); + GR = contract(envs.corners{2,1,1}, [-1 1], envs.edges{2,1,1}, [1 -2 -3 2], envs.corners{3,1,1}, [2 -4]); + n = contract(GL, 1:4, GR, [4,2,3,1]); + GL = GL/sqrt(n); + GR = GR/sqrt(n); + l = contract(envs.edges{3,1,1}, [-1 5 2 1], GL, [1 4 3 8], envs.edges{1,1,1}, [8 9 10 -4], ... + A, [7 4 5 -2 9], conj(A), [6 3 2 -3 10], O, [6 7]); + lw = contract(envs.edges{3,1,1}, [-1 5 2 1], GL, [1 4 3 8], envs.edges{1,1,1}, [8 9 10 -4], ... + A, [6 4 5 -2 9], conj(A), [6 3 2 -3 10]); + rho = contract(l, [2 3 4 1], GR, [1 3 4 2])/contract(lw, [2 3 4 1], GR, [1 3 4 2]); + assert(abs(rho - 0.178787157813086) < sqrt(alg.tol), "CTMRG yields wrong local density for PWave example.") + end + end +end + diff --git a/test/TestFiniteMpo.m b/test/TestFiniteMpo.m new file mode 100644 index 0000000..fa44ed8 --- /dev/null +++ b/test/TestFiniteMpo.m @@ -0,0 +1,108 @@ +classdef TestFiniteMpo < matlab.unittest.TestCase + % Unit tests for finite matrix product operators. + + properties (TestParameter) + mpo = struct(... + 'trivial0', ... + FiniteMpo.randnc(ComplexSpace.new(2, false, 3, false), ComplexSpace.new(4, false)), ... + 'trivial1', ... + FiniteMpo.randnc(ComplexSpace.new(2, false, 4, true, 2, true), ... + ComplexSpace.new(2, true, 3, false)), ... + 'trivial2', ... + FiniteMpo.randnc(ComplexSpace.new(2, false, 4, true, 4, false, 2, true), ... + ComplexSpace.new(2, true, 3, false, 2, false)), ... + 'U1_0', ... + FiniteMpo.randnc(... + GradedSpace.new(U1(0, 1, -1), [1, 1, 1], false, U1(0, 1, -1), [1, 2, 2], false), ... + GradedSpace.new(U1(0, 1, -1), [2, 2, 2], false)), ... + 'U1_1', ... + FiniteMpo.randnc(... + GradedSpace.new(U1(0, 1, -1), [1, 1, 1], false, U1(0, 1, -1), [1, 2, 2], true, U1(0, 1, -1), [1, 1, 1], false), ... + GradedSpace.new(U1(0, 1, -1), [1, 1, 1], true, U1(0, 1, -1), [2, 2, 2], false)), ... + 'U1_2', ... + FiniteMpo.randnc(... + GradedSpace.new(U1(0, 1, -1), [1, 1, 1], false, U1(0, 1, -1), [1, 2, 2], true, U1(0, 1, -1), [1, 2, 2], false, U1(0, 1, -1), [1, 1, 1], false), ... + GradedSpace.new(U1(0, 1, -1), [1, 1, 1], true, U1(0, 1, -1), [2, 2, 2], false, U1(0, 1, -1), [1, 1, 1], true)) ... + ) + + end + + methods (Test) + function testFixedpoints(tc, mpo) + [V, D] = eigsolve(mpo); + tc.assertTrue(isapprox(mpo.apply(V), D * V)); + + v2 = insert_onespace(V); + v3 = apply(mpo, v2); + end + + function testProperties(tc, mpo) + mpo_tensor = Tensor(mpo); + tc.assertTrue(isequal(mpo.domain, mpo_tensor.domain), ... + 'domain should remain fixed after conversion.'); + tc.assertTrue(isequal(mpo.codomain, mpo_tensor.codomain), ... + 'codomain should remain fixed after conversion.'); + + v = initialize_fixedpoint(mpo); + tc.assertTrue(isapprox(mpo.apply(v), mpo_tensor * v)); + end + + function testTransposes(tc, mpo) + tc.assertTrue(isapprox(Tensor(mpo)', Tensor(mpo')), ... + 'ctranspose should not change mpo'); + tc.assertTrue(isequal(domain(mpo'), codomain(mpo))); + tc.assertTrue(isequal(codomain(mpo'), domain(mpo))); + tc.assertTrue(isapprox(Tensor(mpo).', Tensor(mpo.')), ... + 'transpose should not change mpo'); + tc.assertTrue(isequal(domain(mpo.'), codomain(mpo)')); + tc.assertTrue(isequal(codomain(mpo.'), domain(mpo)')); + end + + function test2dIsing(tc) + beta = 0.8 * log(1 + sqrt(2)) / 2; + free_energy_exact = statmech2dIsing_free_energy(beta); + + L = 16; + D = 64; + alg = Dmrg('maxiter', 10, 'which', 'largestabs', 'verbosity', 2); + + mpo = statmech2dIsing('beta', beta, 'L', L); + vspace_max = CartesianSpace.new(D); + mps = initialize_mps(mpo, 'MaxVspace', vspace_max); + + [mps, envs, eta] = fixedpoint(alg, mpo, mps); + + free_energy = - 1 / (beta * (L)) * log(expectation_value(mps, mpo, mps)); + + tc.assertEqual(free_energy, free_energy_exact, 'RelTol', 1e-2); + + mpo = statmech2dIsing('beta', beta, 'L', L, 'Symmetry', 'Z2'); + vspace_max = GradedSpace.new(Z2(0, 1), D ./ [2 2], false); + mps = initialize_mps(mpo, 'MaxVspace', vspace_max); + + [mps, envs, eta] = fixedpoint(alg, mpo, mps); + + free_energy = - 1 / (beta * (L)) * log(expectation_value(mps, mpo, mps)); + + tc.assertEqual(free_energy, free_energy_exact, 'RelTol', 1e-2); + end + + function test1dIsing(tc) + L = 8; + D = 64; + alg = Dmrg('miniter', 2, 'maxiter', 5, 'which', 'smallestreal'); + + mpo = quantum1dIsing('L', L); + vspace_max = CartesianSpace.new(D); + mps = initialize_mps(mpo, 'MaxVspace', vspace_max); + + [mps, envs, eta] = fixedpoint(alg, mpo, mps); + + mpo = quantum1dIsing('L', L, 'Symmetry', 'Z2'); + vspace_max = GradedSpace.new(Z2(0, 1), D ./ [2 2], false); + mps = initialize_mps(mpo, 'MaxVspace', vspace_max); + [mps, envs, eta] = fixedpoint(alg, mpo, mps); + end + end +end + diff --git a/test/TestFiniteMps.m b/test/TestFiniteMps.m new file mode 100644 index 0000000..de3eced --- /dev/null +++ b/test/TestFiniteMps.m @@ -0,0 +1,34 @@ +classdef TestFiniteMps < matlab.unittest.TestCase + % Unit tests for finite matrix product states. + + properties + tol = 1e-10 + end + + methods (Test) + function testFullMps(tc) + L = 12; + P = ComplexSpace.new(2, false); + mps = FiniteMps.new([], P, 'L', L); + + % test conversion + psi = Tensor(mps); + psi2 = Tensor(FiniteMps(psi)); + tc.verifyTrue(isapprox(psi, psi2, 'RelTol', tc.tol)); + + % test norms + tc.verifyEqual(norm(psi), norm(mps), 'RelTol', tc.tol); + tc.verifyEqual(sqrt(abs(overlap(mps, mps))), norm(psi), 'RelTol', tc.tol); + mps_normed = normalize(mps); + tc.verifyEqual(norm(mps_normed), 1, 'RelTol', tc.tol); + tc.verifyEqual(norm(Tensor(mps_normed)), 1, 'RelTol', tc.tol); + + % test moving orthogonality center + for i = 1:length(mps) + tc.verifyEqual(overlap(movegaugecenter(mps_normed, i), mps_normed), 1, ... + 'RelTol', tc.tol); + end + end + end +end + diff --git a/test/TestFusionTree.m b/test/TestFusionTree.m index 94e1b94..48e38c9 100644 --- a/test/TestFusionTree.m +++ b/test/TestFusionTree.m @@ -4,8 +4,8 @@ %#ok<*PROPLC> properties (ClassSetupParameter) - weight = {'small', 'medium'}%, 'large'} - charge = {'A4', 'Z1', 'Z2', 'fZ2', 'U1', 'O2', 'SU2', 'Z2xU1'} + weight = {'small', 'medium'} %, 'large'} + charge = {'A4', 'Z1', 'Z2', 'fZ2', 'U1', 'O2', 'SU2', 'Z2xU1', 'U1xSU2'} end methods (TestClassSetup) @@ -64,6 +64,19 @@ function generateTrees(tc, charge, weight) chargeset = A4(1:4); end + case 'U1xSU2' + switch weight + case 'small' + chargeset = ProductCharge(U1(-1:1), SU2(2,1,2)); + case 'medium' + chargeset = ProductCharge(U1(-2:2), SU2(1,2,1,2,1)); + case 'large' + chargeset = ProductCharge(U1(-2:2), SU2(3,2,1,2,3)); + end + + otherwise + error('not implemented'); + end %% Setup weight @@ -75,7 +88,7 @@ function generateTrees(tc, charge, weight) tc.testWeight = 0.5; case 'medium' legs = 0:4; - maxTrees = 100; + maxTrees = 200; maxLegs = 5; tc.testWeight = 0.25; case 'large' @@ -140,6 +153,8 @@ function trees_properties(tc) end assertTrue(tc, all(isallowed(tc.trees{i,j})), ... 'Generated invalid fusion trees.'); + assertTrue(tc, issorted(tc.trees{i,j}), ... + 'FusionTrees are not properly sorted.'); end end end @@ -220,7 +235,7 @@ function traces(tc) if f.uncoupled(k, j) ~= conj(f.uncoupled(k, j+1)) assertEqual(tc, norm(c1(k, :)), 0); else - a1 = tensortrace(a1_cell{k}, [1:j-1 j j j+2:f.legs+1]); + a1 = tensortrace(a1_cell{k}, j, j+1); a2 = zeros(size(a1)); [~, col, val] = find(c1(k, :)); for l = 1:length(val) diff --git a/test/TestInfJMpo.m b/test/TestInfJMpo.m new file mode 100644 index 0000000..7bf0b5b --- /dev/null +++ b/test/TestInfJMpo.m @@ -0,0 +1,258 @@ +classdef TestInfJMpo < matlab.unittest.TestCase + % Unit tests for infinite matrix product operators. + + properties (TestParameter) + mpo = struct(... + 'trivial', quantum1dIsing(), ... + 'fermion', quantum1d_randomfermion() ... + ) + mps = struct(... + 'trivial', UniformMps.randnc(CartesianSpace.new(2), CartesianSpace.new(4)), ... + 'fermion', UniformMps.randnc(fZ2Space([0 1], [1 1], false), fZ2Space([0 1], [2 2], false)) ... + ) + end + + methods (Test, ParameterCombination='sequential') + function testEnvironments(tc, mpo, mps) +% D = 16; +% mpo = quantum1dIsing('Symmetry', 'Z2'); +% mps = initialize_mps(mpo, GradedSpace.new(Z2(0, 1), D ./ [2 2], false)); + + [GL, GR, lambda] = environments(mpo, mps); + + % left environment + fp_left = repartition(insert_onespace(fixedpoint(mps, 'l_LL'), ... + 2, ~isdual(leftvspace(mpo, 1))), rank(GL{1})); + fp_right = insert_onespace(fixedpoint(mps, 'r_LL'), ... + 2, ~isdual(rightvspace(mpo, 1))); + + TL = transfermatrix(mpo, mps, mps, 'Type', 'LL'); + GL_2 = apply(TL, GL{1}); + lambda2 = overlap(slice(GL_2, length(GL_2.var)), fp_right); % TODO: specify interface to get dimensions of single MpsTensor + GL_2.var(end) = GL_2.var(end) - lambda2 * fp_left; + tc.assertTrue(isapprox(GL_2, GL{1}), ... + 'left environment fixed point equation unfulfilled.'); + tc.assertEqual(lambda, lambda2, 'RelTol', 1e-10); + + % right environment + fp_left = insert_onespace(fixedpoint(mps, 'l_RR'), ... + 2, ~isdual(leftvspace(mpo, 1))); + fp_right = repartition(insert_onespace(fixedpoint(mps, 'r_RR'), ... + 2, ~isdual(rightvspace(mpo, 1))), rank(GR{1})); + + TR = transfermatrix(mpo, mps, mps, 'Type', 'RR').'; + GR_2 = apply(TR, GR{1}); + lambda3 = overlap(slice(GR_2, 1), fp_left); + GR_2.var(1) = GR_2.var(1) - lambda3 * fp_right; + tc.assertTrue(isapprox(GR_2, GR{1}), ... + 'right environment fixed point equation unfulfilled.'); + tc.assertEqual(lambda, lambda3, 'RelTol', 1e-10); + end + + function testQuasiEnvironments(tc) + %% Ising + D = 16; + mpo = quantum1dIsing('Symmetry', 'Z2'); + mps = initialize_mps(mpo, GradedSpace.new(Z2(0, 1), D ./ [2 2], false)); + [GL, GR] = environments(mpo, mps); + + for charge = Z2([1 0]) + for p = [0 pi 0.5] + qp = InfQP.randnc(mps, mps, p, charge); + + % left environments + GBL = leftquasienvironment(mpo, qp, GL, GR); + tc.assertEqual(norm(GBL{1}(1)), 0, 'AbsTol', 1e-10, ... + 'left gauge quasiparticle violation.'); + + T_R = transfermatrix(mpo, qp, qp, 'Type', 'RL'); + T_B = transfermatrix(mpo, qp, qp, 'Type', 'BL'); + + GBL2 = apply(T_R, GBL{1}) + apply(T_B, GL{1}); + + if istrivial(qp) + r = rank(GBL2(end)); + fp_left = repartition(fixedpoint(mpo, qp, 'l_RL_0'), r); + fp_right = fixedpoint(mpo, qp, 'r_RL_1'); + GBL2(end) = GBL2(end) - overlap(GBL2(end), fp_right) * fp_left; + end + + tc.verifyTrue(isapprox(GBL2, GBL{1} * exp(+1i*p)), ... + sprintf('left environment fixed point equation unfulfilled for p=%e, c=%d', p, charge)); + + % right environments + GBR = rightquasienvironment(mpo, qp, GL, GR); + + T_L = transfermatrix(mpo, qp, qp, 'Type', 'LR').'; + T_B = transfermatrix(mpo, qp, qp, 'Type', 'BR').'; + + GBR2 = apply(T_L, GBR{1}) + apply(T_B, GR{1}); + if istrivial(qp) + r = rank(GBR2(1)); + fp_left = fixedpoint(mpo, qp, 'l_LR_1'); + fp_right = repartition(fixedpoint(mpo, qp, 'r_LR_0'), r); + GBR2(1) = GBR2(1) - overlap(GBR2(1), fp_left) * fp_right; + end + tc.assertTrue(isapprox(GBR2, GBR{1} * exp(-1i*p)), ... + sprintf('right environment fixed point equation unfulfilled for p=%e, c=%d', p, charge)); + end + end + end + + function testQuasiEnvironments2(tc) + D = 16; + %% Heisenberg + mpo = quantum1dHeisenberg('Spin', 0.5, 'Symmetry', 'SU2'); + mpo = [mpo mpo]; + vspace = GradedSpace.new(... + SU2(1:2:5), [2 2 1] * (D / 4), false, ... + SU2(2:2:6), [2 2 1] * (D / 4), false); + mps = initialize_mps(mpo, vspace(1), vspace(2)); + alg = Vumps('maxiter', 3, 'which', 'smallestreal'); + mps = fixedpoint(alg, mpo, mps); + [GL, GR] = environments(mpo, mps); + + for charge = SU2(1, 3) + for p = [0 pi 0.5] + qp = InfQP.randnc(mps, mps, p, charge); + + % left environments + GBL = leftquasienvironment(mpo, qp, GL, GR); + T_R = transfermatrix(mpo, qp, qp, 'Type', 'RL'); + T_B = transfermatrix(mpo, qp, qp, 'Type', 'BL'); + + for w = 1:period(mpo) + tc.assertEqual(norm(GBL{w}.var(1)), 0, 'AbsTol', 1e-10, ... + 'left gauge quasiparticle violation.'); + GBL2 = apply(T_R(w), GBL{w}) + apply(T_B(w), GL{w}); + + if istrivial(qp) && w == prev(1, period(mpo)) + r = rank(GBL2); + fp_left = repartition(fixedpoint(mpo, qp, 'l_RL_0', next(w, period(mpo))), r); + fp_right = fixedpoint(mpo, qp, 'r_RL_1', w); + GBL2.var(end) = GBL2.var(end) - overlap(GBL2.var(end), fp_right) * fp_left; + end + + tc.verifyTrue(isapprox(GBL2, GBL{next(w, period(mpo))} * exp(+1i*p/period(mpo))), ... + sprintf('left environment fixed point equation unfulfilled for p=%e, c=%d', p, charge)); + end + + % right environments + GBR = rightquasienvironment(mpo, qp, GL, GR); + T_L = transfermatrix(mpo, qp, qp, 'Type', 'LR').'; + T_B = transfermatrix(mpo, qp, qp, 'Type', 'BR').'; + + for w = 1:period(mpo) + GBR2 = apply(T_L(w), GBR{w}) + apply(T_B(w), GR{w}); + + if istrivial(qp) && w == next(1, period(mpo)) + r = rank(GBR2); + fp_left = fixedpoint(mpo, qp, 'l_LR_1', prev(w, period(mpo))); + fp_right = repartition(fixedpoint(mpo, qp, 'r_LR_0', w), r); + GBR2.var(1) = GBR2.var(1) - overlap(GBR2.var(1), fp_left) * fp_right; + end + + tc.verifyTrue(isapprox(GBR2, GBR{prev(w, period(mpo))} * exp(-1i*p/period(mpo))), ... + sprintf('right environment fixed point equation unfulfilled for p=%e, c=%d', p, charge)); + end + end + end + + end + + function testDerivatives(tc, mpo, mps) + [GL, GR] = environments(mpo, mps, mps); + + H_AC = AC_hamiltonian(mpo, mps, GL, GR); + for i = 1:numel(H_AC) + AC_ = mps.AC{i}; + [AC_.var, lambda] = eigsolve(H_AC{i}, mps.AC{i}.var, 1, 'smallestreal'); + tc.assertTrue(isapprox(apply(H_AC{i}, AC_), lambda * AC_.var)); + end + + H_C = C_hamiltonian(mpo, mps, GL, GR); + for i = 1:numel(H_C) + [C_, lambda] = eigsolve(H_C{i}, mps.C{i}, 1, 'smallestreal'); + tc.assertTrue(isapprox(apply(H_C{i}, C_), lambda * C_)); + end + end + + function test1dIsing(tc) + alg = Vumps('which', 'smallestreal', 'maxiter', 10); + D = 16; + mpo = quantum1dIsing('J', 1, 'h', 1, 'L', Inf); + mps = initialize_mps(mpo, CartesianSpace.new(D)); + [gs, lambda] = fixedpoint(alg, mpo, mps); + tc.verifyTrue(isapprox(lambda, -1.27, 'RelTol', 1e-2)) + + p = 0; + qp = InfQP.randnc(gs, gs, p); + [qp, mu] = excitations(QPAnsatz(), mpo, qp); + tc.verifyEqual(mu, 0.5, 'AbsTol', 1e-8); + + gs2 = [gs gs]; + mpo2 = [mpo mpo]; + gs2 = fixedpoint(alg, mpo2, gs2); + + qp2 = InfQP.randnc(gs2, gs2); + [qp2, mu2] = excitations(QPAnsatz(), mpo2, qp2); + + tc.verifyEqual(mu, mu2, 'AbsTol', 1e-8); + + mpo = quantum1dIsing('J', 1, 'h', 1, 'L', Inf, 'Symmetry', 'Z2'); + mps = initialize_mps(mpo, GradedSpace.new(Z2(0, 1), [D D] ./ 2, false)); + [mps2, lambda2] = fixedpoint(alg, mpo, mps); + tc.verifyTrue(isapprox(lambda2, -1.27, 'RelTol', 1e-2)) + + p = 0; + qp = InfQP.randnc(mps2, mps2, p, Z2(0)); + [qp, mu] = excitations(QPAnsatz(), mpo, qp); + tc.verifyEqual(mu, 0.5, 'AbsTol', 1e-8); + + mpo = [mpo mpo]; + mps2 = [mps2 mps2]; + [mps2, lambda2] = fixedpoint(alg, mpo, mps2); + tc.verifyTrue(isapprox(lambda2/2, -1.27, 'RelTol', 1e-2)) + + p = 0; + qp = InfQP.randnc(mps2, mps2, p, Z2(0)); + [qp, mu] = excitations(QPAnsatz(), mpo, qp); + tc.verifyEqual(mu, 0.5, 'AbsTol', 1e-8); + end + + function test1dHeisenberg(tc) + alg = Vumps('which', 'smallestreal', 'maxiter', 100); + + mpo = quantum1dHeisenberg('Spin', 1, 'Symmetry', 'SU2'); + vspace1 = GradedSpace.new(SU2(2:2:6), [5 5 1], false); + mps = initialize_mps(mpo, vspace1); + + [gs_mps] = fixedpoint(alg, mpo, mps); + lambda = expectation_value(gs_mps, mpo); + tc.verifyEqual(lambda, -1.401, 'RelTol', 1e-2); + + p = pi; + charge = SU2(3); + qp = InfQP.randnc(gs_mps, gs_mps, p, charge); + + [qp, mu] = excitations(QPAnsatz(), mpo, qp); + mu + + mpo = [mpo mpo]; + mps = initialize_mps(mpo, vspace1, vspace1); + [mps2, lambda2] = fixedpoint(alg, mpo, mps); + tc.verifyTrue(isapprox(lambda2/2, -1.401, 'RelTol', 1e-2)) + end + end +end + +function H = quantum1d_randomfermion() + +pspace = fZ2Space([0 1], [1 1], false); +D = Tensor.randnc([pspace pspace], [pspace pspace]); +D = normalize(D + D'); + +H = InfJMpo.twosite(D); + +end + diff --git a/test/TestInfMpo.m b/test/TestInfMpo.m new file mode 100644 index 0000000..fb94490 --- /dev/null +++ b/test/TestInfMpo.m @@ -0,0 +1,185 @@ +classdef TestInfMpo < matlab.unittest.TestCase + % Unit tests for infinite matrix product operators. + + properties (TestParameter) + mpo = struct(... + 'trivial', statmech2dIsing('Symmetry', 'Z1'), ... + 'Z2', statmech2dIsing('Symmetry', 'Z2'), ... + 'fermion', block(InfMpo.fDimer()) ... + ) + mps = struct(... + 'trivial', UniformMps.randnc(CartesianSpace.new(2), CartesianSpace.new(4)), ... + 'Z2', UniformMps.randnc(GradedSpace.new(Z2(0,1), [1 1], false), ... + GradedSpace.new(Z2(0,1), [4 4], false)), ... + 'fermion', UniformMps.randnc(GradedSpace.new(fZ2(0, 1), [1 1], false), ... + GradedSpace.new(fZ2(0, 1), [4 4], false)) ... + ) + end + + methods (Test, ParameterCombination='sequential') + function testEnvironments(tc) + beta = 0.5; + D = 16; + + %% testset 1: environments + mpo = statmech2dIsing('beta', beta, 'Symmetry', 'Z2'); + mps = initialize_mps(mpo, GradedSpace.new(Z2(0, 1), D ./ [2 2], false)); + + [GL, GR, lambda] = environments(mpo, mps); + + % left environment + TL = transfermatrix(mpo, mps, mps, 'Type', 'LL'); + tc.assertTrue(isapprox(apply(TL, GL{1}), lambda * GL{1}), ... + 'left environment fixed point equation unfulfilled.'); + for i = 1:length(GL) + tc.assertTrue(... + isapprox(apply(TL(i), GL{i}), lambda^(1/length(GL)) * GL{next(i, length(GL))}), ... + 'left environment partial fixed point equation unfulfilled.'); + end + + % right environment + TR = transfermatrix(mpo, mps, mps, 'Type', 'RR').'; + tc.assertTrue(isapprox(apply(TR, GR{1}), lambda * GR{1}), ... + 'right environment fixed point equation unfulfilled.'); + for i = length(GR):-1:1 + tc.assertTrue(... + isapprox(apply(TR(i), GR{i}), lambda^(1/length(GR)) * GR{prev(i, length(GR))}), ... + 'right environment partial fixed point equation unfulfilled.'); + end + + % normalization + for i = 1:length(GL) + gl = multiplyright(MpsTensor(GL{i}), mps.C{i}); + gr = multiplyright(MpsTensor(GR{i}), mps.C{i}'); + tc.assertEqual(overlap(gl, gr), 1, 'AbsTol', 1e-10, ... + 'environment normalization incorrect.'); + end + + %% testset 2: quasiparticle environments +% mpo = mpo / lambda; +% [GL, GR, lambda] = environments(mpo, mps, mps, GL, GR); +% tc.assertEqual(lambda, 1, 'AbsTol', 1e-10); +% +% for charge = Z2([0 1]) +% for p = [0 pi 0.5] +% qp = InfQP.randnc(mps, mps, p, charge); +% +% % left environments +% GBL = leftquasienvironment(mpo, qp, GL, GR); +% T_R = transfermatrix(mpo, qp, qp, 'Type', 'RL'); +% T_B = transfermatrix(mpo, qp, qp, 1, 'Type', 'BL'); +% if istrivial(qp) +% C = qp.mpsleft.C1); +% FL_L = insert_onespace(multiplyright(MpsTensor(GL{1}), C), ... +% nspaces(GL{1}) + 1, isdual(auxspace(qp, 1))); +% FR_L = insert_onespace(multiplyright(MpsTensor(GR{1}), C'), ... +% 1, ~isdual(auxspace(qp, 1))); +% +% tc.verifyTrue(isapprox(... +% apply_regularized(T_B, FL_L, FR_L, GL{1}) + ... +% apply_regularized(T_R, FL_L, FR_L, GBL{1}), ... +% exp(1i*p) * GBL{1}), ... +% sprintf('left quasi environment fixed point equation unfulfilled for p=%e, c=%d', p, charge)); +% else +% tc.verifyTrue(isapprox(apply(T_B, GL{1}) + apply(T_R, GBL{1}), ... +% exp(1i*p) * GBL{1}), ... +% sprintf('left quasi environment fixed point equation unfulfilled for p=%e, c=%d', p, charge)); +% end +% +% % right environments +% GBR = rightquasienvironment(mpo, qp, GL, GR); +% T_L = transfermatrix(mpo, qp, qp, 'Type', 'LR').'; +% T_B = transfermatrix(mpo, qp, qp, 'Type', 'BR').'; +% if istrivial(qp) +% C = qp.mpsright.C(1); +% FL_R = insert_onespace(multiplyleft(MpsTensor(GL{1}), C'), ... +% 1, ~isdual(auxspace(qp, 1))); +% FR_R = insert_onespace(multiplyleft(MpsTensor(GR{1}), C), ... +% nspaces(GR{1}) + 1, isdual(auxspace(qp, 1))); +% +% tc.verifyTrue(isapprox(... +% apply_regularized(T_B, FR_R, FL_R, GR{1}) + ... +% apply_regularized(T_L, FR_R, FL_R, GBR{1}), ... +% exp(-1i*p) * GBR{1}), ... +% sprintf('right quasi environment fixed point equation unfulfilled for p=%e, c=%d.', p, charge)); +% else +% tc.verifyTrue(isapprox(apply(T_B, GR{1}) + apply(T_L, GBR{1}), ... +% exp(-1i*p) * GBR{1}), ... +% sprintf('right quasi environment fixed point equation unfulfilled for p=%e, c=%d.', p, charge)); +% end +% end +% end + end + + function testDerivatives(tc, mpo, mps) + [GL, GR] = environments(mpo, mps, mps); + + H_AC = AC_hamiltonian(mpo, mps, GL, GR); + for i = 1:numel(H_AC) + [AC_, lambda] = eigsolve(H_AC{i}, mps.AC{i}, 1, 'largestabs'); + AC_2 = apply(H_AC{i}, AC_); + tc.assertTrue(isapprox(AC_2, lambda * AC_)); + end + + H_C = C_hamiltonian(mpo, mps, GL, GR); + for i = 1:numel(H_C) + [C_, lambda] = eigsolve(H_C{i}, mps.C{i}, 1, 'largestabs'); + tc.assertTrue(isapprox(apply(H_C{i}, C_), lambda * C_)); + end + end + + function test2dIsing(tc) + % compute exact solution + beta = 0.9 * log(1 + sqrt(2)) / 2; + + f = statmech2dIsing_free_energy(beta); + + % compute fixedpoint + D = 16; + alg = Vumps('MaxIter', 10); + mpo = statmech2dIsing('beta', beta, 'Symmetry', 'Z1'); + mps = mpo.initialize_mps(CartesianSpace.new(D)); + [mps2, lambda, GL, GR] = fixedpoint(alg, mpo, mps); + tc.assertEqual(-log(lambda) / beta, f, 'RelTol', 1e-5); + + % compute excitations + p = 0; + qp = InfQP.randnc(mps2, mps2, p); + try + GBL = leftquasienvironment(mpo, qp, GL, GR); + catch + tc.verifyFail('InfMpo.leftquasienvironment broken') + end + + mps = UniformMps.randnc(GradedSpace.new(Z2(0, 1), [1 1], false), ... + GradedSpace.new(Z2(0, 1), [D D] ./ 2, false)); + + mpo = statmech2dIsing('beta', beta, 'Symmetry', 'Z2'); + mps = mpo.initialize_mps(GradedSpace.new(Z2(0, 1), [D D] ./ 2, false)); + [mps2, lambda] = fixedpoint(alg, mpo, mps); + tc.assertEqual(-log(lambda) / beta, f, 'RelTol', 1e-5); + + mps = [mps mps]; + mpo = [mpo mpo]; + + [mps2, lambda] = fixedpoint(alg, mpo, mps); + tc.assertEqual(-log(lambda) / 2 / beta, f, 'RelTol', 1e-5); + + mps = [mps; mps]; + mpo = [mpo; mpo]; + + [mps2, lambda] = fixedpoint(alg, mpo, mps); + tc.assertEqual(-log(lambda)/ 4 / beta, f, 'RelTol', 1e-5); + end + + function test2dfDimer(tc) + D = 32; + mpo = block(InfMpo.fDimer()); + mps = UniformMps.randnc(GradedSpace.new(fZ2(0, 1), [1 1], false), ... + GradedSpace.new(fZ2(0, 1), [D D], false)); + [mps2, lambda] = fixedpoint(Vumps('tol', 1e-4, 'maxiter', 25), mpo, mps); + tc.assertEqual(log(abs(lambda)) / 2, 0.29156, 'RelTol', 1e-4); + end + end +end + diff --git a/test/TestPeps.m b/test/TestPeps.m new file mode 100644 index 0000000..0681661 --- /dev/null +++ b/test/TestPeps.m @@ -0,0 +1,167 @@ +classdef TestPeps < matlab.unittest.TestCase + % Unit tests for PEPS transfer matrices. + + properties (TestParameter) + spaces = struct(... + 'cartesian', CartesianSpace.new([2, 3, 4, 5]), ... + 'complex', ComplexSpace.new(2, false, 3, false, 4, false, 5, false), ... + 'Z2', GradedSpace.new(Z2(0, 1), [1 1], false, Z2(0, 1), [1 2], false, ... + Z2(0, 1), [2 1], false, Z2(0, 1), [3 3], false), ... + 'fZ2', GradedSpace.new(fZ2(0, 1), [1 1], false, fZ2(0, 1), [1 2], false, ... + fZ2(0, 1), [2 1], false, fZ2(0, 1), [5 5], false), ... + 'U1', GradedSpace.new(U1(0, 1, -1), [1 1 1], false, U1(0, 1, -1), [1 2 2], false, ... + U1(0, 1, -1), [2 1 1], false, U1(0, 1, -1), [2 1 1], false), ... + 'SU2', GradedSpace.new(SU2(1, 2), [1 1], false, SU2(2, 3), [2, 1], false, ... + SU2(1, 2), [1, 2], false, SU2(1, 2), [2 1], false) ... + ) + depth = {1} + width = {1, 2} + dualdepth = {false, true} + dualwidth = {false, true} + samebot = {true, false} + end + + methods (Test, ParameterCombination='exhaustive') + + function testFiniteMpo(tc, spaces, depth, width, dualdepth, dualwidth, samebot) + tc.assumeTrue(depth == 1, 'Test ill-defined for multiline PEPS.') + + mpo = tc.random_mpo(spaces, depth, width, dualdepth, dualwidth, samebot); + + % test transpose (co)domain + tc.assertTrue(isequal(domain(mpo.'), codomain(mpo)')); + tc.assertTrue(isequal(codomain(mpo.'), domain(mpo)')); + + % test ctranspose(co)domain + tc.assertTrue(isequal(domain(mpo'), codomain(mpo))); + tc.assertTrue(isequal(codomain(mpo'), domain(mpo))); + + + % test tensor behavior + tc.assumeTrue(width < 2, 'FiniteMpo -> Tensor contraction not reasonable.') + + % test domain and codomain + mpo_tensor = Tensor(mpo); + tc.assertTrue(isequal(mpo.domain, mpo_tensor.domain), ... + 'domain should remain fixed after conversion.'); + tc.assertTrue(isequal(mpo.codomain, mpo_tensor.codomain), ... + 'codomain should remain fixed after conversion.'); + + % test transpose + tc.assertTrue(isapprox(Tensor(mpo).', Tensor(mpo.')), ... + 'transpose should not change mpo'); + + % test ctranspose + tc.assertTrue(isapprox(Tensor(mpo)', Tensor(mpo')), ... + 'ctranspose should not change mpo'); + + % test apply + v = initialize_fixedpoint(mpo); + twistinds = find(isdual(space(v, 1:nspaces(v)))); % compensate for supertrace rules when using contract versus mtimes + tc.assertTrue(isapprox(mpo.apply(v), mpo_tensor * twist(v, twistinds))); + end + + function testFixedpoints(tc, spaces, depth, width, dualdepth, dualwidth, samebot) + mpo = tc.random_mpo(spaces, depth, width, dualdepth, dualwidth, samebot); + + [V, D] = eigsolve(mpo, 'MaxIter', 1e3, 'KrylovDim', 32); + tc.assertTrue(isapprox(mpo.apply(V), D * V)); + + v2 = insert_onespace(V); + v3 = apply(mpo, v2); + end + + function testDerivatives(tc, spaces, depth, width, dualdepth, dualwidth, samebot) + mpo = tc.random_inf_mpo(spaces, depth, width, dualdepth, dualwidth, samebot); + vspaces = repmat({spaces(end)}, 1, width); + mps = mpo.initialize_mps(vspaces{:}); + + [GL, GR] = environments(mpo, mps, mps); + + H_AC = AC_hamiltonian(mpo, mps, GL, GR); + for i = 1:numel(H_AC) + [AC_, lambda] = eigsolve(H_AC{i}, mps.AC{i}, 1, 'largestabs'); + AC_2 = apply(H_AC{i}, AC_); + dist = distance(AC_2, lambda * AC_) / norm(AC_); + if dist > 1e-5 + fprintf('Distance between AC_2 and lambda * AC: %.5e\n', distance(AC_2, lambda * AC_)) + end + tc.assertTrue(isapprox(AC_2, lambda * AC_, 'RelTol', 1e-6)); + end + + H_C = C_hamiltonian(mpo, mps, GL, GR); + for i = 1:numel(H_C) + [C_, lambda] = eigsolve(H_C{i}, mps.C{i}, 1, 'largestabs'); + tc.assertTrue(isapprox(apply(H_C{i}, C_), lambda * C_, 'RelTol', 1e-6)); + end + end + + function testMpoTensor(tc, spaces, dualdepth, dualwidth) + pspace = spaces(1); horzspace = spaces(2); vertspace = spaces(3); + top = TestPeps.random_peps_unitcell(pspace, horzspace, vertspace, 1, 1, dualdepth, dualwidth); + bot = TestPeps.random_peps_unitcell(pspace', vertspace, horzspace, 1, 1, dualdepth, dualwidth); + O = PepsSandwich(top{1}, bot{1}); + t = MpoTensor(O); + end + +end + + methods (Static) + function A = random_peps_unitcell(pspace, horzspace, vertspace, depth, width, dualdepth, dualwidth) + % Spit out random peps unit cell with all kinds of arrows for testing + + if dualwidth, horzspace = horzspace'; end + if dualdepth, vertspace = vertspace'; end + pspaces = repmat(pspace, depth, width); pspaces(2:2:depth*width) = conj(pspaces(2:2:depth*width)); + westspaces = repmat(horzspace, depth, width); + southspaces = repmat(vertspace, depth, width); + + % staggering + if ~mod(width, 2) + westspaces(2:2:depth*width) = conj(westspaces(2:2:depth*width)); + eastspaces = westspaces; + else + eastspaces = conj(westspaces); + end + if ~mod(depth, 2) + southspaces(2:2:depth*width) = conj(southspaces(2:2:depth*width)); + northspaces = southspaces; + else + northspaces = conj(southspaces); + end + + % fill up cell + for d = depth:-1:1, for w = width:-1:1 + A{d, w} = PepsTensor.randnc(pspaces(d, w), ... + westspaces(d, w), southspaces(d, w), eastspaces(d, w), northspaces(d, w)); + end, end + end + + function T = random_sandwich(pspace, horzspace, vertspace, depth, width, dualdepth, dualwidth, samebot) + top = TestPeps.random_peps_unitcell(pspace, horzspace, vertspace, depth, width, dualdepth, dualwidth); + if samebot + bot = cellfun(@conj, top, 'UniformOutput', false); + else + bot = TestPeps.random_peps_unitcell(pspace', horzspace, vertspace, depth, width, dualdepth, dualwidth); + end + T = cellfun(@(t, b) PepsSandwich(t, b), top, bot, 'UniformOutput', false); + end + + function mpo = random_mpo(spaces, depth, width, dualdepth, dualwidth, samebot) + pspace = spaces(1); horzspace = spaces(2); vertspace = spaces(3); vspace = spaces(4); + O = TestPeps.random_sandwich(pspace, horzspace, vertspace, depth, width, dualdepth, dualwidth, samebot); + for d = depth:-1:1 + L = MpsTensor(Tensor.randnc([vspace leftvspace(O{d, 1})'], vspace)); + R = MpsTensor(Tensor.randnc([vspace rightvspace(O{d, end})'], vspace)); + mpo(d, 1) = FiniteMpo(L, O(d, :), R); + end + end + + function mpo = random_inf_mpo(spaces, depth, width, dualdepth, dualwidth, samebot) + pspace = spaces(1); horzspace = spaces(2); vertspace = spaces(3); + O = TestPeps.random_sandwich(pspace, horzspace, vertspace, depth, width, dualdepth, dualwidth, samebot); + mpo = InfMpo(O); + end + end +end + diff --git a/test/TestSolvers.m b/test/TestSolvers.m index 1afb8b1..5483b9a 100644 --- a/test/TestSolvers.m +++ b/test/TestSolvers.m @@ -32,13 +32,13 @@ function linsolve(tc, spaces) [x, flag, relres] = linsolve(A, b, 'Algorithm', alg); tc.assertTrue(isapprox(norm(A * x - b) / norm(b), relres, ... 'AbsTol', tc.tol, 'RelTol', tc.tol)); - tc.assertTrue(flag == 0); + tc.assertTrue(flag == 0 || (flag == 3 && relres < tc.tol)); f = @(x) A * x; [x2, flag, relres] = linsolve(f, b, 'Algorithm', alg); tc.assertTrue(isapprox(norm(f(x2) - b) / norm(b), relres, ... 'AbsTol', tc.tol, 'RelTol', tc.tol)); - tc.assertTrue(flag == 0); + tc.assertTrue(flag == 0 || (flag == 3 && relres < tc.tol)); end end @@ -52,11 +52,11 @@ function eigsolve(tc, spaces) d1 = eigsolve(A, x0, 1, 'IsSymmetric', true); [v, d, flag] = eigsolve(A, x0, 1, 'IsSymmetric', true); tc.verifyTrue(isapprox(d, d1)); - tc.verifyTrue(flag == 0); + tc.verifyTrue(all(flag == 0)); tc.verifyTrue(isapprox(A * v, v * d)); [v, d, flag] = eigsolve(A, x0, 3, 'IsSymmetric', true); - tc.verifyTrue(flag == 0); + tc.verifyTrue(all(flag == 0)); tc.verifyTrue(isapprox(A * v, v * d)); end end diff --git a/test/TestSparseArray.m b/test/TestSparseArray.m new file mode 100644 index 0000000..fbb00c5 --- /dev/null +++ b/test/TestSparseArray.m @@ -0,0 +1,91 @@ +classdef TestSparseArray < matlab.unittest.TestCase + % Unit tests for sparse arrays. + + properties + tol = 1e-12 + end + + methods (Test) + + function test_basics(tc) + a = SparseArray.zeros([2, 2, 2]); + sz1 = size(a(:, :, 1)); + b = SparseArray.zeros([2, 2, 2]); + b(1, 1, 1) = 1; + sz2 = size(b(:, :, 1)); + tc.verifyTrue(isequal(sz1, sz2)); + end + + function test_doctests(tc) + % test examples in docstrings + + % constructor + subs = [1 1 1; 1 1 3; 2 2 2; 4 4 4; 1 1 1; 1 1 1]; + vals = [0.5; 1.5; 2.5; 3.5; 4.5; 5.5]; + sz = [4, 4, 4]; + a = SparseArray(subs, vals, sz); + tc.verifyTrue(isequal(size(a), [4, 4, 4])); + tc.verifyTrue(isequal(a(1, 1, 3), 1.5)); + tc.verifyTrue(isequal(a(4, 4, 4), 3.5)); + tc.verifyTrue(isequal(a(1, 1, 1), 10.5)); + + % groupind + a = SparseArray.random([2, 3, 4, 5, 6], .1); + b = groupind(a, [3, 2]); + tc.verifyTrue(isequal(size(b), [24, 30])); + + % minus + a = SparseArray.random([4 3 2], .1); + b = SparseArray.random([4 3 2], .1); + tc.verifyTrue(isa(a - b, 'SparseArray')); + tc.verifyTrue(isa(a - 5, 'double')); + tc.verifyTrue(isa(a - 0, 'double')); + tc.verifyTrue(isa(a - full(a), 'double')); + + % mrdivide + b = a / 3; + tc.verifyTrue(isa(b, 'SparseArray')); + tc.verifyTrue(isequal(nonzeros(b), nonzeros(a) / 3)); + + % ndims + tc.verifyTrue(isequal(ndims(a), 3)); + + % plus + tc.verifyTrue(isa(a + b, 'SparseArray')); + tc.verifyTrue(isa(a + 5, 'double')); + tc.verifyTrue(isa(a + 0, 'double')); + tc.verifyTrue(isa(a + full(a), 'double')); + + % rdivide + tc.verifyTrue(isa(a ./ 5, 'SparseArray')); + tc.verifyTrue(isa(5 ./ a, 'double')); + tc.verifyTrue(isa(a ./ full(a), 'SparseArray')); + tc.verifyTrue(isa(full(a) ./ a, 'double')); + + % squeeze + c = squeeze(SparseArray.random([2, 1, 3], 0.5)); + tc.verifyTrue(isa(c, 'SparseArray')); + tc.verifyTrue(isequal(size(c), [2, 3])); + d = squeeze(SparseArray([1, 1, 1], 1, [1, 1, 1])); + tc.verifyTrue(isa(d, 'double')); + tc.verifyTrue(isequal(size(d), [1, 1])); + + % subsref + a = SparseArray([4, 4, 4; 2, 2, 1; 2, 3, 2], [3; 5; 1], [4, 4, 4]); + tc.verifyTrue(isequal(a(1, 2, 1), 0)); + tc.verifyTrue(isequal(a(4, 4, 4), 3)); + tc.verifyTrue(isequal(size(a(2, :, :)), [1, 4, 4])); + tc.verifyTrue(isequal(size(a([0, 4, 0])), [4, 1, 4])); + tc.verifyTrue(isequal(size(a([4, 4, 0])), [1, 1, 4])); + + % times + a = SparseArray.random([4 3 2], .1); + b = SparseArray.random([4 3 2], .1); + tc.verifyTrue(isa(a .* b, 'SparseArray')); + tc.verifyTrue(isa(a .* 5, 'SparseArray')); + tc.verifyTrue(isa(a .* 0, 'SparseArray')); + tc.verifyTrue(isa(a .* full(a), 'SparseArray')); + end + + end +end diff --git a/test/TestSparseTensor.m b/test/TestSparseTensor.m new file mode 100644 index 0000000..014ed80 --- /dev/null +++ b/test/TestSparseTensor.m @@ -0,0 +1,75 @@ +classdef TestSparseTensor < matlab.unittest.TestCase + % Unit tests for sparse arrays of tensors. + + properties + tol = 1e-12 + end + + methods (Test) + function basic_linear_algebra(tc) + smallspaces = CartesianSpace.new([2 3 4 5]); + for i = 4:-1:1 + jointspaces(i) = SumSpace(smallspaces(randperm(length(smallspaces), randi([2 3])))); + end + t1 = SparseTensor.randc(jointspaces(1:2), jointspaces(3:4), 'Density', 0.3); + a = randc(); + + tc.verifyTrue(isapprox(norm(t1)^2, dot(t1, t1), ... + 'AbsTol', tc.tol, 'RelTol', tc.tol), 'norm and dot incompatible.'); + + tc.verifyTrue(isapprox(norm(a .* t1), abs(a) * norm(t1), ... + 'AbsTol', tc.tol, 'RelTol', tc.tol), ... + 'norm and scalar multiplication incompatible.'); + tc.verifyTrue(isapprox(t1 + t1, 2 .* t1, ... + 'AbsTol', tc.tol, 'RelTol', tc.tol), ... + '2t and t+t incompatible.'); + + tc.verifyTrue(isapprox(-t1, t1 .* (-1), 'AbsTol', tc.tol), ... + '=t and t * (-1) incompatible.'); + tc.verifyTrue(isapprox(norm(normalize(t1)), 1), ... + 'normalize should result in unit norm.'); + + t2 = t1.randc(t1.codomain, t1.domain, 'Density', 0.3); + b = randc(); + + tc.verifyTrue(isapprox(dot(b .* t2, a .* t1), conj(b) * a * dot(t2, t1), ... + 'AbsTol', tc.tol, 'RelTol', tc.tol) && ... + isapprox(dot(t2, t1), conj(dot(t1, t2)), ... + 'AbsTol', tc.tol, 'RelTol', tc.tol), ... + 'Dot should be sesquilinear.'); + end + + function permute_via_inner(tc) + smallspaces = CartesianSpace.new([2 3 4 5]); + for i = 4:-1:1 + jointspaces(i) = SumSpace(smallspaces(randperm(length(smallspaces), randi([2 3])))); + end + t1 = SparseTensor.randc(jointspaces(1:2), jointspaces(3:4), 'Density', 0.3); + t2 = t1.randc(jointspaces(1:2), jointspaces(3:4), 'Density', 0.3); + + inner = dot(t1, t2); + for i = 0:4 + ps = perms(1:ndims(t1)).'; + for p = ps(:, randperm(size(ps, 2), min(size(ps, 2), 20))) + t3 = tpermute(t1, p.', [i 4-i]); + for j = 1:ndims(t1) + tc.assertTrue(isequal(space(t1, p(j)), space(t3, j)), ... + 'incorrect spaces after permutation.'); + end + tc.assertTrue(... + isapprox(norm(t1), norm(t3), 'AbsTol', tc.tol, 'RelTol', tc.tol), ... + 'Permute should preserve norms.') + + t4 = tpermute(t2, p.', [i 4-i]); + for j = 1:ndims(t1) + tc.assertTrue(isequal(space(t2, p(j)), space(t4, j)), ... + 'incorrect spaces after permutation.'); + end + tc.assertTrue(... + isapprox(dot(t3, t4), inner, 'AbsTol', tc.tol, 'RelTol', tc.tol), ... + 'Permute should preserve inner products.'); + end + end + end + end +end diff --git a/test/TestTensor.m b/test/TestTensor.m index 245716c..3e545a8 100644 --- a/test/TestTensor.m +++ b/test/TestTensor.m @@ -8,7 +8,7 @@ methods (TestClassSetup) function classSetup(tc) orig = Options.CacheEnabled; - Options.CacheEnabled(false); + Options.CacheEnabled(true); tc.addTeardown(@Options.CacheEnabled, orig); end end @@ -26,12 +26,26 @@ function classSetup(tc) U1(0, 1, -1), [1 3 3], true), ... 'SU2', GradedSpace.new(SU2(1, 2), [3 1], false, SU2(1, 3), [2 1], false, ... SU2(2, 3), [1 1], true, SU2(1, 2), [2 2], false, SU2(1, 2, 4), [1 1 1], true), ... - 'A4', GradedSpace.new(A4(1:2), [2 1], false, A4([1 4]), [1 2], false, ... - A4(1:4), [2 1 3 1], true, A4(1:4), [2 2 1 2], false, A4(2:3), [1 2], true) ... + 'A4', GradedSpace.new(A4(1:2), [2 1], false, A4(1:4), [2 1 2 1], false, ... + A4([1 4]), [1 2], true, A4(1:4), [2 2 1 1], false, A4(2:4), [1 2 2], true), ... + 'U1xSU2', GradedSpace.new(... + ProductCharge(U1(-1:1), SU2(2,1,2)), [1 2 1], false, ... + ProductCharge(U1(-2:2), SU2(1,2,1,2,1)), [2 1 2 2 2], true, ... + ProductCharge(U1(-1:1), SU2(2,1,2)), [1 2 2], false, ... + ProductCharge(U1(-1:1), SU2(2,1,2)), [1 2 1], false, ... + ProductCharge(U1(-1:1), SU2(2,1,2)), [1 2 2], false), ... + 'Hubbard', GradedSpace.new(... + ProductCharge(U1(0, 1, 2), SU2(1, 2, 1), fZ2(0, 1, 0)), [1 1 1], false, ... + ProductCharge(U1(-3:1), SU2(2, 1, 2, 1, 2), fZ2(0, 1, 0, 1, 0)), [1 1 3 1 1], false, ... + ProductCharge(U1(0, 1), SU2(1, 2), fZ2(0, 1)), [1 1], true, ... + ProductCharge(U1(0, 1, 2), SU2(1, 2, 1), fZ2(0, 1, 0)), [1 1 1], false, ... + ProductCharge(U1(0, 1), SU2(1, 2), fZ2(1, 0)), [1 1], true) ... ) end + methods (Test) + %% General properties function basic_linear_algebra(tc, spaces) t1 = Tensor.rand(spaces(1:3), spaces(4:5)); @@ -64,6 +78,20 @@ function basic_linear_algebra(tc, spaces) 'Dot should be sesquilinear.'); end + function transpose_via_conversion(tc, spaces) + tc.assumeTrue(istwistless(braidingstyle(spaces))); + + t = Tensor.ones(spaces(1:3), spaces(4:5)); + tdagger = t'; + tc.assertTrue(isequal(t.domain, tdagger.codomain)); + tc.assertTrue(isequal(t.codomain, tdagger.domain)); + + tc.assertEqual(flip(dims(tdagger)), dims(t)); + tc.assertEqual(conj(double(t)), double(conj(t)), ... + 'AbsTol', tc.tol, 'RelTol', tc.tol, ... + sprintf('conj(double(t)) should be double(conj(t)). (%e)', distance(conj(double(t)), double(conj(t))))); + end + function matrix_functions(tc, spaces) for i = 1:3 t = Tensor.randnc(spaces(1:i), spaces(1:i)); @@ -90,6 +118,7 @@ function matrix_functions(tc, spaces) end + %% Contractions function permute_via_inner(tc, spaces) rng(213); t1 = Tensor.rand(spaces, []); @@ -99,15 +128,15 @@ function permute_via_inner(tc, spaces) for i = 0:5 ps = perms(1:nspaces(t1)).'; - for p = ps(:, randperm(size(ps, 2), min(size(ps, 2), 20))) - t3 = permute(t1, p.', [i 5-i]); + for p = ps(:, randperm(size(ps, 2), min(size(ps, 2), 5))) + t3 = tpermute(t1, p.', [i 5-i]); tc.assertTrue(all(dims(t1, p.') == dims(t3)), ... 'Incorrect size after permutation.'); tc.assertTrue(... isapprox(norm(t1), norm(t3), 'AbsTol', tc.tol, 'RelTol', tc.tol), ... 'Permute should preserve norms.') - t4 = permute(t2, p.', [i 5-i]); + t4 = tpermute(t2, p.', [i 5-i]); tc.assertTrue(all(dims(t2, p.') == dims(t4)), ... 'Incorrect size after permutation.'); tc.assertTrue(... @@ -125,8 +154,8 @@ function permute_via_conversion(tc, spaces) tc.assertTrue(all(dims(t) == size(a, 1:nspaces(t)))); for k = 0:nspaces(t) ps = perms(1:nspaces(t)).'; - for p = ps(:, randperm(size(ps, 2), min(size(ps, 2), 10))) - t2 = permute(t, p.', [k nspaces(t)-k]); + for p = ps(:, randperm(size(ps, 2), min(size(ps, 2), 5))) + t2 = tpermute(t, p.', [k nspaces(t)-k]); a2 = double(t2); tc.assertTrue(all(dims(t2) == size(a2, 1:nspaces(t)))); tc.assertTrue(all(dims(t2) == size(a, p.'))); @@ -138,6 +167,7 @@ function permute_via_conversion(tc, spaces) end function multiplication_via_conversion(tc, spaces) + tc.assumeTrue(istwistless(braidingstyle(spaces))); t1 = Tensor.randnc(spaces(1), spaces(2)); t2 = Tensor.randnc(spaces(2), spaces(3)); @@ -150,6 +180,10 @@ function multiplication_via_conversion(tc, spaces) tc.assertTrue(isapprox(double(t1 * t2), ... tensorprod(double(t1), double(t2), [2 3], [2 1], 'NumDimensionsA', 3))); + l1 = contract(t1, [1 2 3], conj(t1), [1 2 3]); + l2 = contract(double(t1), [1 2 3], conj(double(t1)), [1 2 3]); + tc.assertTrue(isapprox(l1, l2)); + W1 = spaces(1:3); W2 = spaces(4:5); @@ -166,14 +200,61 @@ function multiplication_via_conversion(tc, spaces) end function tensorprod_via_conversion(tc, spaces) + tc.assumeTrue(istwistless(braidingstyle(spaces))); t1 = Tensor.randnc([], spaces(1:2)); t2 = Tensor.randnc(spaces(1:2), []); tc.assertTrue(isapprox(tensorprod(t1, t2, [1 2], [2 1]), ... tensorprod(double(t1), double(t2), [1 2], [2 1]), ... 'AbsTol', tc.tol, 'RelTol', tc.tol)); + + A = Tensor.randnc(spaces(1), spaces(1:2)); + C = Tensor.randnc(spaces(1), spaces(1)); + AC = contract(A, [-1 -2 1], C, [1 -3]); + + tc.assertTrue(isapprox(double(AC), contract(double(A), [-1 -2 1], double(C), [1 -3]))); + end + + function tensortrace(tc, spaces) + t1 = Tensor.randnc(spaces(1:3), spaces(1:3)); + t2 = contract(t1, [-1 -2 1 1 -3 -4], 'Rank', [2 2]); + t3 = contract(t2, [-1 1 1 -2], 'Rank', [1 1]); + t4 = contract(t1, [-1 1 2 2 1 -2], 'Rank', [1 1]); + tc.assertTrue(isapprox(t3, t4, 'AbsTol', tc.tol, 'RelTol', tc.tol)); + + % issue with fermionic traces: + Nl = Tensor.randnc(spaces(1), spaces(1:2)); + Nr = Tensor.randnc(spaces(1:2), spaces(1)); + result1 = contract(Nl, [-1 1 2], Nr, [2 1 -2]); + result2 = contract(contract(Nl, [-1 1 -4], Nr, [-2 1 -3]), [-1 1 -2 1]); + tc.assertTrue(isapprox(result1, result2, 'AbsTol', tc.tol, 'RelTol', tc.tol)); + + t5 = contract(t1, [1 2 3 3 2 1]); + t6 = contract(t4, [1 1]); + tc.assertTrue(isapprox(t5, t6, 'AbsTol', tc.tol, 'RelTol', tc.tol)); + if istwistless(braidingstyle(spaces)) + t7 = contract(double(t1), [1 2 3 3 2 1]); + tc.assertTrue(isapprox(t6, t7, 'AbsTol', tc.tol, 'RelTol', tc.tol)); + end + end + + function contract_order(tc, spaces) + A = Tensor.randnc(spaces(1:2), spaces(1)'); + r = Tensor.randnc(spaces(1)', spaces(1)'); + + args = {A, r, conj(A); [-1 2 1], [1 3], [-2 2 3]}; + + r1 = contract(args{:}, 'Rank', rank(r)); + for p = perms(1:3)' + args2 = args(:, p); + r2 = contract(args2{:}, 'Rank', rank(r)); + tc.assertTrue(isapprox(r1, r2), ... + 'Contraction order should leave result invariant.'); + end end + + %% Factorizations function orthogonalize(tc, spaces) t = Tensor.randnc(spaces, []); @@ -182,11 +263,11 @@ function orthogonalize(tc, spaces) p2 = [1 5]; tc.assumeTrue(spaces(3) * spaces(4) * spaces(2) >= spaces(1)' * spaces(5)') - for alg = ["qr", "qrpos", "polar", "svd" "ql" "qlpos"] + for alg = ["qr", "qrpos", "polar", "svd", "ql", "qlpos"] [Q, R] = leftorth(t, p1, p2, alg); assertTrue(tc, ... - isapprox(Q * R, permute(t, [p1 p2], [length(p1) length(p2)]), ... + isapprox(Q * R, tpermute(t, [p1 p2], [length(p1) length(p2)]), ... 'AbsTol', tc.tol, 'RelTol', tc.tol), ... sprintf('Q and R not a valid %s factorization.', alg)); @@ -212,11 +293,11 @@ function orthogonalize(tc, spaces) tc.assumeTrue(spaces(3) * spaces(4) <= spaces(1)' * spaces(2)' * spaces(5)'); p1 = [3 4]; p2 = [2 1 5]; - for alg = ["lq", "lqpos", "polar", "svd" "rq" "rqpos"] + for alg = ["lq", "lqpos", "polar", "svd", "rq", "rqpos"] [L, Q] = rightorth(t, p1, p2, alg); assertTrue(tc, ... - isapprox(L * Q, permute(t, [p1 p2], [length(p1) length(p2)]), ... + isapprox(L * Q, tpermute(t, [p1 p2], [length(p1) length(p2)]), ... 'AbsTol', tc.tol, 'RelTol', tc.tol), ... sprintf('Q and R not a valid %s factorization.', alg)); @@ -240,12 +321,16 @@ function orthogonalize(tc, spaces) function nullspace(tc, spaces) t = Tensor.randnc(spaces, []); - + tc.assumeTrue(spaces(3) * spaces(4) * spaces(2) >= spaces(1)' * spaces(5)', ... + 'tensor not full rank') %% Left nullspace for alg = ["qr", "svd"] N = leftnull(t, [3 4 2], [1 5], alg); - - assertTrue(tc, norm(N' * permute(t, [3 4 2 1 5], [3 2])) < ... + dimN = dims(N, nspaces(N)); + dimW = prod(dims(t, [3 4 2])); + dimV = prod(dims(t, [1 5])); + tc.assertEqual(dimW, dimN + dimV, 'Nullspace should be full rank'); + assertTrue(tc, norm(N' * tpermute(t, [3 4 2 1 5], [3 2])) < ... 100 * eps(norm(t)), ... 'N should be a left nullspace.'); assertTrue(tc, isisometry(N, 'left', ... @@ -255,9 +340,15 @@ function nullspace(tc, spaces) %% Right nullspace + tc.assumeTrue(spaces(3) * spaces(4) <= spaces(1)' * spaces(2)' * spaces(5)', ... + 'tensor not full rank'); for alg = ["lq", "svd"] N = rightnull(t, [3 4], [2 1 5], alg); - assertTrue(tc, norm(permute(t, [3 4 2 1 5], [2 3]) * N') < ... + dimN = dims(N, 1); + dimW = prod(dims(t, [3 4])); + dimV = prod(dims(t, [2 1 5])); + tc.assertEqual(dimV, dimW + dimN, 'Nullspace should be full rank'); + assertTrue(tc, norm(tpermute(t, [3 4 2 1 5], [2 3]) * N') < ... 100 * eps(norm(t)), ... 'N should be a right nullspace.'); assertTrue(tc, isisometry(N, 'right', ... @@ -267,9 +358,9 @@ function nullspace(tc, spaces) end function singularvalues(tc, spaces) - t = Tensor.randnc(spaces, []); + t = normalize(Tensor.randc(spaces, [])); [U, S, V] = tsvd(t, [3 4 2], [1 5]); - assertTrue(tc, isapprox(permute(t, [3 4 2 1 5], [3 2]), U * S * V), ... + assertTrue(tc, isapprox(tpermute(t, [3 4 2 1 5], [3 2]), U * S * V), ... 'USV should be a factorization.'); assertTrue(tc, isisometry(U), ... 'U should be an isometry.'); @@ -277,6 +368,32 @@ function singularvalues(tc, spaces) 'V should be an isometry.'); %% truncation + d = max(cellfun(@(x) min(size(x, 1), size(x, 2)), matrixblocks(S))); + [Utrunc, Strunc, Vtrunc, eta] = tsvd(t, [3 4 2], [1 5], 'TruncDim', d-1); + assertTrue(tc, isapprox(norm(tpermute(t, [3 4 2 1 5], [3 2]) - ... + Utrunc * Strunc * Vtrunc), eta, 'AbsTol', 1e-10, 'RelTol', 1e-6)); + assertTrue(tc, isisometry(U, 'left')); + assertTrue(tc, isisometry(V, 'right')); + d2 = max(cellfun(@(x) max(size(x, 1), size(x, 2)), matrixblocks(Strunc))); + assertTrue(tc, d2 <= ceil(0.95*d)); + + d = min(dims(S, 1:2)); + [Utrunc, Strunc, Vtrunc, eta] = tsvd(t, [3 4 2], [1 5], 'TruncTotalDim', ceil(0.9*d)); + assertTrue(tc, isapprox(norm(tpermute(t, [3 4 2 1 5], [3 2]) - ... + Utrunc * Strunc * Vtrunc), eta, 'AbsTol', 1e-10, 'RelTol', 1e-6)); + assertTrue(tc, isisometry(U, 'left')); + assertTrue(tc, isisometry(V, 'right')); + d2 = max(dims(Strunc, 1:2)); + assertTrue(tc, d2 <= ceil(0.9*d)); + + s = min(cellfun(@(x) min(diag(x), [], 'all'), matrixblocks(S))); + [Utrunc, Strunc, Vtrunc, eta] = tsvd(t, [3 4 2], [1 5], 'TruncBelow', s * 1.2); + assertTrue(tc, isapprox(norm(tpermute(t, [3 4 2 1 5], [3 2]) - ... + Utrunc * Strunc * Vtrunc), eta, 'AbsTol', 1e-10, 'RelTol', 1e-6)); + assertTrue(tc, isisometry(U, 'left')); + assertTrue(tc, isisometry(V, 'right')); + s2 = min(cellfun(@(x) min(diag(x)), matrixblocks(Strunc))); + assertTrue(tc, s * 1.2 <= s2); end @@ -290,5 +407,5 @@ function eigenvalues(tc, spaces) end end end + end - diff --git a/test/TestUniformMps.m b/test/TestUniformMps.m new file mode 100644 index 0000000..a7cdef9 --- /dev/null +++ b/test/TestUniformMps.m @@ -0,0 +1,97 @@ +classdef TestUniformMps < matlab.unittest.TestCase + % Unit tests for uniform matrix product states. + + properties (TestParameter) + A = struct(... + 'trivial', {{MpsTensor.randnc(CartesianSpace.new([4 2]), CartesianSpace.new(4))}}, ... + 'trivial2', {{MpsTensor.randnc(CartesianSpace.new([4 2]), CartesianSpace.new(5)), ... + MpsTensor.randnc(CartesianSpace.new([5 2]), CartesianSpace.new(4))}}, ... + 'trivial3', {{MpsTensor.randnc(CartesianSpace.new([4 2]), CartesianSpace.new(5)), ... + MpsTensor.randnc(CartesianSpace.new([5 2]), CartesianSpace.new(6)), ... + MpsTensor.randnc(CartesianSpace.new([6 2]), CartesianSpace.new(4))}}, ... + 'fermion1', {{MpsTensor.randnc(... + GradedSpace.new(fZ2(0,1), [2 2], false, fZ2(0,1), [1 1], false), ... + GradedSpace.new(fZ2(0,1), [2 2], false))}}, ... + 'fermion2', {{MpsTensor.randnc(... + GradedSpace.new(fZ2(0,1), [2 2], true, fZ2(0,1), [1 1], false), ... + GradedSpace.new(fZ2(0,1), [2 2], true))}}, ... + 'fermion3', {{MpsTensor.randnc(... + GradedSpace.new(fZ2(0,1), [2 2], false, fZ2(0,1), [1 1], true), ... + GradedSpace.new(fZ2(0,1), [2 2], false))}}, ... + 'fermion4', {{MpsTensor.randnc(... + GradedSpace.new(fZ2(0,1), [2 2], true, fZ2(0,1), [1 1], true), ... + GradedSpace.new(fZ2(0,1), [2 2], true))}}, ... + 'haldane', {{MpsTensor.randnc(GradedSpace.new(SU2(1:2:5), [5 3 2], false, SU2(2), 1, false), ... + GradedSpace.new(SU2(2:2:6), [5 2 1], false)), ... + MpsTensor.randnc(GradedSpace.new(SU2(2:2:6), [5 2 1], false, SU2(2), 1, false), ... + GradedSpace.new(SU2(1:2:5), [5 3 2], false))}} ... + ) + end + + methods (Test) + function testCanonical(tc, A) + mps = UniformMps(A); + + for i = 1:length(A) + tc.assertTrue(... + isequal(space(A{i}), space(mps.AL{i}), ... + space(mps.AR{i}), space(mps.AC{i}))); + end + + T = cellfun(@transfermatrix, A, mps.AL); + [v, d] = eigsolve(T, [], 1, 'largestabs'); + T2 = cellfun(@transfermatrix, A, A); + [v2, d2] = eigsolve(T2, [], 1, 'largestabs'); + tc.verifyTrue(isapprox(d^2, d2), 'canonical changed state?'); + + mps = canonicalize(mps); + AL = mps.AL; AR = mps.AR; C = mps.C; AC = mps.AC; + tc.assertTrue(all(cellfun(@(x) isisometry(x, 'left'), AL)), ... + 'AL should be a left isometry.'); + tc.assertTrue(all(cellfun(@(x) isisometry(x, 'right'), AR)), ... + 'AR should be a right isometry.'); + + for w = 1:period(mps) + ALC = multiplyright(AL{w}, C{w}); + CAR = repartition(multiplyleft(AR{w}, C{prev(w, period(mps))}), rank(AC{w})); + tc.assertTrue(isapprox(ALC, AC{w}) && isapprox(AC{w}, CAR), ... + 'AL, AR, C and AC should be properly related.'); + end + end + + function testDiagonalC(tc, A) + mps = UniformMps(A); + mps2 = diagonalizeC(mps); + f = fidelity(mps, mps2); + tc.assertTrue(isapprox(f, 1), 'Diagonalizing C should not alter the state.'); + end + + function testFixedpoints(tc, A) + mps = UniformMps(A); + for top = ["L" "R"] + for bot = ["L" "R"] + T = transfermatrix(mps, mps, 'Type', sprintf('%c%c', top, bot)); + rhoL = fixedpoint(mps, sprintf('l_%c%c', top, bot)); + rhoR = fixedpoint(mps, sprintf('r_%c%c', top, bot)); + tc.verifyTrue(isapprox(rhoL, T.apply(rhoL)), ... + sprintf('rho_left should be a %c%c fixed point.', top, bot)); + tc.verifyTrue(isapprox(rhoR, apply(T', rhoR)), ... + sprintf('rho_right should be a %c%c fixed point.', top, bot)); + end + end + end + + function testTransferEigs(tc, A) + mps = UniformMps(A); + + [V, D] = transfereigs(mps, mps, 1, 'largestabs'); + [~, charges] = matrixblocks(V); + [V2, D2] = transfereigs(mps, mps, 1, 'largestabs', 'Charge', one(charges)); + + xi = correlation_length(mps); + [epsilon, delta] = marek_gap(mps); + tc.verifyEqual(xi, 1/epsilon, 'AbsTol', 1e-12); + end + end +end + diff --git a/test/TestUtility.m b/test/TestUtility.m new file mode 100644 index 0000000..b44b52b --- /dev/null +++ b/test/TestUtility.m @@ -0,0 +1,44 @@ +classdef TestUtility < matlab.unittest.TestCase + % Unit tests for utility functions. + + properties + Property1 + end + + methods (Test) + function testNetcon(tc) + network1 = {[-1, 1, 2, 3] [2, 4, 5, 6] [1, 5, 7, -3] [3, 8, 4, 9] ... + [6, 9, 7, 10] [-2, 8, 11, 12] [10, 11, 12, -4]}; + [~, cost] = netcon(network1, 0, 2, 1, 1); + tc.verifyEqual(cost, [0, 0, 0, 0, 0, 0, 2, 2, 2]); + + network2 = {[1, 4, 5, 6, 7, 8] [2, 9, 10, 11, 12, 13] ... + [3, 14, 15, 16, 17, 18] [-6, 9, 23, 24, 25, 26] [-5, 5, 19, 20, 21, 22] ... + [8, 14, 27, 28, 29, 30] [12, 15, 31, 32, 33, 34] ... + [22, 25, 28, 31, 35, 36, 37, 38] [-4, 20, 23, 35, 39, 40, 41, 42] ... + [42, 36, 37, 38, 43, 44, 45, 46] [41, 24, 44, 26, 47, 48] ... + [19, 40, 21, 43, 49, 50] [27, 45, 29, 30, 51, 52] ... + [46, 32, 33, 34, 53, 54] [-2, -3, 39, 49, 47, 55] [4, 50, 6, 7, 51, 56] ... + [48, 10, 11, 53, 13, 57] [52, 54, 16, 17, 18, 58] ... + [55, 56, 57, 58, -1, 1, 2, 3]}; + [~, cost] = netcon(network2, 0, 2, 1, 1); + tc.verifyEqual(cost, [0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 0, 1, 1, 3, 0, 3]); + + % passes but fairly expensive: +% network3 = {[64, 72, 73, 19, 22] [65, 74, 75, 23, 76] [66, 77, 20, 79, 28] ... +% [67, 21, 24, 29, 34] [68, 25, 78, 35, 80] [69, 81, 30, 83, 84] ... +% [70, 31, 36, 85, 86] [71, 37, 82, 87, 88] [-5, 19, 20, 21, 1, 2, 4, 5] ... +% [22, 23, 24, 25, 3, 26, 6, 27] [28, 29, 30, 31, 7, 8, 32, 33] ... +% [34, 35, 36, 37, 9, 38, 39, 40] 1:18 ... +% [10, 11, 13, 14, 41, 42, 43, 44] [12, 26, 15, 27, 47, 48, 49, 50] ... +% [16, 17, 32, 33, 53, 54, 55, 56] [18, 38, 39, 40, 60, 61, 62, 63] ... +% [-2, -3, -4, 41, 89] [72, 73, 42, 47, 90] [74, 75, 48, 76, 45] ... +% [77, 43, 79, 53, 46] [44, 49, 54, 60, 51] [50, 78, 61, 80, 52] ... +% [81, 55, 83, 84, 57] [56, 62, 85, 86, 58] [63, 82, 87, 88, 59] ... +% [89, 90, 45, 46, 51, 52, 57, 58, 59, -1, 64, 65, 66, 67, 68, 69, 70, 71]}; +% [~, cost] = netcon(network3, 0, 2, 1, 1); +% tc.verifyEqual(cost, [zeros(1, 7) 4 4 0 0 0 1 1 1 0 1 0 0 0 3 0 3 2 0 2 4]); + end + end +end + diff --git a/test/ssh.mat b/test/ssh.mat new file mode 100644 index 0000000..d4c23be Binary files /dev/null and b/test/ssh.mat differ diff --git a/test/testExcitations.m b/test/testExcitations.m new file mode 100644 index 0000000..2d4fd7c --- /dev/null +++ b/test/testExcitations.m @@ -0,0 +1,33 @@ +%% Groundstate search +alg = Vumps('which', 'smallestreal', 'maxiter', 100); + +symmetry = 'U1'; + +mpo = quantum1dHeisenberg('Spin', 1, 'Symmetry', symmetry); + +switch symmetry + case 'SU2' + vspace = GradedSpace.new(SU2(2:2:6), [5 5 1], false); + case 'U1' + vspace = GradedSpace.new(U1(-3:2:3), [2 5 5 2], false); + otherwise + error('invalid symmetry'); +end + +mps = initialize_mps(mpo, vspace); + +if ~exist('gs_mps', 'var') + [gs_mps] = fixedpoint(alg, mpo, mps); +end + +lambda = expectation_value(gs_mps, mpo); +assert(isapprox(lambda, -1.401, 'RelTol', 1e-2)); + +%% Excitations +p = pi; +charge = U1(0); +qp = InfQP.randnc(gs_mps, gs_mps, p, charge); +tic; +[qp, mu] = excitations(QPAnsatz(), mpo, qp); +toc; +mu