+
+
+
+
+
+## About The Project
+
+
+This is a package which aims to efficiently implement the various elementary algorithms that arise in the context of tensor networks. Currently, this includes:
+
+* Various basic and utility tensor operations, such as creation routines, various linear algebra routines (norms, traces, overlaps, ...), index permutations, ...
+* Tensor contraction routines, both pairwise as well as through a network contraction routine.
+* Tensor factorizations, such as QR, LQ and polar decompositions, eigendecompositions and singular value decompositions.
+* Solver algorithms for eigen systems and linear systems.
+
+Additionally, these tensors support a general global symmetries, in which case both memory and CPU usage are optimized. The framework is able to support both Abelian and non-Abelian symmetries, as well as symmetry groups with multiplicities, which can have bosonic or fermionic braiding rules.
+The design of the algorithms is chosen such that the inclusion of symmetries should not alter the code after the creation of the tensors. Currently, the following symmetries are implemented:
+
+* Z2
+* U1
+* SU2
+* O2
+* Direct product groups
+
+
+
+
+
+## Getting Started
+
+### Requirements
+
+This project depends on the following:
+- MATLAB version R2020b or newer
+- [Parallel Computing Toolbox](https://de.mathworks.com/products/parallel-computing.html)
+- A C++ compiler compatible with your MATLAB version for MEX-file compilation.
+
+### Installation
+1. Clone the repo into a local folder.
+ ```sh
+ git clone https://github.com/quantumghent/TensorTrack.git mylocalfolder
+ ```
+
+2. Add the folder and subfolders to your MATLAB path.
+ - Via the MATLAB UI:
+ Home > Environment > Set Path > Add with Subfolders > mylocalfolder/src
+ - Via the MATLAB Command Window:
+ ```matlabsession
+ addpath(genpath('mylocalfolder/src'))
+ ```
+ By default, the path is reset every time you close the application. You can permanently add this package to the path by calling ```savepath```.
+
+3. Precompile the necessary mex files.
+ Within the MATLAB Command Window, call:
+ ```matlabsession
+ GetMD5
+ uninit
+ ```
+
+
+
+
+
+## Contributing
+
+Contributions as well as feature requests are greatly appreciated.
+
+If you have a suggestion that would make this project better, please do not hesitate to open an issue with the tag "enhancement". Alternatively, you could also fork the repo and create a pull request:
+
+1. Fork the Project
+2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`)
+3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
+4. Push to the Branch (`git push origin feature/AmazingFeature`)
+5. Open a Pull Request
+
+Don't forget to give the project a star! Thanks again!
+
+
+
+
+
+## Acknowledgments
+This project has been made possible through the work of the following people:
+* [Lukas Devos](https://orcid.org/0000-0002-0256-4200)
+* [Lander Burgelman](https://orcid.org/0000-0003-1724-5330)
+* [Bram Vanhecke](https://orcid.org/0000-0001-9557-1591)
+* [Jutho Haegeman](https://orcid.org/0000-0002-0858-291X)
+* [Frank Verstraete](https://orcid.org/0000-0003-0270-5592)
+* [Laurens Vanderstraeten](https://orcid.org/0000-0002-3227-9822)
+* ...
+
+
+
+
+{% endblock %}
+
diff --git a/docs/src/conf.py b/docs/src/conf.py
new file mode 100644
index 0000000..aefa38f
--- /dev/null
+++ b/docs/src/conf.py
@@ -0,0 +1,273 @@
+import sys
+import os
+import inspect
+import shutil
+import glob
+import re
+
+
+# Configuration file for the Sphinx documentation builder.
+
+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
+
+# -- Project information
+
+project = 'TensorTrack'
+copyright = '2022, The TensorTrack developers'
+author = 'The TensorTrack developers'
+
+# Extract full version (including alpha/beta/rc tags) from github tag
+tag = re.sub('^v', '', os.popen('git describe --tags').read().strip())
+release = '-'.join(tag.split('-')[:2])
+# The short X.Y version
+version = release.split('-')[0]
+
+
+# -- General configuration -----------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+# needs_sphinx = '4.5'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+ 'sphinx.ext.napoleon',
+ 'sphinx.ext.duration',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.autodoc',
+ 'sphinx.ext.autosummary',
+ 'sphinx.ext.autosectionlabel',
+ 'sphinx.ext.todo',
+ 'sphinx.ext.intersphinx',
+ 'sphinx.ext.linkcode',
+ 'sphinx.ext.extlinks',
+ 'sphinx.ext.mathjax',
+ 'sphinx-prompt',
+ 'sphinxcontrib.matlab',
+ 'nbsphinx',
+ 'myst_parser',
+ 'sphinx_gallery.load_style'
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+source_suffix = ['.rst', '.md']
+
+# The master toctree document.
+master_doc = 'index'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# 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
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'default'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This patterns also effect to html_static_path and html_extra_path
+exclude_patterns = ['_build', '**.ipynb_checkpoints']
+
+
+# -- Options for HTML output -----------------------------------------------
+
+html_theme = 'sphinx_rtd_theme'
+
+html_logo = 'img/logo.png'
+
+html_title = u'The TensorTrack documentation'
+
+html_static_path = ['_static']
+
+html_css_files = [
+ 'css/custom.css',
+]
+
+html_theme_options = {
+ 'logo_only': True,
+ 'collapse_navigation': False,
+}
+
+
+# -- Options for EPUB output -----------------------------------------------
+epub_show_urls = 'footnote'
+
+
+# == Options for extensions ===============================================
+
+
+# -- sphinx.ext.autodoc -----------------------------------------------
+# configuration for autodoc
+add_module_names = False
+autodoc_default_options = {
+ 'members': True,
+ 'show-inheritance': True,
+ 'member-order': 'bysource'
+}
+
+# -- sphinx.ext.autosummary -----------------------------------------------
+# configuration for autosummary
+autosummary_generate = False
+
+
+# -- sphinx.ext.todo -----------------------------------------------
+# configuration for todo notes extension
+todo_include_todos = True
+
+
+# -- sphinx.ext.intersphinx -----------------------------------------------
+# cross links to other sphinx documentations
+# this makes e.g. :class:`numpy.ndarray` work
+intersphinx_mapping = {
+ 'python': ('https://docs.python.org/3', None),
+ 'sphinx': ('https://www.sphinx-doc.org/en/master/', None),
+ 'numpy': ('https://numpy.org/doc/stable', None),
+ 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None),
+ 'matplotlib': ('https://matplotlib.org', None),
+ 'h5py': ('https://docs.h5py.org/en/stable/', None),
+}
+intersphinx_disabled_domains = ['std']
+
+
+# -- sphinx.ext.extlinks --------------------------------------------------
+# allows to use, e.g., :arxiv:`1805.00055`
+extlinks = {
+ 'arxiv': ('https://arxiv.org/abs/%s', 'arXiv:'),
+ 'doi': ('https://dx.doi.org/%s', 'doi:'),
+ 'issue': (GITHUBBASE + '/issues/%s', 'issue #'),
+}
+
+
+# -- sphinx.ext.linkcode --------------------------------------------------
+# linkcode to put links to the github repository from the documentation
+
+def linkcode_resolve(domain, info):
+ pass
+
+# def linkcode_resolve(domain, info):
+# # based on the corresponding linkcode_resolve in the `conf.py` of the TenPy
+# # repository.
+# # TODO: fix this, not working at all
+
+# if domain != 'mat':
+# # only link to .mat files
+# return None
+
+# modname = info['module']
+# fullname = info['fullname']
+
+# if not modname or (modname.split('.')[0] not in ['src']):
+# # these aren't the modules we're looking for
+# return None
+
+# # grab the name of the source file
+# filename = modname.replace('.', '/') + '/' + fullname + '.m'
+
+# # grab the module referenced in the docs
+# submod = sys.modules.get(modname)
+# if submod is None:
+# return None
+
+# obj = submod
+# for part in fullname.split('.'):
+# try:
+# obj = getattr(obj, part)
+# except Exception:
+# return None
+
+# # find out line number
+# try:
+# lineno = inspect.findsource(obj)[1]
+# except:
+# lineno = None
+
+# # Build a reference for the line number in GitHub.
+# if lineno:
+# linespec = '#L%d' % (lineno + 1)
+# else:
+# lineno = ''
+
+# return ('%s/blob/%s/%s%s'
+# % (GITHUBBASE, 'main',
+# filename, linespec))
+
+
+
+
+# -- sphinx.ext.napoleon -----------------------------------------------
+# Napoleon settings
+napoleon_google_docstring = True
+napoleon_numpy_docstring = True
+napoleon_include_init_with_doc = False
+napoleon_include_private_with_doc = False
+napoleon_include_special_with_doc = True
+napoleon_use_admonition_for_examples = False
+napoleon_use_admonition_for_notes = True
+napoleon_use_admonition_for_references = False
+napoleon_use_ivar = False
+napoleon_use_param = True
+napoleon_use_rtype = True
+napoleon_preprocess_types = False
+napoleon_type_aliases = None
+napoleon_attr_annotations = True
+napoleon_custom_sections = [
+ ('Properties', 'params_style'),
+ ('Returns', 'params_style'),
+ ('Inputs', 'params_style'),
+ ('Arguments', 'params_style'),
+ ('Optional Arguments', 'params_style'),
+ ('Keyword Arguments', 'params_style'),
+ ('Repeating Arguments', 'params_style'),
+ ('Optional inputs', 'params_style'),
+ 'Usage',
+ ('Syntax', 'returns_style')
+ ]
+
+
+# -- 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": {
+ "inlineMath": [["\\(", "\\)"]],
+ "displayMath": [["\\[", "\\]"]],
+ "macros": {
+ "vect": ["{\\boldsymbol{#1}}", 1],
+ "lv": "{\\vect{v}_L^\\dagger}",
+ "rv": "{\\vect{v}_R}"
+ }
+ }
+}
+
+
+# -- sphinxcontrib.matlab -----------------------------------------------
+# tell Sphinx matlab extension where to find matlab code.
+matlab_keep_package_prefix = False
+matlab_src_dir = repo_root
+primary_domain = 'mat'
+autoclass_content = 'class'
diff --git a/docs/src/examples/examples.rst b/docs/src/examples/examples.rst
new file mode 100644
index 0000000..70a2f1a
--- /dev/null
+++ b/docs/src/examples/examples.rst
@@ -0,0 +1,10 @@
+Tensors
+=======
+
+
+Uniform matrix product states
+-----------------------------
+
+.. nbgallery::
+ uniformMps/uniformMps
+ uniformMps/localHamiltonians
diff --git a/docs/src/examples/uniformMps/img/2minham.svg b/docs/src/examples/uniformMps/img/2minham.svg
new file mode 100644
index 0000000..5ef24e6
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/2minham.svg
@@ -0,0 +1,119 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/Acprime.svg b/docs/src/examples/uniformMps/img/Acprime.svg
new file mode 100644
index 0000000..79d0034
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/Acprime.svg
@@ -0,0 +1,361 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/Cprime.svg b/docs/src/examples/uniformMps/img/Cprime.svg
new file mode 100644
index 0000000..cf29994
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/Cprime.svg
@@ -0,0 +1,328 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/H_Ac.svg b/docs/src/examples/uniformMps/img/H_Ac.svg
new file mode 100644
index 0000000..09bd358
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/H_Ac.svg
@@ -0,0 +1,285 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/H_C.svg b/docs/src/examples/uniformMps/img/H_C.svg
new file mode 100644
index 0000000..e4b76ba
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/H_C.svg
@@ -0,0 +1,203 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/HeffExcitation.svg b/docs/src/examples/uniformMps/img/HeffExcitation.svg
new file mode 100644
index 0000000..9800f8c
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/HeffExcitation.svg
@@ -0,0 +1,862 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/L1.svg b/docs/src/examples/uniformMps/img/L1.svg
new file mode 100644
index 0000000..52890dd
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/L1.svg
@@ -0,0 +1,507 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/LB.svg b/docs/src/examples/uniformMps/img/LB.svg
new file mode 100644
index 0000000..fcc7e75
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/LB.svg
@@ -0,0 +1,165 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/Lh.svg b/docs/src/examples/uniformMps/img/Lh.svg
new file mode 100644
index 0000000..250a3ab
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/Lh.svg
@@ -0,0 +1,149 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/LhMixed.svg b/docs/src/examples/uniformMps/img/LhMixed.svg
new file mode 100644
index 0000000..9ac7df3
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/LhMixed.svg
@@ -0,0 +1,162 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/R1.svg b/docs/src/examples/uniformMps/img/R1.svg
new file mode 100644
index 0000000..afe27f2
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/R1.svg
@@ -0,0 +1,499 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/RB.svg b/docs/src/examples/uniformMps/img/RB.svg
new file mode 100644
index 0000000..821e9dd
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/RB.svg
@@ -0,0 +1,165 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/Rh.svg b/docs/src/examples/uniformMps/img/Rh.svg
new file mode 100644
index 0000000..abdb1d8
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/Rh.svg
@@ -0,0 +1,149 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/RhMixed.svg b/docs/src/examples/uniformMps/img/RhMixed.svg
new file mode 100644
index 0000000..7639cd6
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/RhMixed.svg
@@ -0,0 +1,171 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/Vl.svg b/docs/src/examples/uniformMps/img/Vl.svg
new file mode 100644
index 0000000..e033808
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/Vl.svg
@@ -0,0 +1,124 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/VlX.svg b/docs/src/examples/uniformMps/img/VlX.svg
new file mode 100644
index 0000000..2bc8724
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/VlX.svg
@@ -0,0 +1,67 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/centerTerms.svg b/docs/src/examples/uniformMps/img/centerTerms.svg
new file mode 100644
index 0000000..a36d93b
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/centerTerms.svg
@@ -0,0 +1,134 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/diagC.svg b/docs/src/examples/uniformMps/img/diagC.svg
new file mode 100644
index 0000000..d83ccd1
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/diagC.svg
@@ -0,0 +1,140 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/excitation.svg b/docs/src/examples/uniformMps/img/excitation.svg
new file mode 100644
index 0000000..ef66171
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/excitation.svg
@@ -0,0 +1,464 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/excitationOrth.svg b/docs/src/examples/uniformMps/img/excitationOrth.svg
new file mode 100644
index 0000000..7e41eb3
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/excitationOrth.svg
@@ -0,0 +1,179 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/expVal.svg b/docs/src/examples/uniformMps/img/expVal.svg
new file mode 100644
index 0000000..0c14c52
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/expVal.svg
@@ -0,0 +1,230 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/expVal2.svg b/docs/src/examples/uniformMps/img/expVal2.svg
new file mode 100644
index 0000000..5840013
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/expVal2.svg
@@ -0,0 +1,73 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/expVal3.svg b/docs/src/examples/uniformMps/img/expVal3.svg
new file mode 100644
index 0000000..5a4e1e1
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/expVal3.svg
@@ -0,0 +1,69 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/expValHam.svg b/docs/src/examples/uniformMps/img/expValHam.svg
new file mode 100644
index 0000000..d742b7c
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/expValHam.svg
@@ -0,0 +1,300 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/fixedPoints.svg b/docs/src/examples/uniformMps/img/fixedPoints.svg
new file mode 100644
index 0000000..32e0bec
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/fixedPoints.svg
@@ -0,0 +1,133 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/gaugeExcitation.svg b/docs/src/examples/uniformMps/img/gaugeExcitation.svg
new file mode 100644
index 0000000..e26a6d2
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/gaugeExcitation.svg
@@ -0,0 +1,126 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/gaugeFix.svg b/docs/src/examples/uniformMps/img/gaugeFix.svg
new file mode 100644
index 0000000..e44fb9f
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/gaugeFix.svg
@@ -0,0 +1,103 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/gaugeTransform.svg b/docs/src/examples/uniformMps/img/gaugeTransform.svg
new file mode 100644
index 0000000..47934c0
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/gaugeTransform.svg
@@ -0,0 +1,89 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/grad.svg b/docs/src/examples/uniformMps/img/grad.svg
new file mode 100644
index 0000000..4798d41
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/grad.svg
@@ -0,0 +1,170 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/gradFull.svg b/docs/src/examples/uniformMps/img/gradFull.svg
new file mode 100644
index 0000000..d9a7b9f
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/gradFull.svg
@@ -0,0 +1,220 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/gradTerm.svg b/docs/src/examples/uniformMps/img/gradTerm.svg
new file mode 100644
index 0000000..45f9e01
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/gradTerm.svg
@@ -0,0 +1,142 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/leftGauge.svg b/docs/src/examples/uniformMps/img/leftGauge.svg
new file mode 100644
index 0000000..0294f3c
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/leftGauge.svg
@@ -0,0 +1,78 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/leftOrth.svg b/docs/src/examples/uniformMps/img/leftOrth.svg
new file mode 100644
index 0000000..59468c1
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/leftOrth.svg
@@ -0,0 +1,72 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/leftOrth2.svg b/docs/src/examples/uniformMps/img/leftOrth2.svg
new file mode 100644
index 0000000..fd0c119
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/leftOrth2.svg
@@ -0,0 +1,210 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/leftTerms.svg b/docs/src/examples/uniformMps/img/leftTerms.svg
new file mode 100644
index 0000000..c5633e9
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/leftTerms.svg
@@ -0,0 +1,62 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/mixedGauge.svg b/docs/src/examples/uniformMps/img/mixedGauge.svg
new file mode 100644
index 0000000..8cb4adb
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/mixedGauge.svg
@@ -0,0 +1,226 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/mixedGauge2.svg b/docs/src/examples/uniformMps/img/mixedGauge2.svg
new file mode 100644
index 0000000..4913fb7
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/mixedGauge2.svg
@@ -0,0 +1,91 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/mpsNorm.svg b/docs/src/examples/uniformMps/img/mpsNorm.svg
new file mode 100644
index 0000000..93309cc
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/mpsNorm.svg
@@ -0,0 +1,323 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/qrConv.svg b/docs/src/examples/uniformMps/img/qrConv.svg
new file mode 100644
index 0000000..5caf51c
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/qrConv.svg
@@ -0,0 +1,91 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/qrStep.svg b/docs/src/examples/uniformMps/img/qrStep.svg
new file mode 100644
index 0000000..efc51e8
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/qrStep.svg
@@ -0,0 +1,120 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/quasi_inveff.svg b/docs/src/examples/uniformMps/img/quasi_inveff.svg
new file mode 100644
index 0000000..71e8026
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/quasi_inveff.svg
@@ -0,0 +1,164 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/regTransfer.svg b/docs/src/examples/uniformMps/img/regTransfer.svg
new file mode 100644
index 0000000..208285f
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/regTransfer.svg
@@ -0,0 +1,106 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/rightGauge.svg b/docs/src/examples/uniformMps/img/rightGauge.svg
new file mode 100644
index 0000000..cd126ec
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/rightGauge.svg
@@ -0,0 +1,98 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/rightOrth.svg b/docs/src/examples/uniformMps/img/rightOrth.svg
new file mode 100644
index 0000000..756e2f3
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/rightOrth.svg
@@ -0,0 +1,72 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/rightTerms.svg b/docs/src/examples/uniformMps/img/rightTerms.svg
new file mode 100644
index 0000000..f6d2b32
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/rightTerms.svg
@@ -0,0 +1,62 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/tm.svg b/docs/src/examples/uniformMps/img/tm.svg
new file mode 100644
index 0000000..52ab081
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/tm.svg
@@ -0,0 +1,131 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/traceNorm.svg b/docs/src/examples/uniformMps/img/traceNorm.svg
new file mode 100644
index 0000000..e2b66ad
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/traceNorm.svg
@@ -0,0 +1,54 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/transferPower.svg b/docs/src/examples/uniformMps/img/transferPower.svg
new file mode 100644
index 0000000..fbfa9d7
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/transferPower.svg
@@ -0,0 +1,87 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/truncMPS.svg b/docs/src/examples/uniformMps/img/truncMPS.svg
new file mode 100644
index 0000000..087a9f4
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/truncMPS.svg
@@ -0,0 +1,318 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/umps.svg b/docs/src/examples/uniformMps/img/umps.svg
new file mode 100644
index 0000000..84ae9a9
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/umps.svg
@@ -0,0 +1,119 @@
+
+
diff --git a/docs/src/examples/uniformMps/img/unitaryGauge.svg b/docs/src/examples/uniformMps/img/unitaryGauge.svg
new file mode 100644
index 0000000..b51482a
--- /dev/null
+++ b/docs/src/examples/uniformMps/img/unitaryGauge.svg
@@ -0,0 +1,72 @@
+
+
diff --git a/docs/src/examples/uniformMps/localHamiltonians.ipynb b/docs/src/examples/uniformMps/localHamiltonians.ipynb
new file mode 100644
index 0000000..5b668a0
--- /dev/null
+++ b/docs/src/examples/uniformMps/localHamiltonians.ipynb
@@ -0,0 +1,2130 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "278a6a0c-a806-4891-bd8a-ccfac3e1d7d1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "addpath(genpath('../../../../src'))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d7ee14c7-816d-422d-99c2-5c698da952d1",
+ "metadata": {},
+ "source": [
+ "# Finding ground states of local Hamiltonians\n",
+ "\n",
+ "This notebook demonstrates how to find the ground state of local one-dimensional Hamiltonians with MPS methods, using the `Tensor` backend. It utilizes the functionalities defined in the notebook on uniform MPS (which should therefore be run before this notebook). Our discussion is based on the fourth and sixth chapters of the [lecture notes on tangent space methods for uniform MPS](https://doi.org/10.21468/SciPostPhysLectNotes.7) by Laurens Vanderstraeten, Jutho Haegeman and Frank Verstraete.\n",
+ "\n",
+ "The contents of this notebook mirror that of a tutorial given at the [2020 school on Tensor Network based approaches to Quantum Many-Body Systems](http://quantumtensor.pks.mpg.de/index.php/schools/2020-school/) held in Bad Honnef, Germany, which can be found [here](https://github.com/leburgel/BadHonnefTutorial).\n",
+ "\n",
+ "## 1 Introduction\n",
+ "\n",
+ "In the notebook on uniform MPS, we stated that these states ccan be used to efficiently approximate low-energy states of one-dimensional systems with gapped local Hamiltonians. Having defined ways of representing and manipulating MPS, the logical next step is therefore to have a look at how exactly they can be used to find ground states. To this end, we consider a nearest-neighbour Hamiltonian $H$ of the form\n",
+ "\n",
+ "$$H = \\sum_n h_{n, n+1}$$\n",
+ "\n",
+ "acting on an infinite one-dimensional system. Here, $h_{n,n+1}$ is a hermitian operator acting non-trivially on sites $n$ and $n+1$. As in any variational approach, the variational principle serves as a guide for finding ground-state approximations, dictating that the optimal MPS approximation of the ground state corresponds to the minimum of the expectation value of the energy,\n",
+ "\n",
+ "$$ \\min_A \\frac{\\left \\langle \\Psi(\\bar{A}) \\middle | H \\middle | \\Psi(A) \\right \\rangle}{\\left \\langle \\Psi(\\bar{A}) \\middle | \\Psi(A) \\right \\rangle}. $$\n",
+ "\n",
+ "In the thermodynamic limit the energy diverges with system size, but, since we are working with translation-invariant states only, we should rather minimize the energy density. In the following we will always restrict our discussion to preoperly normalized states. Diagrammatically, the minimization problem can then be recast as\n",
+ "\n",
+ "
\n",
+ "\n",
+ "In this notebook we illustratre numerical optimization strategies for minimizing this energy density directly."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "44f7d643-8783-43f6-a12a-c8cbce85f9cb",
+ "metadata": {},
+ "source": [
+ "## 2 The gradient\n",
+ "\n",
+ "Any optimization problem relies on an efficient evaluation of the gradient, so the first thing to do is to compute this quantity. The objective function $f$ that we want to minimize is a real function of the complex-valued $A$, or equivalently, of the independent variables $A$ and $\\bar{A}$. The gradient $g$ is then obtained by differentiating $f(\\bar{A},A)$ with respect to $\\bar{A}$,\n",
+ "\n",
+ "$$\n",
+ "\\begin{align}\n",
+ "g &= 2 \\times \\frac{\\partial f(\\bar{A},A) }{ \\partial \\bar{A} } \\\\\n",
+ "&= 2\\times \\frac{\\partial_{\\bar{A}} \\left \\langle \\Psi(\\bar{A}) \\middle | h \\middle | \\Psi(A) \\right \\rangle } {\\left \\langle \\Psi(\\bar{A}) \\middle | \\Psi(A) \\right \\rangle} - 2\\times \\frac{\\left \\langle \\Psi(\\bar{A}) \\middle | h \\middle | \\Psi(A) \\right \\rangle} {\\left \\langle \\Psi(\\bar{A}) \\middle | \\Psi(A) \\right \\rangle^2} \\partial_{\\bar{A}} \\left \\langle \\Psi(\\bar{A}) \\middle | \\Psi(A) \\right \\rangle ,\\\\\n",
+ "&= 2\\times \\frac{\\partial_{\\bar{A}} \\left \\langle \\Psi(\\bar{A}) \\middle | h \\middle | \\Psi(A) \\right \\rangle - e \\partial_{\\bar{A}} \\left \\langle \\Psi(\\bar{A}) \\middle | \\Psi(A) \\right \\rangle } {\\left \\langle \\Psi(\\bar{A}) \\middle | \\Psi(A) \\right \\rangle},\\\\\n",
+ "\\end{align}\n",
+ "$$\n",
+ "\n",
+ "where we have clearly indicated $A$ and $\\bar{A}$ as independent variables and $e$ is the current energy density given by\n",
+ "\n",
+ "$$\n",
+ "e = \\frac{\\left \\langle \\Psi(\\bar{A}) \\middle | h \\middle | \\Psi(A) \\right \\rangle} {\\left \\langle \\Psi(\\bar{A}) \\middle | \\Psi(A) \\right \\rangle}.\n",
+ "$$\n",
+ "\n",
+ "If we make sure that the MPS is properly normalized and subtract the current energy density from every term in the hamiltonian, $h \\leftarrow h - e$, the gradient takes on the simple form\n",
+ "\n",
+ "$$ g = 2 \\times \\partial_{\\bar{A}} \\left \\langle \\Psi(\\bar{A}) \\middle | h \\middle | \\Psi(A) \\right \\rangle.$$\n",
+ "\n",
+ "Thus, the gradient is obtained by differentiating the expression\n",
+ "\n",
+ "
\n",
+ "\n",
+ "with respect to $\\bar{A}$. This gives rise to a sum over all sites, where in every term we differentiate with respect to one tensor $\\bar{A}$ in the bra layer. Differentiating with respect to one $\\bar{A}$ tensor amounts to leaving out that tensor, and interpreting the open legs as outgoing ones, i.e. each term looks like\n",
+ "\n",
+ "
\n",
+ "\n",
+ "The full gradient is then obtained as an infinite sum over these terms. By dividing the terms into three different classes and doing some bookkeeping as illustrated below, we can eventually write this sum in a relatively simple closed form."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "883a3418-3e59-4515-bff8-d52ffd9ad9c8",
+ "metadata": {},
+ "source": [
+ "### Terms of the 'center' kind\n",
+ "The first kind of terms that arise in the above expression for the gradient are obtained by differentiation with respect to an $\\bar{A}$ tensor on the legs of the Hamiltonian operator. This results in two 'center' terms\n",
+ "\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "eb94c05e-ef89-4f78-a091-4fdd76de28c1",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/gradCenterTerms.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file gradCenterTerms.m\n",
+ "function [term1, term2] = gradCenterTerms(hTilde, A, l, r)\n",
+ " % Calculate the value of the center terms.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight.\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % normalized MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D), optional\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D), optional\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % term1 : :class:`Tensor` (D, d, D)\n",
+ " % first term of gradient,\n",
+ " % ordered left-mid-right.\n",
+ " % term2 : :class:`Tensor` (D, d, D)\n",
+ " % second term of gradient,\n",
+ " % ordered left-mid-right.\n",
+ " \n",
+ " arguments\n",
+ " hTilde\n",
+ " A\n",
+ " l = []\n",
+ " r = []\n",
+ " end\n",
+ " \n",
+ " % calculate fixed points if not given\n",
+ " if isempty(l) || isempty(r)\n",
+ " [l, r] = fixedPoints(A);\n",
+ " end\n",
+ " \n",
+ " term1 = contract(l, [-1, 1], r, [5, 7], A, [1, 3, 2], A, [2, 4, 5], conj(A), [-3, 6, 7], hTilde, [3, 4, -2, 6]);\n",
+ " \n",
+ " term2 = contract(l, [6, 1], r, [5, -3], A, [1, 3, 2], A, [2, 4, 5], conj(A), [6, 7, -1], hTilde, [3, 4, 7, -2]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "07b00093-01a4-48ad-b11a-79d7f85767f8",
+ "metadata": {},
+ "source": [
+ "### Terms of the 'left' kind\n",
+ "For the terms where we leave out an $\\bar{A}$ tensor to the left of $h$, which we will call 'left' terms, we can contract everything to the left of this missing $\\bar{A}$ with the left fixed point $l$, while everything to the right of $h$ can be contracted with right fixed point $r$.\n",
+ "\n",
+ "In between these two outer parts of the network there remains a region where the regular MPS transfer matrix $E$ is applied a number of times. The action of this region is therefore captured by the operator $E^n$, where the power $n$ is determined by the seperation between the outer left and right parts for the specific term under consideration. When summing all left terms, the outer parts of the contraction always remain the same, while only the power $n$ differs for every term. Thus, summing all left terms corresponds to contracting the operator \n",
+ "\n",
+ "$$E_\\text{sum} = 1 + E + E^2 + \\dots = \\frac{1}{1-E}$$\n",
+ "\n",
+ "between the left and right outer parts. Here, we have naively used the geometric series to write the sum in a closed form. However, since by our normalization the transfer matrix has leading eigenvalue $1$, this resulting expression will diverge and is therefore ill-defined. We can get around this by introducing a regularized transfer matrix $\\tilde{E}$ which is defined by subtracting the divergent part,\n",
+ "\n",
+ "
\n",
+ "\n",
+ "Since we have already shifted the energy density to have a zero expectation value, $h \\leftarrow h - e$, it can easily be verified that the contribution of the leading divergent part vanishes in every left term, meaning that we can simply replace the original transfer matrix by its regularized version without changing any of the terms, and only then take the infinite sum which now has a well defined expression in terms of an inverse,\n",
+ "\n",
+ "$$ E_\\text{sum} \\rightarrow \\frac{1}{1-\\tilde{E}} \\equiv (1 - E)^P ,$$\n",
+ "\n",
+ "where we have introduced the pseudo-inverse defined as $(1 - E)^P = (1-\\tilde{E})^{-1}$.\n",
+ "\n",
+ "Using this notation we can define the partial contraction\n",
+ "\n",
+ "
\n",
+ "\n",
+ "such that the sum of all left terms equals\n",
+ "\n",
+ "
\n",
+ "\n",
+ "\n",
+ "If we would compute the partial contraction $R_h$ directly by explicitly computing the pseudo-inverse, this would entail a computational complexity $O(D^6)$. Instead, we can define $L_h$ as the solution of a linear problem by multiplying both sides of the corresponding definition by $(1-\\tilde{E})$. This results in an equation of the form $Ax = b$ which may be solved for $x$ by using Krylov-based iterative methods such as those implemented in `Tensor.linsolve`. Note that these methods only require the action of $A = (1-\\tilde{E})$ on a vector and not the full matrix $A$. This action can again be supplied to the linear solver using a function handle."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "f173cce3-2125-4816-9406-9b7a257c357d",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/reducedHamUniform.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file reducedHamUniform.m\n",
+ "function hTilde = reducedHamUniform(h, A, l, r)\n",
+ " % Regularize Hamiltonian such that its expectation value is 0.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % h : :class:`Tensor` (d, d, d, d)\n",
+ " % Hamiltonian that needs to be reduced,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight.\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % normalized MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D), optional\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D), optional\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight.\n",
+ " \n",
+ " arguments\n",
+ " h\n",
+ " A\n",
+ " l = []\n",
+ " r = []\n",
+ " end\n",
+ " \n",
+ " % calculate fixed points if not given\n",
+ " if isempty(l) || isempty(r)\n",
+ " [l, r] = fixedPoints(A);\n",
+ " end\n",
+ " \n",
+ " % calculate expectation value\n",
+ " e = real(expVal2Uniform(h, A, l, r));\n",
+ " \n",
+ " % substract from hamiltonian\n",
+ " I = h.eye(h.dims([1, 1]), 'Rank', [1, 1]);\n",
+ " hTilde = h - e * contract(I, [-1, -3], I, [-2, -4]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "5da73558-9358-4c54-8842-11248913b6d8",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/EtildeRight.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file EtildeRight.m\n",
+ "function vNew = EtildeRight(v, A, l, r)\n",
+ " % Implement the action of (1 - Etilde) on a right vector v.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % v : :class:`Tensor` (D, D)\n",
+ " % right matrix on which\n",
+ " % (1 - Etilde) acts\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % normalized MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D)\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D)\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % vNew : :class:`Tensor` (D, D)\n",
+ " % result of action of (1 - Etilde)\n",
+ " % on a right matrix\n",
+ " \n",
+ " % interpret input as matrix\n",
+ " v = repartition(v, [1, 1]);\n",
+ " \n",
+ " % transfermatrix contribution\n",
+ " transfer = contract(A, [-1, 2, 1], conj(A), [-2, 2, 3], v, [1, 3], 'Rank', [1, 1]);\n",
+ "\n",
+ " % fixed point contribution\n",
+ " fixed = trace(l * v) * r;\n",
+ "\n",
+ " % sum these with the contribution of the identity\n",
+ " vNew = v - transfer + fixed;\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "fdf0a261-3421-431f-9c86-5baa0c62e26a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/RhUniform.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file RhUniform.m\n",
+ "function Rh = RhUniform(hTilde, A, l, r)\n",
+ " % Find the partial contraction for Rh.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % normalized MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D), optional\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D), optional\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % Rh : :class:`Tensor` (D, D)\n",
+ " % result of contraction,\n",
+ " % ordered top-bottom.\n",
+ " \n",
+ " arguments\n",
+ " hTilde\n",
+ " A\n",
+ " l = []\n",
+ " r = []\n",
+ " end\n",
+ " \n",
+ " % calculate fixed points if not given\n",
+ " if isempty(l) || isempty(r)\n",
+ " [l, r] = fixedPoints(A);\n",
+ " end\n",
+ " \n",
+ " % construct b, which is the matrix to the right of (1 - E)^P in the figure above\n",
+ " b = contract(r, [4, 5], A, [-1, 2, 1], A, [1, 3, 4], conj(A), [-2, 8, 7], conj(A), [7, 6, 5], hTilde, [2, 3, 8, 6], 'Rank', [1, 1]);\n",
+ " \n",
+ " % solve Ax = b for x\n",
+ " [Rh, ~] = linsolve(@(v) EtildeRight(v, A, l, r), b);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "1dccad80-5063-4520-a955-de3d1a9656a7",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/gradLeftTerms.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file gradLeftTerms.m\n",
+ "function leftTerms = gradLeftTerms(hTilde, A, l, r)\n",
+ " % Calculate the value of the left terms.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D), optional\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D), optional\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % leftTerms : :class:`Tensor` (D, d, D)\n",
+ " % left terms of gradient,\n",
+ " % ordered left-mid-right.\n",
+ " \n",
+ " arguments\n",
+ " hTilde\n",
+ " A\n",
+ " l = []\n",
+ " r = []\n",
+ " end\n",
+ " \n",
+ " % calculate fixed points if not given\n",
+ " if isempty(l) || isempty(r)\n",
+ " [l, r] = fixedPoints(A);\n",
+ " end\n",
+ " \n",
+ " % calculate partial contraction\n",
+ " Rh = RhUniform(hTilde, A, l, r);\n",
+ " \n",
+ " % calculate full contraction\n",
+ " leftTerms = contract(Rh, [1, -3], A, [2, -2, 1], l, [-1, 2]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e7442172-1899-4408-80c5-8e0cd0511d6e",
+ "metadata": {},
+ "source": [
+ "### Terms of the 'right' kind\n",
+ "\n",
+ "In a similar way, the terms where we leave out an $\\bar{A}$ to the right of $h$ can be evaluated by defining the partial contraction\n",
+ "\n",
+ "
\n",
+ "\n",
+ "which can again be found by solving a linear system, such that the sum of all right terms can be written as\n",
+ "\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "7e911d0c-5830-463e-a2e2-256c4a316dbc",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/EtildeLeft.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file EtildeLeft.m\n",
+ "function vNew = EtildeLeft(v, A, l, r)\n",
+ " % Implement the action of (1 - Etilde) on a left vector matrix v.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % v : :class:`Tensor` (D, D)\n",
+ " % right matrix on which\n",
+ " % (1 - Etilde) acts\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % normalized MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D)\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D)\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % vNew : :class:`Tensor` (D, D)\n",
+ " % result of action of (1 - Etilde)\n",
+ " % on a left matrix\n",
+ " \n",
+ " % interpret input as matrix\n",
+ " v = repartition(v, [1, 1]);\n",
+ "\n",
+ " % transfer matrix contribution\n",
+ " transfer = contract(v, [3, 1], A, [1, 2, -2], conj(A), [3, 2, -1], 'Rank', [1, 1]);\n",
+ "\n",
+ " % fixed point contribution\n",
+ " fixed = trace(v * r) * l;\n",
+ "\n",
+ " % sum these with the contribution of the identity\n",
+ " vNew = v - transfer + fixed;\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "f5baae51-0799-4466-8120-60114e939e4c",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/LhUniform.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file LhUniform.m\n",
+ "function Lh = LhUniform(hTilde, A, l, r)\n",
+ " % Find the partial contraction for Lh.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D), optional\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D), optional\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % Lh : :class:`Tensor` (D, D)\n",
+ " % result of contraction,\n",
+ " % ordered bottom-top.\n",
+ " \n",
+ " arguments\n",
+ " hTilde\n",
+ " A\n",
+ " l = []\n",
+ " r = []\n",
+ " end\n",
+ " \n",
+ " % calculate fixed points if not given\n",
+ " if isempty(l) || isempty(r)\n",
+ " [l, r] = fixedPoints(A);\n",
+ " end\n",
+ " \n",
+ " % construct b, which is the matrix to the right of (1 - E)^P in the figure above\n",
+ " b = contract(l, [5, 1], A, [1, 3, 2], A, [2, 4, -2], conj(A), [5, 6, 7], conj(A), [7, 8, -1], hTilde, [3, 4, 6, 8]);\n",
+ " \n",
+ " % solve Ax = b for x\n",
+ " [Lh, ~] = linsolve(@(v) EtildeLeft(v, A, l, r), b);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "c9526cae-e4f4-46ab-8877-24d056045d0d",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/gradRightTerms.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file gradRightTerms.m\n",
+ "function rightTerms = gradRightTerms(hTilde, A, l, r)\n",
+ " % Calculate the value of the right terms.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D), optional\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D), optional\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % rightTerms : :class:`Tensor` (D, d, D)\n",
+ " % right terms of gradient,\n",
+ " % ordered left-mid-right.\n",
+ " \n",
+ " arguments\n",
+ " hTilde\n",
+ " A\n",
+ " l = []\n",
+ " r = []\n",
+ " end\n",
+ " \n",
+ " % calculate fixed points if not given\n",
+ " if isempty(l) || isempty(r)\n",
+ " [l, r] = fixedPoints(A);\n",
+ " end\n",
+ " \n",
+ " % calculate partial contraction\n",
+ " Lh = LhUniform(hTilde, A, l, r);\n",
+ " \n",
+ " % calculate full contraction\n",
+ " rightTerms = contract(Lh, [-1, 1], A, [1, -2, 2], r, [2, -3]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f7abd285-54e9-4bad-8f69-5948da5448b0",
+ "metadata": {},
+ "source": [
+ "### The gradient\n",
+ "\n",
+ "The full gradient is then found by summing the contributions of all three types of terms,\n",
+ "\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "1b9f08ef-f953-45d4-8774-01d0e7c6a979",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/gradient.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file gradient.m\n",
+ "function grad = gradient(h, A, l, r)\n",
+ " % Calculate the gradient of the expectation value of h @ MPS A.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % h : :class:`Tensor` (d, d, d, d)\n",
+ " % Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D), optional\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D), optional\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % grad : :class:`Tensor` (D, d, D)\n",
+ " % gradient,\n",
+ " % ordered left-mid-right.\n",
+ " \n",
+ " arguments\n",
+ " h\n",
+ " A\n",
+ " l = []\n",
+ " r = []\n",
+ " end\n",
+ " \n",
+ " % calculate fixed points if not given\n",
+ " if isempty(l) || isempty(r)\n",
+ " [l, r] = fixedPoints(A);\n",
+ " end\n",
+ " \n",
+ " % renormalize Hamiltonian\n",
+ " hTilde = reducedHamUniform(h, A, l, r);\n",
+ " \n",
+ " % find terms\n",
+ " [centerTerm1, centerTerm2] = gradCenterTerms(hTilde, A, l, r);\n",
+ " leftTerms = gradLeftTerms(hTilde, A, l, r);\n",
+ " rightTerms = gradRightTerms(hTilde, A, l, r);\n",
+ " \n",
+ " grad = 2 * (centerTerm1 + centerTerm2 + leftTerms + rightTerms);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dfe6d612-824a-45a4-8627-8e856939451d",
+ "metadata": {},
+ "source": [
+ "## 3 Gradient descent algorithms\n",
+ "\n",
+ "The most straightforward way to use this expression for the gradient to find the ground state of a Hamiltonian is to implement a gradient-search method for minimizing the energy expecation value. The simplest such method is a steepest-descent search, where in every iteration the tensor $A$ is updated in the direction opposite to the gradient along a small step $\\varepsilon$,\n",
+ "\n",
+ "$$ A_{i+1} = A_i - \\varepsilon g .$$\n",
+ "\n",
+ "This procedure is repeated until we find the optimal MPS tensor $A^*$ for which the gradient vanishes. This approach can be improved upon by resorting to other optimization schemes such a conjugate-gradient or quasi-Newton methods. Below we demonstrate both a simple steepest-descent with a fixed step size, as well as an approach using Matlab's builtin nonlinear optimization routine `fminunc`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "c0ec6a5e-18eb-4926-8863-86a4b2991c71",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/groundStateGradDescent.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file groundStateGradDescent.m\n",
+ "function [E, A] = groundStateGradDescent(h, D, eps, A0, tol, maxIter)\n",
+ " % Find the ground state using gradient descent.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % h : :class:`Tensor` (d, d, d, d)\n",
+ " % Hamiltonian to minimize,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight.\n",
+ " % D : int\n",
+ " % bond dimension\n",
+ " % eps : double, optional\n",
+ " % Stepsize.\n",
+ " % A0 : :class:`Tensor` (D, d, D), optional\n",
+ " % normalized MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % initial guess.\n",
+ " % tol : float, optional\n",
+ " % tolerance for convergence criterium.\n",
+ " % maxIter : int, optional\n",
+ " % maximum number of iterations\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % E : double\n",
+ " % expectation value @ minimum\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % ground state MPS,\n",
+ " % ordered left-mid-right.\n",
+ " \n",
+ " arguments\n",
+ " h\n",
+ " D\n",
+ " eps = 1e-1\n",
+ " A0 = []\n",
+ " tol = 1e-4\n",
+ " maxIter = 1e4\n",
+ " end\n",
+ " \n",
+ " % if no initial value, choose random\n",
+ " if isempty(A0)\n",
+ " A0 = createMPS(D, h.dims(1));\n",
+ " A0 = normalizeMPS(A0);\n",
+ " end\n",
+ " \n",
+ " % calculate gradient\n",
+ " g = gradient(h, A0);\n",
+ " g0 = g.zeros(g.codomain, g.domain);\n",
+ " \n",
+ " A = A0;\n",
+ " \n",
+ " i = 0;\n",
+ " while norm(g) > tol\n",
+ " % do a step\n",
+ " A = A - eps * g;\n",
+ " A = normalizeMPS(A);\n",
+ " i = i + 1;\n",
+ " \n",
+ " if ~mod(i, 20)\n",
+ " E = real(expVal2Uniform(h, A));\n",
+ " fprintf('iteration:\\t%d,\\tenergy:\\t%f\\tgradient norm\\t%.4e\\n', i, E, norm(g))\n",
+ " end\n",
+ " \n",
+ " % calculate new gradient\n",
+ " g = gradient(h, A);\n",
+ " \n",
+ " if i > maxIter\n",
+ " warning('Gradient descent did not converge!')\n",
+ " break\n",
+ " end\n",
+ " \n",
+ " % calculate ground state energy\n",
+ " E = real(expVal2Uniform(h, A));\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6f815406-b108-4de6-9779-9b9c07040a5a",
+ "metadata": {},
+ "source": [
+ "In order to use Matlab's `fminunc` for this purpose, one must keep in mind the fact that this routine requires an objective function that maps a real vector to a scalar. In particular, complex tensors must be given as reals vectors in the input, and must be again represented as real vectors in the output. This can be done using the `Tensor.vectorize` and `Tensor.devectorize` methods."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "6cc78f34-f008-4f96-9531-e85bbf392d92",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/groundStateMinimize.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file groundStateMinimize.m\n",
+ "function [E, A] = groundStateMinimize(h, D, A0, tol)\n",
+ " % Find the ground state using a scipy minimizer.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % h : :class:`Tensor` (d, d, d, d)\n",
+ " % Hamiltonian to minimize,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight.\n",
+ " % D : int\n",
+ " % Bond dimension\n",
+ " % A0 : :class:`Tensor` (D, d, D), optional\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % initial guess.\n",
+ " % tol : double, options\n",
+ " % Relative convergence criterium.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % E : double\n",
+ " % expectation value @ minimum\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % ground state MPS,\n",
+ " % ordered left-mid-right.\n",
+ " \n",
+ " arguments\n",
+ " h\n",
+ " D\n",
+ " A0 = []\n",
+ " tol = 1e-4\n",
+ " end\n",
+ " \n",
+ " % if no initial value, choose random\n",
+ " if isempty(A0)\n",
+ " A0 = createMPS(D, d);\n",
+ " A0 = normalizeMPS(A0);\n",
+ " end\n",
+ " \n",
+ " % calculate minimum\n",
+ " options = optimoptions('fminunc', 'SpecifyObjectiveGradient', true, 'Display', 'final', 'OptimalityTolerance', tol);\n",
+ " [Avar, E] = fminunc(@(x) f(x, A0), vectorize(A0, 'real'), options);\n",
+ " \n",
+ " % unpack result\n",
+ " A = devectorize(Avar, A0, 'real');\n",
+ " \n",
+ " % define f for minimize with fminunc\n",
+ " function [e, varg] = f(varA, A0)\n",
+ " % Function to optimize via fminunc.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % varA : double (2 * D * d * D)\n",
+ " % MPS tensor in real vector form.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % e : double\n",
+ " % function value @varA\n",
+ " % varg : double (2 * D * d * D)\n",
+ " % gradient vector @varA\n",
+ " \n",
+ " % unwrap varA\n",
+ " A = devectorize(varA, A0, 'real');\n",
+ " A = normalizeMPS(A);\n",
+ " \n",
+ " % calculate fixed points\n",
+ " [l, r] = fixedPoints(A);\n",
+ " \n",
+ " % calculate function value and gradient\n",
+ " e = real(expVal2Uniform(h, A, l, r));\n",
+ " g = gradient(h, A, l, r);\n",
+ " \n",
+ " % wrap g\n",
+ " varg = vectorize(g, 'real');\n",
+ " end\n",
+ " \n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "afa1129f-fb0b-44d3-95e1-6bad6ab8efc5",
+ "metadata": {},
+ "source": [
+ "To demonstrate these methods, we now have a look the specific case of the antiferromagnetic spin-1 Heisenberg model in one dimension. To this end we first define the spin-1 Heisenberg Hamiltonian:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "3caa10bf-35ed-467b-8168-18c2b1f04acc",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/Heisenberg.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file Heisenberg.m\n",
+ "function h = Heisenberg(Jx, Jy, Jz, hz)\n",
+ " % Construct the spin-1 Heisenberg Hamiltonian for given couplings.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % Jx : double\n",
+ " % Coupling strength in x direction\n",
+ " % Jy : double\n",
+ " % Coupling strength in y direction\n",
+ " % Jy : double\n",
+ " % Coupling strength in z direction\n",
+ " % hz : double\n",
+ " % Coupling for Sz terms\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % h : :class:`Tensor` (3, 3, 3, 3)\n",
+ " % Spin-1 Heisenberg Hamiltonian.\n",
+ " \n",
+ " Sx = Tensor([0, 1, 0; 1, 0, 1; 0, 1, 0] / sqrt(2));\n",
+ " Sy = Tensor([0, -1, 0; 1, 0, -1; 0, 1, 0] * 1.0j / sqrt(2));\n",
+ " Sz = Tensor([1, 0, 0; 0, 0, 0; 0, 0, -1]);\n",
+ " I = Tensor(eye(3));\n",
+ " \n",
+ " h = -Jx * contract(Sx, [-1, -3], Sx, [-2, -4]) ...\n",
+ " -Jy * contract(Sy, [-1, -3], Sy, [-2, -4]) ...\n",
+ " -Jz * contract(Sz, [-1, -3], Sz, [-2, -4]) ...\n",
+ " -hz * contract(I, [-1, -3], Sz, [-2, -4]) ...\n",
+ " -hz * contract(Sz, [-1, -3], I, [-2, -4]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "6ca9bcc6-72b0-45d9-99b7-9901b50c6dff",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Gradient descent optimization:\n",
+ "iteration:\t20,\tenergy:\t-1.387964\tgradient norm\t4.4666e-02\n",
+ "iteration:\t40,\tenergy:\t-1.394092\tgradient norm\t2.4404e-02\n",
+ "iteration:\t60,\tenergy:\t-1.396492\tgradient norm\t1.6779e-02\n",
+ "iteration:\t80,\tenergy:\t-1.397722\tgradient norm\t1.2414e-02\n",
+ "Time until convergence: 25.7345 s\n",
+ "Computed energy: -1.398312125810\n",
+ "Optimization using Matlab\"s builtin fminunc:\n",
+ "\n",
+ "Local minimum found.\n",
+ "\n",
+ "Optimization completed because the size of the gradient is less than\n",
+ "the value of the optimality tolerance.\n",
+ "\n",
+ "Time until convergence: 68.3774 s\n",
+ "Computed energy: -1.401346835477\n"
+ ]
+ }
+ ],
+ "source": [
+ "d = 3;\n",
+ "D = 12;\n",
+ "A = createMPS(D, d);\n",
+ "A = normalizeMPS(A);\n",
+ "\n",
+ "h = Heisenberg(-1, -1, -1, 0);\n",
+ "\n",
+ "% energy optimization using naive gradient descent\n",
+ "% for D=12 or higher: tolerance lower than 1e-3 gives very long runtimes\n",
+ "fprintf('Gradient descent optimization:\\n')\n",
+ "t1 = tic;\n",
+ "[E1, A1] = groundStateGradDescent(h, D, 3e-1, A, 1e-2, 100);\n",
+ "fprintf('Time until convergence: %.4f s\\n', toc(t1))\n",
+ "fprintf('Computed energy: %.12f\\n', E1)\n",
+ "\n",
+ "% energy optimization using Matlab's builtin fminunc\n",
+ "fprintf('Optimization using Matlab\"s builtin fminunc:\\n')\n",
+ "t2 = tic;\n",
+ "[E2, A2] = groundStateMinimize(h, D, A, 1e-4);\n",
+ "fprintf('Time until convergence: %.4f s\\n', toc(t2))\n",
+ "fprintf('Computed energy: %.12f\\n', E2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d44e16c2-ce1f-4d09-a6b4-c7c8a0601c45",
+ "metadata": {},
+ "source": [
+ "## 4 The VUMPS algorithm\n",
+ "\n",
+ "In the previous section we have derived an expression for the gradient starting from an MPS in the uniform gauge, which corresponds to an object that lives in the space of MPS tensors. We now discuss how to improve upon direct optimization schemes based on this form of the gradient by exploiting the structure of the MPS manifold as well as the mixed gauge for MPS.\n",
+ "\n",
+ "Indeed, while the gradient in the above form indicates a direction in the space of complex tensors in which the energy decreases, intuitively it would make more sense if we could find a way to interpret the gradient as a direction *along the MPS manifold* along which we can decrease the energy. This can be achieved by interpreting the gradient as a *tangent vector in the tangent space to the MPS manifold*. By formulating the energy optimization in terms of this tangent space gradient written in mixed gauge, one arives at the [VUMPS](https://doi.org/10.1103/PhysRevB.97.045145) algorithm (which stand for 'variational uniform matrix product states'). The precise derivation of the tangent space gradient in mixed gauge falls beyond the scope of this tutorial, and can be found in the [lecture notes](https://doi.org/10.21468/SciPostPhysLectNotes.7). Instead we will simply illustrate the implementation of the VUMPS algorithm given the mixed gauge tangent space gradient.\n",
+ "\n",
+ "Most of the following required steps will be reminiscent of those outlined above, where we now consistently work in the mixed gauge. We start off by implementing the regularization of the two-site Hamiltonian in the mixed gauge."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "3357d029-bd87-42aa-8ea3-83cf91f23623",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/reducedHamMixed.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file reducedHamMixed.m\n",
+ "function hTilde = reducedHamMixed(h, Ac, Ar)\n",
+ " % Regularize Hamiltonian such that its expectation value is 0.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % h : :class:`Tensor` (d, d, d, d)\n",
+ " % Hamiltonian that needs to be reduced,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight.\n",
+ " % Ac : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauged.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right gauged.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight.\n",
+ " \n",
+ " % calculate expectation value\n",
+ " e = real(expVal2Mixed(h, Ac, Ar));\n",
+ " \n",
+ " % substract from hamiltonian\n",
+ " I = h.eye(h.dims([1, 1]), 'Rank', [1, 1]); % TODO: change this once eyes are better\n",
+ " hTilde = h - e * contract(I, [-1, -3], I, [-2, -4]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6f9dde4c-44df-4505-babc-0078807443c6",
+ "metadata": {},
+ "source": [
+ "The variational optimum of the energy is characterized by the condition that the gradient is zero at this point. Writing the tangent space gradient as $G$, we now wish to formulate an algorithm which minimizes the error measure\n",
+ "\n",
+ "$$ \\varepsilon = \\left( \\boldsymbol{G}^\\dagger \\boldsymbol{G} \\right)^{1/2} $$\n",
+ "\n",
+ "in an efficient way. The explicit form of the tangent space gradient in mixed gauge is given by\n",
+ "\n",
+ "$$ G = A^\\prime_{C} - A_L C^\\prime = A^\\prime_{C} - C^\\prime A_R, $$\n",
+ "\n",
+ "where $A^\\prime_{C}$ and $C^\\prime$ are defined as\n",
+ "\n",
+ "
\n",
+ "\n",
+ "and\n",
+ "\n",
+ "
\n",
+ "\n",
+ "Here, we again use $L_h$ and $R_h$ to indicate the partial contractions\n",
+ "\n",
+ "
\n",
+ "\n",
+ "and\n",
+ "\n",
+ "
\n",
+ "\n",
+ "where the transfer matrices $E^L_L$ and $E^R_R$ appearing in these expressions now contain only left-gauged and right-gauged MPS tensors $A_L$ and $A_R$ respectively.\n",
+ "\n",
+ "If we interpret the two terms appearing in the tangent space gradient as defining the effective Hamiltonians $H_{A_C}(\\bullet)$ and $H_C(\\bullet)$ such that\n",
+ "\n",
+ "$$\n",
+ "\\begin{align}\n",
+ "H_{A_C}(A_C) = A_C^\\prime \\\\\n",
+ "H_C(C) = C^\\prime ,\n",
+ "\\end{align}\n",
+ "$$\n",
+ "\n",
+ "we can characterize the variational optimum in terms of the fixed points of these operators. Indeed, since the tangent space gradient should be zero at the variational optimum, this point satisfies $A_C' = A_L C' = C' A_R$. This implies that the optimal MPS should obey the following set of equations,\n",
+ "\n",
+ "$$\n",
+ "\\begin{align}\n",
+ "H_{A_C}(A_C) \\propto A_C \\\\\n",
+ "H_C(C) \\propto C \\\\\n",
+ "A_C = A_L C = C A_R ,\n",
+ "\\end{align}\n",
+ "$$\n",
+ "\n",
+ "meaning that the optimal MPS should correspond to a fixed point of the effective Hamiltonians $H_{A_C}$ and $H_C$ and satisfy the mixed gauge condition. The VUMPS algorithm then consists of an iterative method for finding a set $\\{A_L, A_C, A_R, C\\}$ that satisfies these equations simultaneously."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9e980395-58fa-45d9-a651-3d9658a76191",
+ "metadata": {},
+ "source": [
+ "### Defining the required operators\n",
+ "\n",
+ "Similar to before, we again have to compute the contributions of the left and right environment terms $L_h$ and $R_h$ given above. We therefore require function handles defining the action of the left (resp. right) transfer matrix $E^L_L$ (resp. $E^R_R$) on a left (resp. right) matrix. To this end, we can simply reuse the implementations `EtildeLeft` and `EtildeRight` defined above, if we take into account that the left (resp. right) fixed point of $E^L_L$ (resp. $E^R_R$) is the identity while its right (resp. left) fixed point is precisely $C C^\\dagger$ (resp. $C^\\dagger C$). This last fact follows immediately from the mixed gauge condition."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "8e2d1edc-d570-4fa7-86c8-9651e4bdd736",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/LhMixed.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file LhMixed.m\n",
+ "function Lh = LhMixed(hTilde, Al, C, tol)\n",
+ " % Calculate Lh, for a given MPS in mixed gauge.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left-orthonormal.\n",
+ " % C : :class:`Tensor` (D, D)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right.\n",
+ " % tol : double, optional\n",
+ " % tolerance for gmres\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % Lh : :class:`Tensor` (D, D)\n",
+ " % result of contraction,\n",
+ " % ordered bottom-top.\n",
+ " \n",
+ " arguments\n",
+ " hTilde\n",
+ " Al\n",
+ " C\n",
+ " tol = 1e-3\n",
+ " end\n",
+ " \n",
+ " % construct fixed points for Al\n",
+ " l = C.eye(C.codomain, C.domain, 'Rank', [1, 1]); % left fixed point of left transfer matrix: left orthonormal\n",
+ " r = C * C'; % right fixed point of left transfer matrix\n",
+ " \n",
+ " % construct b\n",
+ " b = contract(Al, [4, 2, 1], Al, [1, 3, -2], conj(Al), [4, 5, 6], conj(Al), [6, 7, -1], hTilde, [2, 3, 5, 7], 'Rank', [1, 1]);\n",
+ " \n",
+ " % solve Ax = b for x\n",
+ " [Lh, ~] = linsolve(@(v) EtildeLeft(v, Al, l, r), b, 'Tol', tol);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "d5db7c95-44c1-4e59-8f2a-4510383f2275",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/RhMixed.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file RhMixed.m\n",
+ "function Rh = RhMixed(hTilde, Ar, C, tol)\n",
+ " % Calculate Rh, for a given MPS in mixed gauge.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right-orthonormal.\n",
+ " % C : :class:`Tensor` (D, D)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right.\n",
+ " % tol : double, optional\n",
+ " % tolerance for gmres\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % Rh : :class:`Tensor` (D, D)\n",
+ " % result of contraction,\n",
+ " % ordered top-bottom.\n",
+ " \n",
+ " arguments\n",
+ " hTilde\n",
+ " Ar\n",
+ " C\n",
+ " tol = 1e-3\n",
+ " end\n",
+ " \n",
+ " % construct fixed points for Ar\n",
+ " l = C' * C; % left fixed point of right transfer matrix\n",
+ " r = C.eye(C.codomain, C.domain, 'Rank', C.rank); % right fixed point of right transfer matrix: right orthonormal\n",
+ "\n",
+ " % construct b\n",
+ " b = contract(Ar, [-1, 2, 1], Ar, [1, 3, 4], conj(Ar), [-2, 7, 6], conj(Ar), [6, 5, 4], hTilde, [2, 3, 7, 5], 'Rank', [1, 1]);\n",
+ " \n",
+ " % solve Ax = b for x\n",
+ " [Rh, ~] = linsolve(@(v) EtildeRight(v, Ar, l, r), b, 'Tol', tol);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0cac621c-2d3d-4a44-90b5-07b6ddaa9224",
+ "metadata": {},
+ "source": [
+ "Next we implement the actions of the effective Hamiltonians $H_{A_C}$ and $H_{C}$ defined above,\n",
+ "\n",
+ "
\n",
+ "\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "c084408d-a4ab-41da-9054-9c9998300dd7",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/H_Ac.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file H_Ac.m\n",
+ "function H_AcV = H_Ac(v, hTilde, Al, Ar, Lh, Rh)\n",
+ " % Action of the effective Hamiltonian H_Ac on an input tensor.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % v : :class:`Tensor` (D, d, D)\n",
+ " % Tensor of size (D, d, D)\n",
+ " % hTilde : np.array (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left-orthonormal.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right-orthonormal.\n",
+ " % Lh : :class:`Tensor` (D, D)\n",
+ " % left environment,\n",
+ " % ordered bottom-top.\n",
+ " % Rh : :class:`Tensor` (D, D)\n",
+ " % right environment,\n",
+ " % ordered top-bottom.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % H_AcV : :class:`Tensor` (D, d, D)\n",
+ " % result of the action of H_Ac on the tensor v\n",
+ "\n",
+ " % first term\n",
+ " term1 = contract(Al, [4, 2, 1], v, [1, 3, -3], conj(Al), [4, 5, -1], hTilde, [2, 3, 5, -2], 'Rank', v.rank);\n",
+ "\n",
+ " % second term\n",
+ " term2 = contract(v, [-1, 2, 1], Ar, [1, 3, 4], conj(Ar), [-3, 5, 4], hTilde, [2, 3, -2, 5], 'Rank', v.rank);\n",
+ "\n",
+ " % third term\n",
+ " term3 = contract(Lh, [-1, 1], v, [1, -2, -3], 'Rank', v.rank);\n",
+ "\n",
+ " % fourth term\n",
+ " term4 = contract(v, [-1, -2, 1], Rh, [1, -3], 'Rank', v.rank);\n",
+ "\n",
+ " % sum\n",
+ " H_AcV = term1 + term2 + term3 + term4;\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "f9f5cd32-396e-43c3-8dd9-59c7f973a143",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/H_C.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file H_C.m\n",
+ "function H_CV = H_C(v, hTilde, Al, Ar, Lh, Rh)\n",
+ " % Action of the effective Hamiltonian H_C on an input tensor.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left-orthonormal.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right-orthonormal.\n",
+ " % Lh : :class:`Tensor` (D, D)\n",
+ " % left environment,\n",
+ " % ordered bottom-top.\n",
+ " % Rh : :class:`Tensor` (D, D)\n",
+ " % right environment,\n",
+ " % ordered top-bottom.\n",
+ " % v : :class:`Tensor` (D, D)\n",
+ " % Matrix of size (D, D)\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % H_CV : :class:`Tensor` (D, D)\n",
+ " % Result of the action of H_C on the tensor v.\n",
+ "\n",
+ " % first term\n",
+ " term1 = contract(Al, [5, 3, 1], v, [1, 2], Ar, [2, 4, 7], conj(Al), [5, 6, -1], conj(Ar), [-2, 8, 7], hTilde, [3, 4, 6, 8], 'Rank', v.rank);\n",
+ "\n",
+ " % second term\n",
+ " term2 = Lh * v;\n",
+ "\n",
+ " % third term\n",
+ " term3 = v * Rh;\n",
+ "\n",
+ " % sum\n",
+ " H_CV = term1 + term2 + term3;\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "34c368d9-2ea2-45c2-b1cd-c81986aee892",
+ "metadata": {},
+ "source": [
+ "### Implementing the VUMPS algorithm\n",
+ "\n",
+ "In order to find a set $\\{A_L^*, A_C^*, A_R^*, C^*\\}$ that satisfies the VUMPS fixed point equations given above, we use an iterative method in which each iteration consists of the following steps, each time starting from a given set $\\{A_L, A_C, A_R, C\\}$:\n",
+ "\n",
+ "1. Solve the eigenvalue equations for $H_{A_C}$ and $H_C$, giving new center tensors $\\tilde{A}_C$ and $\\tilde{C}$.\n",
+ "\n",
+ "2. From these new center tensors, construct a set $\\{\\tilde{A}_L, \\tilde{A}_R, \\tilde{A}_C, \\tilde{C}\\}$.\n",
+ "\n",
+ "3. Update the set of tensors $\\{A_L, A_C, A_R, C\\} \\leftarrow \\{\\tilde{A}_L, \\tilde{A}_C, \\tilde{A}_R, \\tilde{C}\\}$ and evaluate the norm of the gradient $\\varepsilon = \\left | \\left | H_{A_C} (A_C) - A_L H_C(C) \\right | \\right |$.\n",
+ "\n",
+ "4. If the norm of the gradient lies above the given tolerance, repeat."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "653db3da-2e3b-4a5a-bf10-b926e1746be7",
+ "metadata": {},
+ "source": [
+ "#### Updating the center tensors\n",
+ "\n",
+ "We start by defining a routine `calcNewCenter` which finds the new center tensors $\\tilde{A_C}$ and $\\tilde{C}$ by solving the eigenvalue problem defined by the effective Hamiltonians implemented above."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "a1be63ef-fc74-4704-87d8-c47c8e0e06db",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/calcNewCenter.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file calcNewCenter.m\n",
+ "function [AcTilde, CTilde] = calcNewCenter(hTilde, Al, Ar, C, Ac, Lh, Rh, tol)\n",
+ " % Find new guess for Ac and C as fixed points of the maps H_Ac and H_C.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left orthonormal.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right orthonormal.\n",
+ " % Ac : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauge.\n",
+ " % C : :class:`Tensor` (D, D)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right.\n",
+ " % Lh : :class:`Tensor` (D, D)\n",
+ " % left environment,\n",
+ " % ordered bottom-top.\n",
+ " % Rh : :class:`Tensor` (D, D)\n",
+ " % right environment,\n",
+ " % ordered top-bottom.\n",
+ " % tol : double, optional\n",
+ " % current tolerance\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % AcTilde : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauge.\n",
+ " % CTilde : :class:`Tensor` (D, D)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right.\n",
+ " \n",
+ " arguments\n",
+ " hTilde\n",
+ " Al\n",
+ " Ar\n",
+ " C\n",
+ " Ac\n",
+ " Lh = []\n",
+ " Rh = [];\n",
+ " tol = 1e-3\n",
+ " end\n",
+ " \n",
+ " % calculate left en right environment if they are not given\n",
+ " if isempty(Lh)\n",
+ " Lh = LhMixed(hTilde, Al, C, tol);\n",
+ " end\n",
+ " if isempty(Rh)\n",
+ " Rh = RhMixed(hTilde, Ar, C, tol);\n",
+ " end\n",
+ " \n",
+ " % calculate AcTilde\n",
+ " [AcTilde, ~] = eigsolve(@(v) H_Ac(v, hTilde, Al, Ar, Lh, Rh), Ac, 1, 'smallestreal', 'Tol', tol);\n",
+ " \n",
+ " % calculate CTilde\n",
+ " [CTilde, ~] = eigsolve(@(v) H_C(v, hTilde, Al, Ar, Lh, Rh), C, 1, 'smallestreal', 'Tol', tol);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "48efa0af-8086-46e7-bca7-36a3cbb8e0ee",
+ "metadata": {},
+ "source": [
+ "#### Extract a new set of mixed-gauge MPS tensors\n",
+ "\n",
+ "Once we have new center tensors, we can use these to construct a new set of mixed-gauge MPS tensors. To do this in a stable way, we will determine the global updates $\\tilde{A}_L$ and $\\tilde{A}_R$ as the left and right isometric tensors that minimize\n",
+ "\n",
+ "$$\n",
+ "\\begin{align}\n",
+ "\\varepsilon_L = \\min ||\\tilde{A}_C - \\tilde{A}_L \\tilde{C}||_2 \\\\\n",
+ "\\varepsilon_R = \\min ||\\tilde{A}_C - \\tilde{C} \\tilde{A}_L||_2 .\n",
+ "\\end{align}\n",
+ "$$\n",
+ "\n",
+ "This can be achieved in a robust and close to optimal way by making use of the left and right polar decompositions\n",
+ "\n",
+ "$$\n",
+ "\\begin{align}\n",
+ "\\tilde{A}_C = U^l_{A_C} P^l_{A_C}, \\qquad \\tilde{C} = U^l_{C} P^l_{C}, \\\\\n",
+ "\\tilde{A}_C = P^r_{A_C} U^r_{A_C} , \\qquad \\tilde{C} = P^r_{C} U^r_{C},\n",
+ "\\end{align}\n",
+ "$$\n",
+ "\n",
+ "to obtain\n",
+ "\n",
+ "$$ \\tilde{A}_L = U^l_{A_C} (U^l_C)^\\dagger, \\qquad \\tilde{A}_R = (U^r_C)^\\dagger U^r_{A_C}. $$\n",
+ "\n",
+ "In order to give the procedure some additional stability, we may also choose to use the $\\tilde{A}_L$ obtained with these polar decompositions to compute the tensors $\\tilde{A}_R$ and $\\tilde{A}_C$ by right orthonormalization of this $\\tilde{A}_L$. This approach ensures that the MPS satisfies the mixed gauge condition at all times, improving the overal stabilitiy of the VUMPS algorithm. This procedure is implemented in the `minAcC` routine."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "46b86ef2-570f-4fec-8513-48d4774afc2e",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/minAcC.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file minAcC.m\n",
+ "function [Al, Ar, C, Ac] = minAcC(AcTilde, CTilde, tol)\n",
+ " % Find Al and Ar corresponding to Ac and C, according to algorithm 5 in the lecture notes.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % AcTilde : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % new guess for center gauge. \n",
+ " % CTilde : :class:`Tensor` (D, D)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right,\n",
+ " % new guess for center gauge\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor zith 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left orthonormal.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor zith 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right orthonormal.\n",
+ " % Ac : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauge. \n",
+ " % C : :class:`Tensor` (D, D)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right,\n",
+ " % center gauge\n",
+ " \n",
+ " arguments\n",
+ " AcTilde\n",
+ " CTilde\n",
+ " tol = 1e-3\n",
+ " end\n",
+ " \n",
+ " % polar decomposition of Ac\n",
+ " [UlAc, ~] = leftorth(AcTilde, [1, 2], 3, 'polar');\n",
+ " \n",
+ " % polar decomposition of C\n",
+ " [UlC, ~] = leftorth(CTilde, 1, 2, 'polar');\n",
+ " \n",
+ " % construct Al\n",
+ " Al = (UlAc * UlC');\n",
+ " \n",
+ " % find corresponding Ar, C, and Ac through right orthonormalizing Al\n",
+ " [C, Ar] = rightOrthonormalize(Al, CTilde, tol);\n",
+ " C = C / norm(C);\n",
+ " Ac = contract(Al, [-1, -2, 1], C, [1, -3], 'Rank', Al.rank);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "6475ae4f-e039-42a8-9e5d-c58b0842feac",
+ "metadata": {},
+ "source": [
+ "#### Evaluating the norm of the gradient\n",
+ "\n",
+ "As a last step, we use the routine `gradientNorm` to compute the norm of the tangent space gradient in order to check if the procedure has converged."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "c83ae9fc-83cc-4df7-9549-9594ca81c92e",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/gradientNorm.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file gradientNorm.m\n",
+ "function nrm = gradientNorm(hTilde, Al, Ar, C, Ac, Lh, Rh)\n",
+ " % Calculate the norm of the gradient.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % hTilde : :class:`Tensor` (d, d, d, d)\n",
+ " % reduced Hamiltonian,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight,\n",
+ " % renormalized.\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor zith 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left orthonormal.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor zith 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right orthonormal.\n",
+ " % Ac : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor zith 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauge.\n",
+ " % C : :class:`Tensor` (D, D)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right.\n",
+ " % Lh : :class:`Tensor` (D, D)\n",
+ " % left environment,\n",
+ " % ordered bottom-top.\n",
+ " % Rh : :class:`Tensor` (D, D)\n",
+ " % right environment,\n",
+ " % ordered top-bottom.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % nrm : double\n",
+ " % norm of the gradient @Al, Ar, C, Ac\n",
+ " \n",
+ " % calculate update on Ac and C using maps H_Ac and H_c\n",
+ " AcUpdate = H_Ac(Ac, hTilde, Al, Ar, Lh, Rh);\n",
+ " CUpdate = H_C(C, hTilde, Al, Ar, Lh, Rh);\n",
+ " AlCupdate = contract(Al, [-1, -2, 1], CUpdate, [1, -3], 'Rank', AcUpdate.rank);\n",
+ " \n",
+ " nrm = norm(AcUpdate - AlCupdate);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dde6dc00-7848-4365-adab-a325a0af4a47",
+ "metadata": {},
+ "source": [
+ "Finally, this allows to implement the VUMPS algorithm."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "4cdf85b1-83cd-4ceb-b1be-7f4dc1e0614d",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/vumps.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file vumps.m\n",
+ "function [E, Al, Ar, C, Ac] = vumps(h, D, A0, tol)\n",
+ " % Find the ground state of a given Hamiltonian using VUMPS.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % h : :class:`Tensor` (d, d, d, d)\n",
+ " % Hamiltonian to minimize,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight.\n",
+ " % D : int\n",
+ " % Bond dimension\n",
+ " % A0 : :class:`Tensor` (D, d, D), optional\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % initial guess.\n",
+ " % tol : double, optional\n",
+ " % Relative convergence criterium.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % E : double\n",
+ " % expectation value @ minimum\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left orthonormal.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right orthonormal.\n",
+ " % Ac : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauge.\n",
+ " % C : :class:`Tensor` (D, D)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right.\n",
+ " \n",
+ " arguments\n",
+ " h\n",
+ " D\n",
+ " A0 = []\n",
+ " tol = 1e-4\n",
+ " end\n",
+ " \n",
+ " % if no initial guess, random one\n",
+ " if isempty(A0)\n",
+ " A0 = createMPS(D, h.dims(1));\n",
+ " end\n",
+ " \n",
+ " flag = true;\n",
+ " delta = 1e-5;\n",
+ " i = 0;\n",
+ " \n",
+ " % go to mixed gauge\n",
+ " [Al, Ar, C, Ac] = mixedCanonical(A0, [], [], delta/100);\n",
+ " while flag\n",
+ " i = i + 1;\n",
+ " \n",
+ " % regularize H\n",
+ " hTilde = reducedHamMixed(h, Ac, Ar);\n",
+ " \n",
+ " % calculate environments\n",
+ " Lh = LhMixed(hTilde, Al, C, delta/10);\n",
+ " Rh = RhMixed(hTilde, Ar, C, delta/10);\n",
+ " \n",
+ " % calculate norm\n",
+ " delta = gradientNorm(hTilde, Al, Ar, C, Ac, Lh, Rh);\n",
+ " \n",
+ " % check convergence\n",
+ " if delta < tol\n",
+ " flag = false;\n",
+ " end\n",
+ " \n",
+ " % calculate new center\n",
+ " [AcTilde, CTilde] = calcNewCenter(hTilde, Al, Ar, C, Ac, Lh, Rh, delta/10);\n",
+ " \n",
+ " % find Al, Ar from Ac, C\n",
+ " [AlTilde, ArTilde, CTilde, AcTilde] = minAcC(AcTilde, CTilde, delta/100);\n",
+ " \n",
+ " % update tensors\n",
+ " Al = AlTilde; Ar = ArTilde; C = CTilde; Ac = AcTilde;\n",
+ " \n",
+ " % print current energy\n",
+ " E = real(expVal2Mixed(h, Ac, Ar));\n",
+ " fprintf('iteration:\\t%d,\\tenergy:\\t%.12f\\tgradient norm\\t%.4e\\n', i, E, delta)\n",
+ " end\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "783e9d52-d92c-4b2d-99b2-dfedd9b94f42",
+ "metadata": {},
+ "source": [
+ "We can again test this implementation on the spin-1 Heisenberg antiferromagnet."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "f1a3809b-91d5-46b5-a1b0-0fa0d401b7e8",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Energy optimization using VUMPS:\n",
+ "iteration:\t1,\tenergy:\t-0.202362149000\tgradient norm\t8.5548e-01\n",
+ "iteration:\t2,\tenergy:\t-1.332868316483\tgradient norm\t1.1595e+00\n",
+ "iteration:\t3,\tenergy:\t-1.400163756833\tgradient norm\t4.5117e-01\n",
+ "iteration:\t4,\tenergy:\t-1.401087856469\tgradient norm\t5.0505e-02\n",
+ "iteration:\t5,\tenergy:\t-1.401270044661\tgradient norm\t2.3839e-02\n",
+ "iteration:\t6,\tenergy:\t-1.401307669639\tgradient norm\t1.6703e-02\n",
+ "iteration:\t7,\tenergy:\t-1.401343224227\tgradient norm\t1.5580e-02\n",
+ "iteration:\t8,\tenergy:\t-1.401367665850\tgradient norm\t1.3053e-02\n",
+ "iteration:\t9,\tenergy:\t-1.401379018822\tgradient norm\t7.3319e-03\n",
+ "iteration:\t10,\tenergy:\t-1.401380421344\tgradient norm\t2.4273e-03\n",
+ "iteration:\t11,\tenergy:\t-1.401380612479\tgradient norm\t8.6638e-04\n",
+ "iteration:\t12,\tenergy:\t-1.401380639024\tgradient norm\t3.0713e-04\n",
+ "iteration:\t13,\tenergy:\t-1.401380642830\tgradient norm\t1.1264e-04\n",
+ "iteration:\t14,\tenergy:\t-1.401380643411\tgradient norm\t4.1935e-05\n",
+ "\n",
+ "Time until convergence: 4.0988 s\n",
+ "Computed energy: -1.401380643411"
+ ]
+ }
+ ],
+ "source": [
+ "d = 3;\n",
+ "D = 12;\n",
+ "A = createMPS(D, d);\n",
+ "A = normalizeMPS(A);\n",
+ "\n",
+ "h = Heisenberg(-1, -1, -1, 0);\n",
+ "\n",
+ "% energy optimization using VUMPS\n",
+ "fprintf('Energy optimization using VUMPS:\\n')\n",
+ "t0 = tic;\n",
+ "[E, Al, Ar, C, Ac] = vumps(h, D, A, 1e-4);\n",
+ "fprintf('\\nTime until convergence: %.4f s\\n', toc(t0))\n",
+ "fprintf('Computed energy: %.12f', E)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1265a00d-5e94-42ce-afdc-b27909a97177",
+ "metadata": {},
+ "source": [
+ "Having obtained this ground state MPS, it is worthwile to have a look at its entanglement spectrum."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "71cd2d3d-ef3b-4b59-a755-3aa9093abbdc",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAjAAAAGkCAIAAACgjIjwAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAB3RJTUUH5gYUCgYgPFFlzwAAACR0RVh0U29mdHdhcmUATUFUTEFCLCBUaGUgTWF0aFdvcmtzLCBJbmMuPFjdGAAAACJ0RVh0Q3JlYXRpb24gVGltZQAyMC1KdW4tMjAyMiAxMjowNjozMqRWY1gAACAASURBVHic7d1/VFR1/sfxC8NIiuJBFEuTypTBI1qk6Fr++OpiVuaWmqvb6mqmnkXTLCX1rLkp1prZumc7tEZqK9uubWlmYJqzEmrij9ZQQwwGRw8CFmpmSDo7v75/3NM90wDjMDPM/dzr8/HXcOfO/bzvnXvvi3vv596JcLvdEgAAaotUuwAAACSJQAIACIJAAgAIgUACAAiBQAIACIFAAgAIgUACAAiBQAIACCFK7QJE5HA4rl+/3uhbUVFRt9xyS5jrCdJnn32Wk5MjSdKgQYPmzZundjnQht27d2/duvWHH36QJCkqKuof//iH2hW1lFdfffXYsWOSJM2bN2/QoEFql3NTI5AakZ+fP3bs2EbfGj16dH5+vp/TKS0tffXVVyVJioqK2rBhQ8jqayar1free+/JrzUUSIIsvWbRYs2NWr58+UsvvaT8GR0dreNA2r9//44dOyRJevzxxwMLpGC+d92sMyFBILWgmpqa3NxcSZKio6NZ1ZpLi0tPizU35HK5Xn75Zfn1s88+O3DgwKgodhS+BPO962OdCRXWsxuQ1xXF7bff3tSYLpdLkqTIyOZdlnM4HJGRkf58KrDp+zNZl8vlzx7H4XB4jXbDkvyfeKjmzs8WQ1V5w2XivyBnOYCvo2EBjc6jw+Gw2+3y6z//+c8hX+Wkxkr1f1UJuAn/3/UxzYCL9H9LD227GuNGA9u2bbvh8nn33Xe7dOnSpUuXjIyMQ4cODRgwQJIkg8EwePDg06dPu93uyZMnx8XFKdPp8pMjR44cOnTo4YcfbteuncFgkN/t2LHjtGnTKisr/Z++4vjx40OHDjUYDAaDYejQoSdPnpw8ebL82e3bt7vd7vXr18utTJo0SflUXV3d0qVL77zzTvmt1q1bz5gxo6qqShlh48aNSgE7duzo3r27JEkxMTErVqxwu93//e9/+/XrJ5c0atSompoaz5JuOPFgll6jX8eVK1eee+65hIQEeXyDwZCYmDhhwoSysjKv5goLC1NTU5XKvRbmDSt3u93Xrl1buXKlyWSSvz6DwZCamlpYWOi7Zs8aioqK5BrS09MzMzPl4ZmZmUoT/fr1kwcWFBQE/3U0XFaLFi1KTEyU64yOjn7ssceKi4vldzMyMrp06eI1F5MnT25qajdc/XzM+w2Lcbvdfi6fZm0v999/v/Lu8ePHR48eLbf+3nvvNbXEfKxdwWzpvtcZf9ZGnSGQGuEZSHU/Z7fb5XGUvbzJZIqOjpY89OrVy+12K2u5l3379ilXdLwkJCRcvHjRz+nLLBZL+/btPd+Ni4vr3bu35wbWMJBqa2t79erVaAEWi0UeJzs7Wx7Ys2dPr9GmT5/erl07zyH333+/UpI/Ew9m6TX6lTV1zc9sNns217NnT6PR6DlCly5dlGXuT+XfffedvD/1sn79et81KzXcc889MTEx8uv09PQZM2bIr2fMmKHMjhIJO3bsCPLr8FJbW6vs4Ly8//77brd70qRJDd8aPXp0o1PzZ/XzMe83LMbtdvu5fILZXpQvvalA8r12BbOl+/isP2uj/tDt+wba/dzmzZu9RigrKxs0aNDWrVuXLVsmDzl16tTevXsXLFiwaNEieYjRaMz9SVJS0u233/7GG2+cPHnS6XQ6nc5Tp07J+7ja2lplu7rh9OXXCxcuvHLliiRJiYmJ27dv37lz5y9+8YuTJ0/6nqnFixefOnVKkqRRo0ZVVVXZ7Xa51Nra2qlTp3qNbLFYJkyYsG3btjFjxshDNm7cGB8f/957761atUoeUlRUVF5eHsDEA1h6DWfH5XJ9/PHHkiT17t1bPji4du1aXl7e3LlzvfbUFotl4sSJn3766bvvviv/w1tTU/OnP/3J/8oXLlxYXFwsSVK7du3Wrl1bWFhoNptXrFgRGxvrZ83Hjx+PiYlZunTp2rVrR4wY4eNralRzvw4vCxcuPHv2rPTTCmM2m5UEevrppy9cuDB79uyNGzcq48uzsGDBgqam1qzVz2veb1iM/4tF4Xt7efHFFxsWLH/pTbnh2hXMlu7js83ajvRD7UQUkecRkpfc3Fx5HCU54uLi6uvr5YF9+vSRB/7rX/9yu91ms1n+Mzo6umErV65c2blz544dO3bs2PHcc8/JYyoHMf5M3263K6cCtm3bpkxWOQho9AjJbrcrI5w9e1apR/m/Uj4RofxLfuuttzqdTrfbLfdEksn/G7rdbuWimnx+xs+JB7/0PDmdTnk5dOnSZevWradOnZILVijNJSYmKgM9Z9DPyj3HeeuttxpW4qNmpQaDweD5H65yBDBr1ixloI8jpGZ9HV4861dWGKfTeeutt8oD5dXbZrMpU/ax2P1c/Zqadz+L8XP5+LNGOZ1OpUXlCKzRgj3dcO1yB7elN/pZP7cj/eEI6QZyf+6BBx7wGmHEiBFt2rSRXyunwh0Oh49pWq3WIUOGtG/f/uGHHx49evTo0aPXrl0rv+W5L7jh9CsrK51OpzzkoYcekl/ExsY2ek5Jcfz4ceWS9Z133hnxE6Xpr7/+2nP8IUOGyFdiPY82lH/tk5OT5RfyVeLmTjyApddQZGTko48+KklSTU3N+PHje/Xq1apVqyFDhqxbt85rzIEDByqvlQOXb775xuFw+FO55zi/+93vmlWk4sEHH+zRo0fD4V7nEpvSrK/DS3FxsVL/I488Ir+IjIxUFsvnn3/uTw2yAFY/z3lvbjF+Lh8fa1R1dbXSonJ8GRsb27dvXx8T9H/taqhZW7qn5m5HunETdNsIzpQpU3yP4Od24mnkyJFWq1WSpF69eg0aNCgyMrKioqKwsLC501f+P/Wi7CYa5XnPr9fZdv9baaqzUHMnHsDSa1Rubu6KFSs2b95cU1MjSZLT6fz8888///zzy5cvL1myxJ+m/ancc5yA+555XcNQeEbItWvXmvp4s74OL03VH1j3rQBWP895b24xfi4fH2tUfX19o8OVy1pNCWDtkjVrS/fU3O1INzhCCrfS0lJ5HTUYDEeOHNmwYcPbb789dOjQACbVrVs3ZfPbvn27/OLbb7+VbztvSlpamrIrOXfu3PUGfvWrXwVQTBgm7kNsbOyaNWuqq6ttNltBQYFyzUM++68oKSlRXldVVckvYmJioqKi/Kncc5xdu3aFdhaU3eW3334rPx8h5DwPEA8ePKi8lq+KSZJ0zz33+D+1wFa/gIsJfvl0795d+fqUFh0Ox+HDh31/0M+1y0swW7pa25HqCKQbmPlzs2fP9v+zyhZls9neeeedTz75ZPfu3Z7/6NXW1kqSVFFRIT/ap7kiIyMnTpwov37++ec3bdr00UcfjRs3zvcRUqtWrdLT0+XXc+bMUf4X++GHH955553hw4cHUElLTLzRpdfUyC+++OIXX3whFzB8+PDHHntMHu71r/SpU6eWL1/ucDi+/vrr5cuXywMnTJjgZ+WtWrVSzk1lZGR89tlnkiQ5HI4PP/xQvmzerJplSp+0Xbt2HTx4sLS0dPLkyb6/wYB5zuMLL7xw/vx5l8u1fPlyZb/ZVKevRgW2+jW3mBAuH/lUm/x68eLF8o5+zpw5vs+eSX6sXcFs6Y1+tkU3UqGpfRErEOXl5cuWLcvMzNy9e3dLTN9HpwblwmOjN/co27PS98FkMnl+vF27dm63W76JRJIko9GYkJAg39YgDxk7dmyzpl9ZWancHiFLSEhQVvGmun1bLBblU3INys0QHTt2lMdRrqIrn9q3b1/D1UbZbJTr0v5MPJil1yj5gkp0dHRCQkLHjh2V/y6XLl3q2ZzXspKnqVxm96fyhgtctn79et81NzrLbrf77NmzXue+Wrdu3bp1a/l1w04Nzf06vJSVlXne+OLplVdekcfxs1NDo0uj4ernY979KcbP5ePnGnXkyJGGZxqVniBNdfv2vXbJAt7Sm/qsP2uj/mjvCKmsrGz8+PGdO3fu16/fihUrNm3apHZFvnz44Yfp6eleZ4G3b98u32Fgt9svXbr0+9//fuXKlYFNv1u3bkeOHBk7dmx0dHR0dPT48eOPHDminIVv6tJCjx49jhw5MmHCBIPBYLfba2trL1++LElSnz59gn/YXQgn3ujSa9SIESOio6NtNlttbe3FixedTmdMTExmZqZyGKSM9tZbbynXDBITE3fs2KFcZven8m7dun355ZeTJ0/2vFZhNBpvu+225tYsu+OOOzZt2qSUdOedd3766adN7aaDl5SUJN+t6blf7t69e25uru/LIY0KbPVrVjGhXT5paWkffvihsqNPSEjYvn37DU9U+rN2BbOlN/rZFt1IhRXhdrvVrqF5Zs2a1b1798WLF0uStHfv3mefffbo0aNNXV8VWUlJyTfffDN48OAgHx/ucrk8t/zz589369ZNPqfx1VdfpaSk+P7sF198UVdX1759+9TU1NA+m6RFJ96oS5cuyVcgbr/99qSkJGWxbNiwQe49PGnSpM2bN7tcrgMHDrRv376p7lX+VO5yuY4ePXrlyhWvtgLjcDgOHDgQFxfnu8dXCP3vf/87ePCg3W7v3bu3kqYBCGb187+Y0C4fl8slXzcaOHCg/19cU2vXDQW5pYd/O1KR9gKpd+/eb7755rBhwyRJcrlcvXv3zsnJUU4N34RSUlKmTJnSp0+fqKioCxcuZGVllZWVSZI0ePDg/fv3q12dELwCSe1ydIXVDyGksbC9du2aw+G444475D8jIyPbtGnTQl2StKKiokI+XvRkMpmaemwJEEKsfgghjQWSfDzneR3VaDS2UJckrVizZs2+ffsqKirOnTt32223JScnjxo1asqUKa1atVK7NFHccccd8uPI0tLS1K5Fb1j9EEIqnLJzOp1FRUVWq7WkpMThcKxatcrrap7NZlu3bl1BQcHVq1eHDRuWkZHRqVMn+S273Z6SkvLPf/6zf//+8pB77733tddeGzlyZJjnAgAQWiocIR0+fHjGjBlGo7Fz585VVVXKT4EpZs2aVVpampGRERMTk5OTU1hYmJ+fLz8OxGg0dunSRb5fWpKkCxcuXLt2rdEHsQAAtEWFbt8mkyk/P7+kpGTu3LkN3927d++hQ4dWr149ffr0iRMn5ubmnj9/3vOHFMeOHbthwwb5Vol169alpqbedddd4aseANAyVDhCio+Pj4+Pb+rdvLy82NhY5Vbkrl27DhkyZNeuXUp6ZWRklJWVpaWltWvXLjY2NrBnHAAARCNcp4aKigqvK8+JiYn79+9XbncwGo3Z2dk//PDDlStXunXr5mNSnvc/y11RAQDCEi6QKisrvX4TMzk52eVy2Ww25XkhkiTFxsbGxsbecGrkEABohYiPDoqIiGg4UHM38AIAmkW4QGrbtq3nb4FIPz0o96b6URAAuAkJF0h9+vQ5c+aM5xCr1RoXF6fFp9UBAPwnXCANGDCgvLy8urpa/tPpdB44cCCw368DAGiIOoFkNpvNZrP885179uwxm83Kr0yOGzeuQ4cOmZmZNTU1dXV1S5Ys+f7772fOnBlYQ14/NAIAEJY6T/tumBPp6enKT5CdOHFi/vz58kFSXFxcVlZWYE8GMplM9LIDAK0Q9+cnLBZLfX193759A/6lGQIJADREuPuQFF53IwEA9E24Tg0AgJsTgQQAEAKBBAAQAoEEABCCzgOJ+5AAQCt0Hkh0+wYArdB5IAEAtIJAAgAIgUACAAiBQApKxIICXbYFAOFHIAXF/foIr5xoudgIZ1sAEH4EUrA8cyJiQYH79RH6aAsAwkzcp30HT74JKTw9v+WcCE9ChLMtAAgbnR8hcR8SAGiFzgMpPOSzZw2v8Wi9LQAIJwIpWJ7Xclo6J8LZFgCEmc6vIbX0Kbtw9iygFwMAfSOQAABC4JQdAEAIBBIAQAgEEgBACAQSAEAIOg8kfjEWALRC54FELzsA0AqdBxIAQCsIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQdB5IPDoIALRC54HEo4MAQCt0HkgAAK0gkAAAQiCQAABCIJAAAEIgkAAAQiCQAABCIJAAAEIgkAAAQiCQAABCIJAAAEIgkAAAQiCQAABC0Hkg8bRvANAKnQcST/sGAK3QeSABALSCQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAhB54HEL8YCgFboPJD4xVgA0AqdBxIAQCsIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQdB5IJpNJ7RIAAH7ReSCVlZWpXQIAwC86DyQAgFYQSAAAIRBIAAAhEEgAACEQSAAAIRBIAAAhEEgAACEQSAAAIRBIAAAhEEgAACEQSAAAIRBIAAAhEEgAACEQSAAAIRBIAAAhEEgAACEQSAAAIRBIAAAhEEgAACEQSAAAIRBIAAAhEEgAACEQSAAAIRBIAAAhEEgAACEQSAAAIRBIAAAhEEgAACEQSAAAIRBIAAAhEEgAACEQSAAAIUSpXUAgXC7Xl19+WVVV5XQ6x48fr3Y5AIAQ0GQgLVu2bOfOnXfffXdpaSmBBAD6oMlTdn/84x+PHj06Z84ctQsBAISMJgPJaDSqXQIAIMQ0GUgAAP3RwDUkl8vldDrl1xwbAYBehSmQnE5nUVGR1WotKSlxOByrVq2Kjo72HMFms61bt66goODq1avDhg3LyMjo1KmT/Nbu3bsXLlwovy4uLiaTAECXwhRIhw8fnjFjhtFo7Ny5c1VV1csvv+w1wqxZs0pLSzMyMmJiYnJycgoLC/Pz89u0aSNJ0kMPPfTQQw+Fp04AgFrCdA3JZDLl5+eXlJTMnTu34bt79+49dOjQ6tWrp0+fPnHixNzc3PPnz2/YsKGpqblcLrvd7nA4JEmy2+12u70FSwcAhEWYAik+Pr5nz55NvZuXlxcbGzt8+HD5z65duw4ZMmTXrl1Njb9z586UlJTZs2fb7faUlJSUlJSmMsn0kyDrBwC0NCE6NVRUVKSlpXkOSUxM3L9/v8vlioxsJDJHjx49evRof6ZcVlYWmhIBAC1MiG7flZWVMTExnkOSk5NdLpfNZlOrJABAmAkRSJIkRURENBzodrvDXwkAQBVCBFLbtm2vX7/uOaS2tlaSJK+u4QAAHRMikPr06XPmzBnPIVarNS4uzmAwqFUSACDMhAikAQMGlJeXV1dXy386nc4DBw4MHTpU3aoAAOEUvkAym81ms7mkpESSpD179pjN5mPHjslvjRs3rkOHDpmZmTU1NXV1dUuWLPn+++9nzpwZfKN0+AYArYgIW8eBhtmQnp6enZ0tvz5x4sT8+fPlg6S4uLisrKyRI0cG3yLdvgMTsaDA/foI2tJWc4DWhS+Q/GGxWOrr6/v27dvo7UfNRSAFw2tn2qL7Vr22Ff7mAE0TK5BCi0AKkrL3DMNuVK9thb85QLsIJPgSsaBAkqTw7Eb12lb4mwM0SohedgAAEEhoknyKyf36CPkffNrSRHOAduk8kOj2HTDPCx4tvTPVa1vhbw7QNK4hoRF67YpNt29AZAQSAEAIOj9lBwDQCgIJACAEAgkAIAQCCQAgBAIJACAEAgkAIASdBxI3xgKAVug8kLgPCQC0QueBBADQCgIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBJ0HEjfGAoBW6DyQuDEWALRC54EEANAKAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEnQcSz7IDAK3QeSDxLDsA0AqdBxIAQCsIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQdB5IPO0bALRC54HE074BQCt0HkgAAK0gkAAAQiCQAABCIJAAAEIgkAAAQiCQAABCIJAAAEIgkAAAQiCQAABCIJAAAEIgkAAAQiCQAABCIJAAAEIgkAAAQiCQAABCIJAAAELQeSDxi7EAoBU6DyR+MRYAtELngQQA0AoCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBJ0HkslkUrsEAIBfdB5IZWVlapcAAPCLzgMJAKAVBBIAQAgEEqAHEQsKdNkWbioEEqAH7tdHeOVEy8VGONvCTYVAAnTCMyciFhS4Xx+hj7Zw84hwu91q19BSTCYTvexws5FzIjwJEc62cDPgCAkAIAQCCdAP+exZw2s8Wm8LNwkCCdAJz2s5LZ0T4WwLNw+uIQF6EM6eBfRiQAshkAAAQuCUHQBACAQSAEAIBBIAQAgEEgBACAQSAEAIBBIAQAgEEgBACAQSAEAIBBIAQAgEEgBACAQSAEAIBBIAQAgEEgBACAQSAEAIBBIAQAgEEgBACAQSAEAIBBIAQAgEEgBACAQSAEAIBBIAQAhRahcQCIvFYjabz5w506ZNm8cee+y+++5TuyIAQLA0eYT05JNPnjlzZuDAgUajccqUKdu2bVO7IgBAsDR5hLRnz57Y2Fj5dfv27d98882xY8eqWxIAIEiaPEJS0kiSpISEBIfDoWIxAICQ0GQgKex2e25u7rhx49QuBAAQLA2csnO5XE6nU35tNBo931qwYEHHjh1nz56tRl0AgFAKUyA5nc6ioiKr1VpSUuJwOFatWhUdHe05gs1mW7duXUFBwdWrV4cNG5aRkdGpUyf5rd27dy9cuFB+XVxcrGTSwoULa2trN27caDAYwjMXAICWE+F2u8PQTFFR0VNPPWU0Gjt37lxVVVVcXNymTRvPEaZOnVpaWpqRkRETE5OTk+N2u/Pz873G8fTCCy+Ul5fn5uZ6Xk/yYjKZysrKQjkbAIAWE6ZrSCaTKT8/v6SkZO7cuQ3f3bt376FDh1avXj19+vSJEyfm5uaeP39+w4YNTU1t6dKlX3311dtvv926dWu73W6321uydgBAOITplF18fHx8fHxT7+bl5cXGxg4fPlz+s2vXrkOGDNm1a1ej6SVJ0gcffCBJ0uDBg+U/jUZjSUlJqEsGAISVEJ0aKioq0tLSPIckJibu37/f5XJFRjZyDOf/iTiTydTcjwAAVCFEIFVWVvbs2dNzSHJyssvlstlsrVu3DmbK5BAAaIUo9yFFREQ0HBieDhcAABEIEUht27a9fv2655Da2lpJkry6hgMAdEyIQOrTp8+ZM2c8h1it1ri4OG4wAoCbhxCBNGDAgPLy8urqavlPp9N54MCBoUOHqlsVACCcwhdIZrPZbDbL/bP37NljNpuPHTsmvzVu3LgOHTpkZmbW1NTU1dUtWbLk+++/nzlzZthqAwCoLkxPapA8emAr0tPTs7Oz5dcnTpyYP3++fJAUFxeXlZU1cuTIkLRIRzsA0ITwBZI/LBZLfX193759G739qLl4dBAAaIgQ9yEpvO5GAgDcPITo1AAAAIEEABACgQQAEAKBBAAQgs4DqWFfcwCAmHQeSHT7BgCt0HkgAQC0gkACAAiBQAIACIFAAgAIgUACAAiBQAIACEHngcR9SACgFToPJO5DAgCt0HkgAQC0gkACAAiBQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAhB54HEkxoAQCt0Hkg8qQEAtELngQQA0AoCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEAgkAIAQCCQAgBAIJACAEnQcSjw4CAK3QeSDx6CAA0AqdBxIAQCsIJACAEAgkAIAQCCQAgBAIJACAEAgk7dFrX3a9zpek31kLw3xFLCho6SYatqWz+ZLCO2vBIJAAiMv9+givfXfL7cr12lb4mwsYgQRAaJ4704gFBe7XR9CW+M0FJsLtdqtdQ0sR/OAUgP/KH/2bJElJ+Rm0FXxzYqaRJElRahfQgnhMA6Ab8n/34dmo9dqWJOqZOgWn7ACITj7F1PBCCG2J3FwACCQAQvO84NHSO1O9thX+5gKj52tIALQunJff9dpW+JsLGIEEABACp+wAAEIgkAAAQiCQAABCIJAAAELQ4Y2xNptt3bp1BQUFV69eHTZsWEZGRqdOndQuKlgWiyUvL6+kpKS0tLRHjx4DBw58+umn27Rpo3ZdIbZt27Z9+/b179//t7/9rdq1hIbFYnn77bdPnDgRERGRnJz8zDPP3H333WoXFSyLxbJx48Zjx47V19enpqY++eSTAwcOVLuoQDidzqKiIqvVWlJS4nA4Vq1aFR0d7TmCRncmvudL5J2JDnvZTZ06tbS0NCMjIyYmJicnx+125+fnC7K4AzZr1qzTp0/ff//999xzz/Hjx7ds2WIymT744AOj0ah2aSFz9uzZxx9/3GazjRkzZvXq1WqXEwKfffbZM888YzKZHnnkkcjIyK+++mrEiBFjxoxRu66gnD59esKECZ07d542bVp0dPT7779/9OjRnJycYcOGqV1asxUVFT311FNGo7Fz585VVVXFxcVeOwqN7kx8z5fQOxO3vhQWFiYlJRUUFMh/VlVVJScn//Wvf1W3quCVl5d7/rl9+/akpKT8/Hy16mkJv/nNb/7yl7/07t07MzNT7VpC4LvvvrvvvvsWLFigdiEhlpWVlZycXFtbK//pcDj+7//+LyMjQ92qAnPx4kV5y9q2bVtSUlJ9fb3nu9rdmfieL5F3Jnq7hpSXlxcbGzt8+HD5z65duw4ZMmTXrl3qVhW8nj17ev758MMPS5JUWlqqUjmht2nTpu+++2727NlqFxIyW7du/fHHBAt/HwAABNxJREFUHxctWqR2ISF29epVg8GgnLkyGAx33XWXw+FQt6rAxMfHe21ZnrS7M/E9XyLvTPQWSBUVFWlpaZ5DEhMTrVary+VSq6SWcPToUUmS7r33XrULCY3q6uq1a9euXLlSiJMGIVJcXCz/c5qVlTVv3rysrCyLxaJ2USHw61//2m63K/vlkydPHj58+IEHHlC3qpbAziT89BZIlZWVMTExnkOSk5NdLpfNZlOrpJC7du3a8uXLe/To8ctf/lLtWkLjD3/4w6OPPtq/f3+1Cwml8vJyh8Mxbty4ixcvxsXFHTp06PHHHz906JDadQXrvvvuy8nJeeWVV8aMGfPEE09MmzZtyZIlU6dOVbuu0GNnEn467GUXERHRcKBbL303XC7XvHnzLl++vHnz5shIPfw/8e9///v06dNvvPGG2oWEWF1d3eXLlxctWjR9+nRJkmw227hx45YvX75z5061SwvKpUuXsrOzjUbjgw8+GBMT88knn2zcuDEtLU2XPz/GziTMhCgihNq2bXv9+nXPIbW1tZIkefXm1K65c+d++eWXOTk5d911l9q1hMCFCxdeffXVxYsX33LLLXa73W63S5LkdrvtdrvWT4zI3bunTJki/xkdHT127Fir1frjjz+qWlew1qxZc+7cuS1btsydO3f69Olbtmzp0KHDCy+8oHZdocfOJPz0Fkh9+vQ5c+aM5xCr1RoXF2cwGNQqKYSeffbZgwcPrl+/vm/fvmrXEhoWi6W+vv75559P+Yndbv/4449TUlL279+vdnVBue222yIjIz2visXHx0uSdOnSJfWKCoHCwsIBAwbExcUpQ4YOHfr11187nU4Vq2oJ7EzCT2+n7AYMGPCf//ynurq6a9eukiQ5nc4DBw4MHTpU7bpC4Lnnntu7d+/69etTU1PVriVkevXqlZOT4zlk9uzZ/fv3nz59ekpKilpVhcQDDzyQl5dnsViUTk1HjhyJioqS10ztSkhIOH/+vOeQc+fORUZGNnp2S9PYmYSf4aWXXlK7hlC6++67t2zZ8sUXXwwaNEiSpGXLlp04cWLNmjXyP6fatXLlyq1bt06aNKljx47Wn9hsNk3cN+5D69at7/y5v/3tb6mpqTNnzhT/9kPfkpKS8vLy9u3bN2DAgPbt23/00UfZ2dlTpkzR+h7t8uXL27dvt9ls9957b2Rk5AcffLB+/fpRo0bJvYc1x2w2W63W4uLiEydO9OzZs7Kysq6u7tZbb5U0vjPxMV8i70x0+KSGEydOzJ8/v7q6WpKkuLi4rKyskSNHql1UsKZNm3bw4EGvgRMmTFi5cqUq9bSclJSURx55RB9Paqiurp4zZ86pU6ckSYqMjHziiSdeeuklHZzwee211/7+978r9x6NGjVq5cqVsbGx6lYVmIZ9MdLT07Ozs+XX2t2Z+JgvkXcmOgwkmXxxom/fvoL0HsFNq7q6+ty5c6mpqbq5GC5JksvlOn78uM1m69evn57uHmsUO5Ow0W0gAQC0hcAHAAiBQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAiBQAIACIFAAgAIgUACAAiBQAIACOH/AfMYud3dH/qtAAAAAElFTkSuQmCC\n",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "[~, S, ~] = tsvd(C); % singular values of center matrix give entanglement spectrum\n",
+ "S = diag(double(S));\n",
+ "scatter(1:D, S, 'Marker', 'x')\n",
+ "set(gca, 'YScale', 'log')\n",
+ "title('Entanglement spectrum of ground state')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "74755516-8b37-4163-aa8c-eb4f84670a5d",
+ "metadata": {},
+ "source": [
+ "We can clearly see that the entanglement spectrum consists of degenerate groups, which reflects an underlying symmetry in the ground state of the spin-1 Heisenberg antiferromagnet."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "45a4543f-a99c-425b-9d9c-e37ff5e2b771",
+ "metadata": {},
+ "source": [
+ "## 5 Elementary excitations\n",
+ "\n",
+ "### Quasiparticle ansatz\n",
+ "\n",
+ "The methods described above can be extended beyond computing the ground state. We briefly discuss how one can also study excitations on top of a given ground state. For this, we introduce the MPS quasiparticle ansatz, given by\n",
+ "\n",
+ "
\n",
+ "\n",
+ "This ansatz cosists of defining a new state by changing one $A$ tensor of the ground state at site $n$ and taking a momentum superposition.\n",
+ "\n",
+ "Before describing how to optimize the tensor $B$, it is worthwile to investigate the corresponding variational space in a bit more detail. First, we note that this excitation ansatz can be interpreted as nothing more than a boosted version of a tangent vector to the MPS manifold. In particular, this means that we will be able to apply all kinds of useful tricks and manipulations to the tensor $B$ (cfr. the [lecture notes](https://doi.org/10.21468/SciPostPhysLectNotes.7) for an introduction to tangent vectors and their properties). For example, we can see that $B$ has gauge degrees of freedom, as the corresponding excited state is invariant under an additive gauge transformation of the form\n",
+ "\n",
+ "
\n",
+ "\n",
+ "where $Y$ is an arbitrary $D \\times D$ matrix. This gauge freedom can be eliminated, thereby removing the zero modes in the variational subspace, by imposing a *left gauge-fixing condition*\n",
+ "\n",
+ "
\n",
+ "\n",
+ "If we parametrize the tensor $B$ as\n",
+ "\n",
+ "
\n",
+ "\n",
+ "where $V_L$ is the $ D \\times d \\times D(d-1)$ tensor corresponding to the $D(d-1)$-dimensional null space of $A_L$ satisfying\n",
+ "\n",
+ "
\n",
+ "\n",
+ "then the gauge condition is automatically satisfied. In particular, this fixing of the gauge freedom ensures that the excitation is orthogonal to the ground state,\n",
+ "\n",
+ "
\n",
+ "\n",
+ "In this form, we have put forward an ansatz for an excited state characterized by a single $D(d-1) \\times D$ matrix $X$ such that\n",
+ "\n",
+ "1. All gauge degrees of freedom are fixed.\n",
+ "2. All zero modes in the variational subspace are removed.\n",
+ "3. Calculating the norm becomes straightforward.\n",
+ "4. The excitation is orthogonal to the ground state."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f730a096-db4e-4a81-a9cc-76020c4a2084",
+ "metadata": {},
+ "source": [
+ "### Solving the eigenvalue problem\n",
+ "\n",
+ "Having introduced an excitation ansatz which has all the right properties and is defined in terms of a single matrix $X$, all that is left to do is to minimize the energy function,\n",
+ "\n",
+ "$$ \\min_{X} \\frac{\\left \\langle \\Phi_p(X) \\middle | H \\middle | \\Phi_p(X) \\right \\rangle}{\\left \\langle \\Phi_p(X) \\middle | \\Phi_p(X) \\right \\rangle}. $$\n",
+ "\n",
+ "As both the numerator and the denominator are quadratic functions of the variational parameters $X$, this optimization problem reduces to solving a generalized eigenvalue problem\n",
+ "\n",
+ "$$ H_{\\text{eff}}(q) X = \\omega N_{\\text{eff}}(q) X, $$\n",
+ "\n",
+ "where the effective energy and normalization matrices are defined as\n",
+ "\n",
+ "$$\n",
+ "\\begin{align}\n",
+ "& 2\\pi\\delta(p-p') (\\boldsymbol{X'})^\\dagger H_{\\text{eff}}(q) \\boldsymbol{X} = \\left \\langle \\Phi_{p'}(X') \\middle | H \\middle | \\Phi_p(X) \\right \\rangle \\\\\n",
+ "& 2\\pi\\delta(p-p') (\\boldsymbol{X'})^\\dagger N_{\\text{eff}}(q) \\boldsymbol{X} = \\left \\langle \\Phi_{p'}(X') \\middle | \\Phi_p(X) \\right \\rangle,\n",
+ "\\end{align}\n",
+ "$$\n",
+ "\n",
+ "and $\\boldsymbol{X}$ denotes a vectorized version of the matrix $X$. Since the overlap between two excited states is of the simple Euclidean form (cfr. the [lecture notes](https://doi.org/10.21468/SciPostPhysLectNotes.7)), the effective normalization matrix reduces to the unit matrix, and we are left with an ordinary eigenvalue problem.\n",
+ "\n",
+ "To solve this eigenvalue problem, we need to find an expression for $H_{\\text{eff}}$, or rather of the action thereof on a trial vector $\\boldsymbol{Y}$. In order to find this action we first transform the vector $\\boldsymbol{X}$ into a tensor $B$ by contracting its corresponding matrix with the right leg of $V_L$, and then compute all different contributions that pop up in a matrix element of the form $\\left \\langle \\Phi_p(B') \\middle | H \\middle | \\Phi_p(B) \\right \\rangle$. This procedure is similar to what we have done when computing the gradient above, where we now need to take into account all different positions of the nearest-neighbor operator $h$ of the Hamiltonian, the input tensor $B$ and the output. Though slightly more involved than before, we can again define the following partion contractions\n",
+ "\n",
+ "
\n",
+ "\n",
+ " \n",
+ "\n",
+ "
\n",
+ "\n",
+ " \n",
+ "\n",
+ "
\n",
+ "\n",
+ " \n",
+ "\n",
+ "
\n",
+ "\n",
+ " \n",
+ "\n",
+ "
\n",
+ "\n",
+ " \n",
+ "\n",
+ "
\n",
+ "\n",
+ "Using these partial contractions, we find the action of the effective energy matrix on a given input tensor $B(Y)$ as\n",
+ "\n",
+ "
\n",
+ "\n",
+ "In the last step, we need the action of $H_{\\text{eff}}(p)$ on the vector $\\boldsymbol{Y}$, so we need to perform a last contraction\n",
+ "\n",
+ "
\n",
+ "\n",
+ "The total procedure is implemented in the routine `quasiParticle`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "id": "12b3c911-c3b4-429b-9a0b-168de1f7e0d9",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/quasiParticle.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file quasiParticle.m\n",
+ "function [x, e] = quasiParticle(h, Al, Ar, C, Ac, p, num, tol)\n",
+ "\n",
+ " % renormalize hamiltonian and find left and right environments\n",
+ " hTilde = reducedHamMixed(h, Ac, Ar);\n",
+ " Lh = LhMixed(hTilde, Al, C, tol);\n",
+ " Rh = RhMixed(hTilde, Ar, C, tol);\n",
+ " \n",
+ " % find reduced parametrization\n",
+ " Vl = leftnull(Al, [1, 2], 3);\n",
+ " \n",
+ " % solve eigenvalue problem\n",
+ " x0 = Vl.randnc([Vl.dims(3), Al.dims(3)], 'Rank', [1, 1]);\n",
+ " [x, e] = eigsolve(@ApplyHeff, x0, num, 'smallestreal', 'Tol', tol);\n",
+ " e = diag(e);\n",
+ " \n",
+ " function y = ApplyHeff(x)\n",
+ " \n",
+ " B = contract(Vl, [-1, -2, 1], x, [1, -3], 'Rank', Al.rank);\n",
+ " \n",
+ " % right disconnected\n",
+ " right = contract(B, [-1, 2, 1], conj(Ar), [-2, 2, 1], 'Rank', [1, 1]);\n",
+ " [right, ~] = linsolve(@(x) ApplyELR(x, p), right, [], [], [], 'Tol', tol);\n",
+ " \n",
+ " % left disconnected\n",
+ " left = contract(Lh, [1, 2], B, [2, 3, -2], conj(Al), [1, 3, -1]) + ...\n",
+ " contract(Al, [1, 2, 4], B,[4, 5, -2], conj(Al),[1, 3, 6], conj(Al), [6, 7, -1], hTilde, [3, 7, 2, 5]) + ...\n",
+ " exp(-1i*p) * contract(B, [1, 2, 4], Ar, [4, 5, -2], conj(Al), [1, 3, 6], conj(Al), [6, 7, -1], hTilde, [3, 7, 2, 5]);\n",
+ " left = repartition(left, [1, 1]);\n",
+ " [left, ~] = linsolve(@(x) ApplyERL(x, p), left, [], [], [], 'Tol', tol);\n",
+ " \n",
+ " y = contract(B, [-1, 2, 1], Ar, [1, 3, 4], conj(Ar), [-3,5,4], hTilde, [-2, 5, 2, 3]) + ...\n",
+ " exp(1i*p) * contract(Al, [-1, 2, 1], B, [1, 3, 4], conj(Ar), [-3, 5, 4], hTilde, [-2, 5, 2, 3]) + ...\n",
+ " exp(-1i*p) * contract(B, [4,3,1], Ar, [1, 2, -3], conj(Al), [4, 5, -1], hTilde, [5, -2, 3, 2]) + ...\n",
+ " contract(Al, [4, 3, 1], B, [1, 2, -3], conj(Al), [4, 5, -1], hTilde,[5,-2,3,2]) + ...\n",
+ " exp(1i*p) * contract(Al, [1, 2, 4], Al, [4, 5, 6], conj(Al), [1, 3, -1], right, [6, -3], hTilde, [3, -2, 2, 5]) + ...\n",
+ " exp(2*1i*p) * contract(Al, [-1, 6, 5], Al, [5, 3, 2], conj(Ar), [-3, 4, 1],right, [2, 1], hTilde, [-2, 4, 6, 3]) + ...\n",
+ " contract(Lh, [-1, 1], B, [1, -2, -3]) + ...\n",
+ " contract(B, [-1,-2,1], Rh, [1, -3]) + ...\n",
+ " exp(-1i*p) * contract(left, [-1,1], Ar,[1,-2,-3]) + ...\n",
+ " exp(+1i*p) * contract(Lh, [-1,1], Al, [1, -2, 2], right, [2, -3]);\n",
+ " \n",
+ " y = contract(y, [1, 2, -2], conj(Vl), [1, 2, -1], 'Rank', x.rank);\n",
+ " \n",
+ " function y = ApplyELR(x, p)\n",
+ " overlap = contract(conj(C), [1, 2], x, [1, 2]);\n",
+ " y = contract(Al, [-1, 3, 1], conj(Ar), [-2, 3, 2], x, [1, 2], 'Rank', x.rank);\n",
+ " y = x - exp(1i*p) * (y - overlap * C);\n",
+ " end\n",
+ "\n",
+ " function y = ApplyERL(x, p)\n",
+ " overlap = contract(conj(C), [1, 2], x, [1, 2]);\n",
+ " y = contract(x, [1, 2], Ar, [2, 3, -2], conj(Al), [1, 3, -1], 'Rank', x.rank);\n",
+ " y = x - exp(1i*p) * (y - overlap * C);\n",
+ " end\n",
+ " \n",
+ " end\n",
+ " \n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8f2b8df5-0ec2-487e-beff-d125b24b90d8",
+ "metadata": {},
+ "source": [
+ "We can use this to compute the Haldane gap on top of the ground state of the spin-1 Heisenberg antiferromagnet we have just obtained using VUMPS."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "id": "880f4919-36e1-4003-b004-35e80596f7d9",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "First triplet: 0.40938, 0.40938, 0.40938\n"
+ ]
+ }
+ ],
+ "source": [
+ "p = pi;\n",
+ "num = 3;\n",
+ "tol = 1e-12;\n",
+ "[x, e] = quasiParticle(h, Al, Ar, C, Ac, p, num, tol);\n",
+ "fprintf('First triplet: %s\\n', join(string(real(e)), ', '));"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "50d79755-67c1-48de-99ef-aeba8af6d627",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Matlab",
+ "language": "matlab",
+ "name": "matlab"
+ },
+ "language_info": {
+ "codemirror_mode": "octave",
+ "file_extension": ".m",
+ "help_links": [
+ {
+ "text": "MetaKernel Magics",
+ "url": "https://metakernel.readthedocs.io/en/latest/source/README.html"
+ }
+ ],
+ "mimetype": "text/x-octave",
+ "name": "matlab",
+ "version": "0.17.1"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/src/examples/uniformMps/uniformMps.ipynb b/docs/src/examples/uniformMps/uniformMps.ipynb
new file mode 100644
index 0000000..9688854
--- /dev/null
+++ b/docs/src/examples/uniformMps/uniformMps.ipynb
@@ -0,0 +1,1606 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "ad36b914-421f-4c19-a45b-66194aa40a79",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "addpath(genpath('../../../../src'))\n",
+ "relTol = 1e-12;"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d28d1943-c20b-4838-90aa-7763d10e0d47",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "# Matrix product states in the thermodynamic limit\n",
+ "\n",
+ "This notebook briefly demonstrates the implementation of uniform matrix product states (MPS) directly in the thermodynamic limit, using the `Tensor` backend. It is based on the second chapter of the [lecture notes on tangent space methods for uniform MPS](https://doi.org/10.21468/SciPostPhysLectNotes.7) by Laurens Vanderstraeten, Jutho Haegeman and Frank Verstraete.\n",
+ "\n",
+ "The contents of this notebook mirror that of a tutorial given at the [2020 school on Tensor Network based approaches to Quantum Many-Body Systems](http://quantumtensor.pks.mpg.de/index.php/schools/2020-school/) held in Bad Honnef, Germany, which can be found [here](https://github.com/leburgel/BadHonnefTutorial).\n",
+ "\n",
+ "## 1 Normalization\n",
+ "We start by considering a uniform MPS in the thermodynamic limit, which is defined as \n",
+ "$$\\left | \\Psi(A) \\right \\rangle = \\sum_{\\{i\\}} \\boldsymbol{v}_L^\\dagger \\left[ \\prod_{m\\in\\mathbb{Z}} A^{i_m} \\right] \\boldsymbol{v}_R \\left | \\{i\\} \\right \\rangle.$$\n",
+ "\n",
+ "Here, $\\boldsymbol{v}_L^\\dagger$ and $\\boldsymbol{v}_R$ represent boundary vectors at infinity and the $A^i$ are complex matrices of size $D \\times D$ for every entry of the index $i$. This allows for the interpretation of the object $A$ as a three-legged tensor of dimensions $D\\times d \\times D$, where we will refer to $D$ as the bond dimension and $d$ as the physical dimension. With this object and the diagrammatic language of tensor networks, we can represent the state as\n",
+ "\n",
+ "
\n",
+ "\n",
+ "Thus, we initialize and represent a uniform MPS state as follows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "47f2554d-5c1e-4e7f-a83e-ea876791a3d3",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/createMPS.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file createMPS.m\n",
+ "function A = createMPS(D, d)\n",
+ " % Returns a random complex MPS tensor.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % D : int\n",
+ " % Bond dimension for MPS.\n",
+ " % d : int\n",
+ " % Physical dimension for MPS.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " A = Tensor.randnc([D, d, D]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "38426af0-f06c-4d71-baa0-0ba012c5f6db",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "d = 3;\n",
+ "D = 5;\n",
+ "A = createMPS(D, d);\n",
+ "\n",
+ "assert(isequal(A.dims, [D, d, D]), 'Generated MPS tensor has incorrect shape.')\n",
+ "assert(~isreal(A), 'MPS tensor should have complex values.')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9d0f0e08-7a05-4fd6-8c6d-b1107fc2eb3c",
+ "metadata": {},
+ "source": [
+ "One of the central objects in any MPS calculation is the transfer matrix, defined in our case as\n",
+ "\n",
+ "
\n",
+ "\n",
+ "This object corresponds to an operator acting on the space of $D \\times D$ matrices, and can be interpreted as a 4-leg tensor. We will use the following convention for ordering the legs:\n",
+ "1. top left\n",
+ "2. bottom left\n",
+ "3. top right\n",
+ "4. bottom right\n",
+ "\n",
+ "The transfer matrix can be shown to be a completely positive map, such that its leading eigenvalue is a positive number. This eigenvalue should be rescaled to one to ensure a proper normalization of the state in the thermodynamic limit. To perform this normalization, we must therefore find the left and right fixed points $l$ and $r$ which correspond to the largest eigenvalues of the eigenvalue equations\n",
+ "\n",
+ "
\n",
+ "\n",
+ "Normalizing the state then means rescaling the MPS tensor $A \\leftarrow A / \\sqrt{\\lambda}$. Additionally, we may fix the normalization of the eigenvectors by requiring that their trace is equal to one:\n",
+ "\n",
+ "
\n",
+ "\n",
+ "With these properties in place, the norm of an MPS can be evaluated as\n",
+ "\n",
+ "
\n",
+ "\n",
+ "It can be readily seen that the infinite product of transfer matrices reduces to a projector onto the fixed points, so that the norm reduces to the overlap between the boundary vectors and the fixed points. Since there is no effect of the boundary vectors on the bulk properties of the MPS, we can always choose these such that MPS is properly normalized as $ \\left \\langle \\Psi(\\bar{A})\\middle | \\Psi(A) \\right \\rangle = 1$."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "6f0d407d-3dab-40ff-9116-0fd4239bc894",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/createTransfermatrix.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file createTransfermatrix.m\n",
+ "function E = createTransfermatrix(A)\n",
+ " % Form the transfermatrix of an MPS.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ "\n",
+ " % Returns\n",
+ " % -------\n",
+ " % E : :class:`Tensor` (D, D, D, D)\n",
+ " % Transfer matrix with 4 legs,\n",
+ " % ordered topLeft-bottomLeft-topRight-bottomRight.\n",
+ " E = contract(A, [-1, 1, -3], conj(A), [-2, 1, -4], 'Rank', [2, 2]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "94c3d14e-c523-48ef-b5f2-daf47f3c6445",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/normalizeMPS.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file normalizeMPS.m\n",
+ "function Anew = normalizeMPS(A)\n",
+ " % Normalize an MPS tensor.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % Anew : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^6) algorithm,\n",
+ " % diagonalizing (D^2, D^2) matrix.\n",
+ " \n",
+ " % create transfer matrix and reshape to appropriate tensor map acting on right fixed point\n",
+ " E = createTransfermatrix(A);\n",
+ " E = permute(E, [1, 2, 4, 3]);\n",
+ " \n",
+ " % determine eigenvalue and rescale MPS tensor accordingly\n",
+ " norm = eigsolve(E);\n",
+ " Anew = A / sqrt(norm);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "260176be-4963-4e1a-820d-c18c857fef3f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/leftFixedPoint.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file leftFixedPoint.m\n",
+ "function l = leftFixedPoint(A)\n",
+ " % Find left fixed point.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % l : :class:`Tensor` (D, D)\n",
+ " % left fixed point with 2 legs,\n",
+ " % ordered bottom-top.\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^6) algorithm,\n",
+ " % diagonalizing (D^2, D^2) matrix.\n",
+ " \n",
+ " % initialize transfer matrix and reshape to appropriate tensor map acting on left fixed point\n",
+ " E = createTransfermatrix(A);\n",
+ " E = permute(E, [4, 3, 1, 2]);\n",
+ " \n",
+ " % find fixed point\n",
+ " [l, ~] = eigsolve(E);\n",
+ " \n",
+ " % interpret as matrix\n",
+ " l = repartition(l, [1, 1]);\n",
+ " \n",
+ " % make left fixed point hermitian and positive semidefinite explicitly\n",
+ " l = l * trace(l) / abs(trace(l)); % remove possible phase, actually forgot why I had to do this\n",
+ " l = (l + l') / 2; % force hermitian\n",
+ " l = l * sign(trace(l)); % force positive semidefinite\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "78f3a074-7c39-45c4-974d-326af7a1a65b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/rightFixedPoint.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file rightFixedPoint.m\n",
+ "function r = rightFixedPoint(A)\n",
+ " % Find right fixed point.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % r : :class:`Tensor` (D, D)\n",
+ " % left fixed point with 2 legs,\n",
+ " % ordered top-bottom.\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^6) algorithm,\n",
+ " % diagonalizing (D^2, D^2) matrix.\n",
+ " \n",
+ " % initialize transfer matrix and reshape to appropriate tensor map acting on right fixed point\n",
+ " E = createTransfermatrix(A);\n",
+ " E = permute(E, [1:E.rank(1), flip(1:E.rank(2)) + E.rank(1)]);\n",
+ " \n",
+ " % find fixed point\n",
+ " [r, ~] = eigsolve(E);\n",
+ " \n",
+ " % interpret to matrix\n",
+ " r = repartition(r, [1, 1]);\n",
+ " \n",
+ " % make right fixed point hermitian and positive semidefinite explicitly\n",
+ " r = r * trace(r) / abs(trace(r)); % remove possible phase, actually forgot why I had to do this\n",
+ " r = (r + r') / 2; % force hermitian\n",
+ " r = r * sign(trace(r)); % force positive semidefinite\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "61ccca0f-75de-44ad-9ac7-371a897ed9f2",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/fixedPoints.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file fixedPoints.m\n",
+ "function [l, r] = fixedPoints(A)\n",
+ " % Find normalized fixed points.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % l : :class:`Tensor` (D, D)\n",
+ " % left fixed point with 2 legs,\n",
+ " % ordered bottom-top.\n",
+ " % r : :class:`Tensor` (D, D)\n",
+ " % right fixed point with 2 legs,\n",
+ " % ordered top-bottom.\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^6) algorithm,\n",
+ " % diagonalizing (D^2, D^2) matrix\n",
+ "\n",
+ " % find fixed points\n",
+ " l = leftFixedPoint(A);\n",
+ " r = rightFixedPoint(A);\n",
+ " \n",
+ " % calculate trace and normalize\n",
+ " l = l / trace(l * r);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "21c35e9f-e001-4e82-8078-a3adcbeb9f69",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "A = normalizeMPS(A);\n",
+ "[l, r] = fixedPoints(A);\n",
+ "\n",
+ "assert(isapprox(l, l', 'RelTol', relTol), 'left fixed point should be hermitian!')\n",
+ "assert(isapprox(r, r', 'RelTol', relTol), 'right fixed point should be hermitian!')\n",
+ "\n",
+ "assert(isapprox(l, contract(A, [1, 2, -2], l, [3, 1], conj(A), [3, 2, -1]), 'RelTol', relTol), 'l should be a left fixed point!')\n",
+ "assert(isapprox(r, contract(A, [-1, 2, 1], r, [1, 3], conj(A), [-2, 2, 3]), 'RelTol', relTol), 'r should be a right fixed point!')\n",
+ "assert(abs(trace(l * r) - 1) < relTol, 'Left and right fixed points should be trace normalized!') % TODO: fix once traces are a thing"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ebea5103-1f7c-479b-9e7d-371d0d648589",
+ "metadata": {},
+ "source": [
+ "## 2 Gauge freedom\n",
+ "While a given MPS tensor $A$ corresponds to a unique state $\\left | \\Psi(A) \\right \\rangle$, the converse is not true, as different tensors may give rise to the same state. This is easily seen by noting that the gauge transform\n",
+ "\n",
+ "
\n",
+ "\n",
+ "leaves the physical state invariant. We may use this freedom in parametrization to impose canonical forms on the MPS tensor $A$.\n",
+ "\n",
+ "We start by considering the *left-orthonormal form* of an MPS, which is defined in terms of a tensor $A_L$ that satisfies the condition\n",
+ "\n",
+ "
\n",
+ "\n",
+ "We can find the gauge transform $L$ that brings $A$ into this form\n",
+ "\n",
+ "
\n",
+ "\n",
+ "by decomposing the fixed point $l$ as $l = L^\\dagger L$, such that\n",
+ "\n",
+ "
\n",
+ "\n",
+ "Note that this gauge choice still leaves room for unitary gauge transformations\n",
+ "\n",
+ "
\n",
+ "\n",
+ "which can be used to bring the right fixed point $r$ into diagonal form. Similarly, we can find the gauge transform that brings $A$ into *right-orthonormal form*\n",
+ "\n",
+ "
\n",
+ "\n",
+ "such that\n",
+ "\n",
+ "
\n",
+ "\n",
+ "and the left fixed point $l$ is diagonal. The routines that bring a given MPS into canonical form by decomposing the corresponding transfer matrix fixed points can be defined as follows:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "id": "f33dcb84-4d39-4384-9718-2635e539dd9b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/leftOrthonormalize.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file leftOrthonormalize.m\n",
+ "function [L, Al] = leftOrthonormalize(A, l)\n",
+ " % Transform A to left-orthonormal gauge.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % L : :class:`Tensor` (D, D)\n",
+ " % left gauge with 2 legs,\n",
+ " % ordered left-right.\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left orthonormal\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^6) algorithm,\n",
+ " % diagonalizing (D^2, D^2) matrix\n",
+ " \n",
+ " % find left fixed point\n",
+ " if nargin < 2 || isempty(l)\n",
+ " l = leftFixedPoint(A);\n",
+ " end\n",
+ " \n",
+ " % decompose l = L * L\n",
+ " L = sqrtm(l);\n",
+ " \n",
+ " % apply gauge L to A\n",
+ " Al = contract(L, [-1, 1], A, [1, -2, 2], inv(L), [2, -3]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "259522be-dc9e-4964-b302-676a3b210efd",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/rightOrthonormalize.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file rightOrthonormalize.m\n",
+ "function [R, Ar] = rightOrthonormalize(A, r)\n",
+ " % Transform A to right-orthonormal gauge.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % R : :class:`Tensor` (D, D)\n",
+ " % right gauge with 2 legs,\n",
+ " % ordered left-right.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left orthonormal\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^6) algorithm,\n",
+ " % diagonalizing (D^2, D^2) dmatrix\n",
+ " \n",
+ " % find right fixed point\n",
+ " if nargin < 2 || isempty(r)\n",
+ " r = rightFixedPoint(A);\n",
+ " end\n",
+ " \n",
+ " % decompose r = R * R\n",
+ " R = sqrtm(r);\n",
+ " \n",
+ " % apply gauge R to A\n",
+ " Ar = contract(inv(R), [-1, 1], A, [1, -2, 2], R, [2, -3]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "678f4581-dba0-4476-a811-2715effef870",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "[L, Al] = leftOrthonormalize(A, l);\n",
+ "[R, Ar] = rightOrthonormalize(A, r);\n",
+ "\n",
+ "assert(isapprox(R * R', r, 'RelTol', relTol), 'Right gauge doesn\"t square to r')\n",
+ "assert(isapprox(L' * L, l, 'RelTol', relTol), 'Left gauge doesn\"t sqaure to l')\n",
+ "assert(isapprox(contract(Ar, [-1, 1, 2], conj(Ar), [-2, 1, 2]), R.eye(R.codomain, R.domain), 'RelTol', 1e-10), 'Ar not in right-orthonormal form')\n",
+ "assert(isapprox(contract(Al, [1, 2, -2], conj(Al), [1, 2, -1]), L.eye(L.codomain, L.domain), 'RelTol', 1e-10), 'Al not in left-orthonormal form')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "edf5f45f-d689-410c-8722-fad54892f76f",
+ "metadata": {},
+ "source": [
+ "Finally, we can define a *mixed gauge* for the uniform MPS by choosing one site, the 'center site', and bringing all tensors to the left of it in the left-orthonormal form and all the tensors to the right of it in the right-orthonormal form. Defining a new tensor $A_C$ on the center site, we obtain the form\n",
+ "\n",
+ "
\n",
+ "\n",
+ "By contrast, the original representation using the same tensor at every site is commonly referred to as the *uniform gauge*. The mixed gauge has an intuitive interpretation. Defining $C = LR$, this tensor then implements the gauge transform that maps the left-orthonormal tensor to the right-orthonormal one, thereby defining the center-site tensor $A_C$:\n",
+ "\n",
+ "
\n",
+ "\n",
+ "This relation is called the mixed gauge condition and allows us to freely move the center tensor $A_C$ through the MPS, linking the left- and right orthonormal tensors.\n",
+ "\n",
+ "Finally we may bring $C$ into diagonal form by performing a singular value decomposition $C = USV^\\dagger$ and absorbing $U$ and $V^\\dagger$ into the definition of $A_L$ and $A_R$ using the residual unitary gauge freedom\n",
+ "\n",
+ "
\n",
+ "\n",
+ "The mixed canonical form with a diagonal $C$ now allows to straightforwardly write down a Schmidt decomposition of the state across an arbitrary bond in the chain\n",
+ "\n",
+ "$$ \\left | \\Psi(A) \\right \\rangle = \\sum_{i=1}^{D} C_i \\left | \\Psi^i_L(A_L) \\right \\rangle \\otimes \\left | \\Psi^i_R(A_R) \\right \\rangle,$$\n",
+ "\n",
+ "where the states $\\left | \\Psi^i_L(A_L) \\right \\rangle$ and $\\left | \\Psi^i_R(A_R) \\right \\rangle$ are orthogonal states on half the lattice. The diagonal elements $C_i$ are exactly the Schmidt numbers of any bipartition of the MPS, and as such determine its bipartite entanglement entropy\n",
+ "\n",
+ "$$ S = -\\sum_i C_i^2 \\log(C_i^2) .$$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "a1e6f023-1e19-4374-8fec-d707a137571b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/mixedCanonical.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file mixedCanonical.m\n",
+ "function [Al, Ar, C, Ac] = mixedCanonical(A)\n",
+ " % Bring MPS tensor into mixed gauge, such that -Al-C- = -C-Ar- = Ac.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left orthonormal.\n",
+ " % Ac : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauge.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right orthonormal.\n",
+ " % C : :class:`Tensor` (D, D)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right,\n",
+ " % diagonal.\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^6) algorithm,\n",
+ " % diagonalization of (D^2, D^2) matrix\n",
+ " \n",
+ " % Compute left and right orthonormal forms\n",
+ " [L, Al] = leftOrthonormalize(A);\n",
+ " [R, Ar] = rightOrthonormalize(A);\n",
+ " \n",
+ " % center matrix C is matrix multiplication of L and R\n",
+ " C = L * R;\n",
+ " \n",
+ " % singular value decomposition to diagonalize C\n",
+ " [U, S, V] = tsvd(C, 1, 2);\n",
+ "\n",
+ " % absorb corresponding unitaries in Al and Ar\n",
+ " Al = contract(U', [-1, 1], Al, [1, -2, 2], U, [2, -3]);\n",
+ " Ar = contract(V, [-1, 1], Ar, [1, -2, 2], V', [2, -3]);\n",
+ "\n",
+ " % normalize center matrix\n",
+ " C = S / norm(S);\n",
+ "\n",
+ " % compute center MPS tensor\n",
+ " Ac = contract(Al, [-1, -2, 1], C, [1, -3]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "id": "e322559f-a4d5-41ba-8239-261d798f1d01",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/entanglementSpectrum.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file entanglementSpectrum.m\n",
+ "function [S, entropy] = entanglementSpectrum(A)\n",
+ " % Calculate the entanglement spectrum of an MPS.\n",
+ "\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % S : double (D, 1)\n",
+ " % Singular values of center matrix,\n",
+ " % representing the entanglement spectrum\n",
+ " % entropy : double\n",
+ " % Entanglement entropy across a leg.\n",
+ " \n",
+ " % go to mixed gauge\n",
+ " [~, ~, C, ~] = mixedCanonical(A);\n",
+ "\n",
+ " % calculate entropy\n",
+ " S = diag(double(C));\n",
+ " entropy = -sum(S.^2 .* log(S.^2));\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 15,
+ "id": "e3321c09-63a9-42b7-b207-efb329f1dc05",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "% check that gauging is still correct\n",
+ "[Al, Ar, C, Ac] = mixedCanonical(A);\n",
+ "[S, entropy] = entanglementSpectrum(A);\n",
+ "\n",
+ "assert(isapprox(contract(Ar, [-1, 1, 2], conj(Ar), [-2, 1, 2]), C.eye(C.codomain, C.domain), 'RelTol', 1e-10), 'Ar not in right-orthonormal form')\n",
+ "assert(isapprox(contract(Al, [1, 2, -2], conj(Al), [1, 2, -1]), C.eye(C.codomain, C.domain), 'RelTol', 1e-10), 'Al not in left-orthonormal form')\n",
+ "LHS = contract(Al, [-1, -2, 1], C, [1, -3]);\n",
+ "RHS = contract(C, [-1, 1], Ar, [1, -2, -3]);\n",
+ "assert(isapprox(LHS, RHS, 'RelTol', relTol) && isapprox(RHS, Ac, 'RelTol', relTol), 'Mixed gauge condition not satisfied!')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a049a0e6-f651-4f56-a62a-4653e7a1aaf5",
+ "metadata": {},
+ "source": [
+ "## 3 Truncation of a uniform MPS\n",
+ "\n",
+ "The mixed canonical form also enables efficient truncatation an MPS. The sum in the above Schmidt decomposition can be truncated, giving rise to a new MPS that has a reduced bond dimension for that bond. This truncation is optimal in the sense that the norm between the original and the truncated MPS is maximized. To arrive at a translation invariant truncated MPS, we can truncate the columns of the absorbed isometries $U$ and $V^\\dagger$ correspondingly, thereby transforming *every* tensor $A_L$ or $A_R$. The truncated MPS in the mixed gauge is then given by\n",
+ "\n",
+ "
\n",
+ "\n",
+ "We note that the resulting state based on this local truncation is not guaranteed to correspond to the MPS with a lower bond dimension that is globally optimal. This would require a variational optimization of the cost function.\n",
+ "\n",
+ "$$ \\left | \\left | ~\\left | \\Psi(A) \\right \\rangle - \\left | \\Psi(\\tilde{A}) \\right \\rangle ~\\right | \\right |^2.$$"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "2a4b977f-32e0-411c-9297-5b0e16738e7d",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/truncateMPS.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file truncateMPS.m\n",
+ "function [AlTilde, ArTilde, CTilde, AcTilde] = truncateMPS(A, Dtrunc)\n",
+ " % Truncate an MPS to a lower bond dimension.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % Dtrunc : int\n",
+ " % lower bond dimension\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % AlTilde : :class:`Tensor` (Dtrunc, d, Dtrunc)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left orthonormal.\n",
+ " % AcTilde : :class:`Tensor` (Dtrunc, d, Dtrunc)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauge.\n",
+ " % ArTilde : :class:`Tensor` (Dtrunc, d, Dtrunc)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right orthonormal.\n",
+ " % CTilde : :class:`Tensor` (Dtrunc, Dtrunc)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right,\n",
+ " % diagonal.\n",
+ " \n",
+ " [Al, Ar, C, Ac] = mixedCanonical(A);\n",
+ " \n",
+ " % perform SVD with truncation option\n",
+ " [U, S, V] = tsvd(C, 1, 2, 'TruncDim', Dtrunc);\n",
+ " \n",
+ " % reabsorb unitaries\n",
+ " AlTilde = contract(U', [-1, 1], Al, [1, -2, 2], U, [2, -3]);\n",
+ " ArTilde = contract(V, [-1, 1], Ar, [1, -2, 2], V', [2, -3]);\n",
+ " \n",
+ " % normalize center matrix\n",
+ " CTilde = S / norm(S);\n",
+ "\n",
+ " % compute center MPS tensor\n",
+ " AcTilde = contract(AlTilde, [-1, -2, 1], CTilde, [1, -3]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "id": "8431e495-95f0-4619-b1c3-3b2c6289ab7f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "Dtrunc = 3;\n",
+ "[AlTilde, ArTilde, CTilde, AcTilde] = truncateMPS(A, Dtrunc);\n",
+ "assert(AlTilde.dims(1) == Dtrunc && AlTilde.dims(3) == Dtrunc, 'Something went wrong in truncating the MPS')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fe3eb8e0-870a-4bd8-8f83-a4577d3ed42b",
+ "metadata": {
+ "tags": []
+ },
+ "source": [
+ "## 4 Algorithms for finding canonical forms\n",
+ "The success of using MPS for describing physical systems stems from the fact that they provide efficient approximations to a large class of physically relevant states. In one dimension, they have been shown to approximate low-energy states of gapped systems arbitrarily well at only a polynomial cost in the bond dimension $D$. This means that in principle we can push MPS results for these systems to arbitrary precision as long as we increase the bond dimension enough. However, increasing the bond dimension comes at a numerical cost, as the complexity of any MPS algorithm scales with $D$. As opposed to the naive routines given above, it is possible to ensure that the complexity of all MPS algorithms scales as $O(D^3)$, so long as we are a bit careful when implementing them.\n",
+ "\n",
+ "As a first example, we can refrain from explicitly contructing the matrices that are used in the eigenvalue problems, and instead pass a function that implements the action of the corresponding operator on a vector to the eigenvalue solver. We demonstrate this for the problem of normalizing an MPS, where instead of explicitly constructing the transfer matrix we now define a function handle which implements its action on the right and left fixed points using an optimal contraction sequence:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 18,
+ "id": "68787943-9fdc-424d-b27b-b4cf1f805297",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/normalizeMPS.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file normalizeMPS.m\n",
+ "function Anew = normalizeMPS(A)\n",
+ " % Normalize an MPS tensor.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % Anew : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^3) algorithm,\n",
+ " % D^3 contraction for transfer matrix handle.\n",
+ " \n",
+ " % define handle for transfer matrix acting on right vector\n",
+ " handleERight = @(v) contract(A, [-1, 2, 1], conj(A), [-2, 2, 3], v, [1, 3], 'Rank', [1, 1]);\n",
+ " \n",
+ " % start from random initial guess for fixed point\n",
+ " r0 = A.randnc(A.dims([3, 3]), 'Rank', [1, 1]);\n",
+ " \n",
+ " % calculate eigenvalue\n",
+ " lam = eigsolve(handleERight, r0);\n",
+ "\n",
+ " Anew = A / sqrt(lam);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 19,
+ "id": "1293165e-6cde-4b7e-8f03-f2e6563a5c1a",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/leftFixedPoint.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file leftFixedPoint.m\n",
+ "function l = leftFixedPoint(A)\n",
+ " % Find left fixed point.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % l : :class:`Tensor` (D, D)\n",
+ " % left fixed point with 2 legs,\n",
+ " % ordered bottom-top.\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^3) algorithm,\n",
+ " % D^3 contraction for transfer matrix handle.\n",
+ "\n",
+ " % define handle for transfer matrix acting on left vector\n",
+ " handleELeft = @(v) contract(A, [1, 2, -2], conj(A), [3, 2, -1], v, [3, 1], 'Rank', [1, 1]);\n",
+ " \n",
+ " % start from random initial guess for fixed point\n",
+ " l0 = A.randnc(A.dims([1, 1]), 'Rank', [1, 1]);\n",
+ " \n",
+ " % calculate fixed point\n",
+ " [l, ~] = eigsolve(handleELeft, l0);\n",
+ " \n",
+ " % make left fixed point hermitian and positive semidefinite explicitly\n",
+ " l = l * trace(l) / abs(trace(l)); % remove possible phase, actually forgot why I had to do this\n",
+ " l = (l + l') / 2; % force hermitian\n",
+ " l = l * sign(trace(l)); % force positive semidefinite\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 20,
+ "id": "f0a19278-4c7e-43c8-bfbc-0b2647a8efad",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/rightFixedPoint.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file rightFixedPoint.m\n",
+ "function r = rightFixedPoint(A)\n",
+ " % Find right fixed point.\n",
+ "\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ "\n",
+ " % Returns\n",
+ " % -------\n",
+ " % r : :class:`Tensor` (D, D)\n",
+ " % right fixed point with 2 legs,\n",
+ " % ordered top-bottom.\n",
+ "\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^3) algorithm,\n",
+ " % D^3 contraction for transfer matrix handle.\n",
+ " \n",
+ " % define handle for transfer matrix acting on right vector\n",
+ " handleERight = @(v) contract(A, [-1, 2, 1], conj(A), [-2, 2, 3], v, [1, 3], 'Rank', [1, 1]);\n",
+ " \n",
+ " % start from random initial guess for fixed point\n",
+ " r0 = A.randnc(A.dims([3, 3]), 'Rank', [1, 1]);\n",
+ " \n",
+ " % calculate fixed point\n",
+ " [r, ~] = eigsolve(handleERight, r0);\n",
+ " \n",
+ " % make right fixed point hermitian and positive semidefinite explicitly\n",
+ " r = r * trace(r) / abs(trace(r)); % remove possible phase, actually forgot why I had to do this\n",
+ " r = (r + r') / 2; % force hermitian\n",
+ " r = r * sign(trace(r)); % force positive semidefinite\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "d86c05e9-c927-4a35-8c91-aac1fe59ad07",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/fixedPoints.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file fixedPoints.m\n",
+ "function [l, r] = fixedPoints(A)\n",
+ " % Find normalized fixed points.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % l : :class:`Tensor` (D, D)\n",
+ " % left fixed point with 2 legs,\n",
+ " % ordered bottom-top.\n",
+ " % r : :class:`Tensor` (D, D)\n",
+ " % right fixed point with 2 legs,\n",
+ " % ordered top-bottom.\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^6) algorithm,\n",
+ " % diagonalizing (D^2, D^2) matrix\n",
+ "\n",
+ " % find fixed points\n",
+ " l = leftFixedPoint(A);\n",
+ " r = rightFixedPoint(A);\n",
+ " \n",
+ " % calculate trace and normalize\n",
+ " l = l / trace(l * r);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "16963523-d412-40ab-af26-92900349f259",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "A = createMPS(D, d);\n",
+ "A = normalizeMPS(A);\n",
+ "[l, r] = fixedPoints(A);\n",
+ "\n",
+ "assert(isapprox(l, l', 'RelTol', relTol), 'left fixed point should be hermitian!')\n",
+ "assert(isapprox(r, r', 'RelTol', relTol), 'right fixed point should be hermitian!')\n",
+ "\n",
+ "assert(isapprox(l, contract(A, [1, 2, -2], l, [3, 1], conj(A), [3, 2, -1]), 'RelTol', relTol), 'l should be a left fixed point!')\n",
+ "assert(isapprox(r, contract(A, [-1, 2, 1], r, [1, 3], conj(A), [-2, 2, 3]), 'RelTol', relTol), 'r should be a right fixed point!')\n",
+ "assert(isapprox(trace(l * r), 1, 'RelTol', relTol), 'Left and right fixed points should be trace normalized!') % TODO: fix once traces are a thing"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "15694438-b2ce-432d-b26f-42ffe1576340",
+ "metadata": {},
+ "source": [
+ "We can similarly improve both the efficiency and accuracy of the routines bringing a given MPS into its mixed canonical form. While plugging in the more efficient ways of finding the left and right fixed point into the above `mixedCanonical` routine would reduce its complexity to $O(D^3)$, this algorithm would still be suboptimal in terms of numerical accuracy. This arises from the fact that, while $l$ and $r$ are theoretically known to be positive hermitian matrices, at least one of them will nevertheless have small eigenvalues, say of order $\\eta$, if the MPS is supposed to provide a good approximation to an actual state. In practice, $l$ and $r$ are determined using an iterative eigensolver and will only be accurate up to a specified tolerance $\\epsilon$. Upon taking the 'square roots' $L$ and $R$, the numerical precision will then decrease to $\\text{min}(\\sqrt{\\epsilon}, \\epsilon/\\sqrt{\\eta})$. Furthermore, gauge transforming $A$ with $L$ or $R$ requires the potentially ill-conditioned inversions of $L$ and $R$, and will typically yield $A_L$ and $A_R$ which violate the orthonormalization condition in the same order $\\epsilon/\\sqrt{\\eta}$. We can circumvent both these probelems by resorting to so-called *single-layer algorithms*. These are algorithms that only work on the level of the MPS tensors in the ket layer, and never consider operations for which contractions with the bra layer are needed. We now demonstrate such a single-layer algorithm for finding canonical forms.\n",
+ "\n",
+ "Suppose we are given an MPS tensor $A$, then from the above discussion we know that bringing it into left canonical form means finding a left-orthonormal tensor $A_L$ and a matrix $L$ such that $L A=A_L L$. The idea is then to solve this equation iteratively, where in every iteration\n",
+ "\n",
+ "1. we start from a matrix $L^{i}$\n",
+ "2. we construct the tensor $L^{i}A$\n",
+ "3. we take a QR decomposition to obtain $A_L^{i+1} L^{i+1} = L^{i}A$, and\n",
+ "4. we take $L^{i+1}$ to the next iteration\n",
+ "\n",
+ "The QR decomposition is represented diagrammatically as\n",
+ "\n",
+ "
\n",
+ "\n",
+ "This iterative procedure is bound to converge to a fixed point for which $L^{(i+1)}=L^{(i)}=L$ and $A_L$ is left orthonormal by construction:\n",
+ "\n",
+ "
\n",
+ "\n",
+ "A similar procedure can be used to find a right-orthonormal tensor $A_R$ and a matrix $R$ such that $A R = R A_R$. It is important to note that the convergence of this procedure relies on the fact that the QR decomposition is unique, which is not actually the case in general. However, it can be made unique by imposing that the diagonal elements of the triangular matrix $R$ must be positive. This extra condition is imposed by calling the `Tensor` factorization methods `leftorth` and `rightorth` with values `'qrpos'` and `'rqpos'` for the algorithm argument respectively."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 23,
+ "id": "4593b3ff-f9c4-470d-ba37-219b0043e048",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/rightOrthonormalize.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file rightOrthonormalize.m\n",
+ "function [R, Ar] = rightOrthonormalize(A, R0, tol, maxIter)\n",
+ " % Transform A to right-orthonormal gauge.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % R0 : :class:`Tensor` (D, D), optional\n",
+ " % Right gauge matrix,\n",
+ " % initial guess.\n",
+ " % tol : float, optional\n",
+ " % convergence criterium,\n",
+ " % norm(R - Rnew) < tol.\n",
+ " % maxIter : int\n",
+ " % maximum amount of iterations.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % R : :class:`Tensor` (D, D)\n",
+ " % right gauge with 2 legs,\n",
+ " % ordered left-right.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right-orthonormal\n",
+ " \n",
+ " arguments\n",
+ " A\n",
+ " R0 = []\n",
+ " tol = 1e-14\n",
+ " maxIter = 1e5\n",
+ " end\n",
+ " \n",
+ " if isempty(R0)\n",
+ " R0 = A.randnc(A.dims([3, 3]), 'Rank', [1, 1]);\n",
+ " end\n",
+ " \n",
+ " % Normalize R0\n",
+ " R0 = R0 / norm(R0);\n",
+ "\n",
+ " % Initialize loop\n",
+ " i = 1;\n",
+ " [R, Ar] = rightorth(contract(A, [-1, -2, 1], R0, [1, -3]), 1, [2, 3], 'rqpos');\n",
+ " R = R / norm(R);\n",
+ " convergence = distance(R, R0);\n",
+ "\n",
+ " % Decompose A*R until R converges\n",
+ " while convergence > tol\n",
+ " % calculate AR and decompose\n",
+ " [Rnew, Ar] = rightorth(contract(A, [-1, -2, 1], R, [1, -3]), 1, [2, 3], 'rqpos');\n",
+ "\n",
+ " % normalize new R\n",
+ " Rnew = Rnew / norm(Rnew);\n",
+ "\n",
+ " % calculate convergence criterium\n",
+ " convergence = distance(Rnew, R);\n",
+ " R = Rnew;\n",
+ "\n",
+ " % check if iterations exceeds maxIter\n",
+ " if i > maxIter\n",
+ " warning('Right-orthonormalization has not converged!')\n",
+ " break\n",
+ " end\n",
+ " i = i + 1;\n",
+ " end\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 24,
+ "id": "0838f7b2-d694-4dac-9643-55822c269f0c",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/leftOrthonormalize.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file leftOrthonormalize.m\n",
+ "function [L, Al] = leftOrthonormalize(A, L0, tol, maxIter)\n",
+ " % Transform A to left-orthonormal gauge.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % L0 : :class:`Tensor` (D, D), optional\n",
+ " % Left gauge matrix,\n",
+ " % initial guess.\n",
+ " % tol : float, optional\n",
+ " % convergence criterium,\n",
+ " % norm(R - Rnew) < tol.\n",
+ " % maxIter : int\n",
+ " % maximum amount of iterations.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % L : :class:`Tensor` (D, D)\n",
+ " % left gauge with 2 legs,\n",
+ " % ordered left-right.\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left-orthonormal\n",
+ "\n",
+ " arguments\n",
+ " A\n",
+ " L0 = []\n",
+ " tol = 1e-14\n",
+ " maxIter = 1e5\n",
+ " end\n",
+ " \n",
+ " if isempty(L0)\n",
+ " L0 = A.randnc(A.dims([1, 1]), 'Rank', [1, 1]);\n",
+ " end\n",
+ " \n",
+ " % Normalize L0\n",
+ " L0 = L0 / norm(L0);\n",
+ "\n",
+ " % Initialize loop\n",
+ " i = 1;\n",
+ " [Al, L] = leftorth(contract(L0, [-1, 1], A, [1, -2, -3]), [1, 2], 3, 'qrpos');\n",
+ " L = L / norm(L);\n",
+ " convergence = distance(L, L0);\n",
+ " \n",
+ " % Decompose L*A until L converges\n",
+ " while convergence > tol\n",
+ " % calculate LA and decompose\n",
+ " [Al, Lnew] = leftorth(contract(L, [-1, 1], A, [1, -2, -3]), [1, 2], 3, 'qrpos');\n",
+ "\n",
+ " % normalize new L\n",
+ " Lnew = Lnew / norm(Lnew);\n",
+ "\n",
+ " % calculate convergence criterium\n",
+ " convergence = distance(Lnew, L);\n",
+ " L = Lnew;\n",
+ "\n",
+ " % check if iterations exceeds maxIter\n",
+ " if i > maxIter\n",
+ " warning('Left-orthonormalization has not converged!')\n",
+ " break\n",
+ " end\n",
+ " i = i + 1;\n",
+ " end\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "id": "c01b4ec1-eb01-46be-b625-caa607d495e0",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/mixedCanonical.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file mixedCanonical.m\n",
+ "function [Al, Ar, C, Ac] = mixedCanonical(A, L0, R0, tol, maxIter)\n",
+ " % Bring MPS tensor into mixed gauge, such that -Al-C- = -C-Ar- = Ac.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % Al : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % left orthonormal.\n",
+ " % Ac : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauge.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right orthonormal.\n",
+ " % C : :class:`Tensor` (D, D)\n",
+ " % Center gauge with 2 legs,\n",
+ " % ordered left-right,\n",
+ " % diagonal.\n",
+ " %\n",
+ " % Complexity\n",
+ " % ----------\n",
+ " % O(D^3) algorithm.\n",
+ "\n",
+ " arguments\n",
+ " A\n",
+ " L0 = []\n",
+ " R0 = []\n",
+ " tol = 1e-14\n",
+ " maxIter = 1e5\n",
+ " end\n",
+ " \n",
+ " if isempty(L0)\n",
+ " L0 = A.randnc(A.dims([1, 1]), 'Rank', [1, 1]);\n",
+ " end\n",
+ " if isempty(R0)\n",
+ " R0 = A.randnc(A.dims([3, 3]), 'Rank', [1, 1]);\n",
+ " end\n",
+ "\n",
+ " % Compute left and right orthonormal forms\n",
+ " [L, Al] = leftOrthonormalize(A, L0, tol, maxIter);\n",
+ " [R, Ar] = rightOrthonormalize(A, R0, tol, maxIter);\n",
+ "\n",
+ " % center matrix C is matrix multiplication of L and R\n",
+ " C = L * R;\n",
+ "\n",
+ " % singular value decomposition to diagonalize C\n",
+ " [U, S, V] = tsvd(C, 1, 2);\n",
+ "\n",
+ " % absorb corresponding unitaries in Al and Ar\n",
+ " Al = contract(U', [-1, 1], Al, [1, -2, 2], U, [2, -3], 'Rank', Al.rank);\n",
+ " Ar = contract(V, [-1, 1], Ar, [1, -2, 2], V', [2, -3], 'Rank', Ar.rank);\n",
+ "\n",
+ " % normalize center matrix\n",
+ " C = S / norm(S);\n",
+ "\n",
+ " % compute center MPS tensor\n",
+ " Ac = contract(Al, [-1, -2, 1], C, [1, -3], 'Rank', Al.rank);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "id": "c697d27f-eb88-4dbb-bb58-22d5713262ef",
+ "metadata": {
+ "tags": []
+ },
+ "outputs": [],
+ "source": [
+ "% check that gauging is still correct\n",
+ "[Al, Ar, C, Ac] = mixedCanonical(A);\n",
+ "\n",
+ "assert(isapprox(contract(Ar, [-1, 1, 2], conj(Ar), [-2, 1, 2]), C.eye(C.codomain, C.domain), 'RelTol', relTol), 'Ar not in right-orthonormal form')\n",
+ "assert(isapprox(contract(Al, [1, 2, -2], conj(Al), [1, 2, -1]), C.eye(C.codomain, C.domain), 'RelTol', relTol), 'Al not in left-orthonormal form')\n",
+ "LHS = contract(Al, [-1, -2, 1], C, [1, -3]);\n",
+ "RHS = contract(C, [-1, 1], Ar, [1, -2, -3]);\n",
+ "assert(isapprox(LHS, RHS, 'RelTol', relTol) && isapprox(RHS, Ac, 'RelTol', relTol), 'Mixed gauge condition not satisfied!')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "84bb722f-6aa6-44e9-a048-ea9b060b1ed2",
+ "metadata": {},
+ "source": [
+ "## 5 Computing expectation values \n",
+ "Now that we have seen the different ways to parametrize a given MPS, namely the uniform gauge and the mixed gauge, we wish to use these to compute expectation values of an extensive operator:\n",
+ "$$ O = \\frac{1}{\\mathbb{Z}} \\sum_{n \\in \\mathbb{Z}} O_n. $$\n",
+ "\n",
+ "If we assume that each $O_n$ acts on a single site and we are working with a properly normalized MPS, translation invariance dictates that the expectation value of $O$ is given by the contraction\n",
+ "\n",
+ "
\n",
+ "\n",
+ "In the uniform gauge, we can use the fixed points of the transfer matrix to contract everything to the left and to the right of the operator, such that we are left with the contraction\n",
+ "\n",
+ "
\n",
+ "\n",
+ "In the mixed gauge, we can locate the center site where the operator is acting, and then contract everything to the left and right to the identity to arrive at the particularly simple expression\n",
+ "\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "id": "afdd32d5-3cd4-463f-9a9e-cc04b06a99c9",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/expVal1Uniform.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file expVal1Uniform.m\n",
+ "function o = expVal1Uniform(O, A, l, r)\n",
+ " % Calculate the expectation value of a 1-site operator in uniform gauge.\n",
+ "\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % O : :class:`Tensor` (d, d)\n",
+ " % single-site operator.\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D), optional\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D), optional\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ "\n",
+ " % Returns\n",
+ " % -------\n",
+ " % o : complex float\n",
+ " % expectation value of O.\n",
+ " \n",
+ " arguments\n",
+ " O\n",
+ " A\n",
+ " l = []\n",
+ " r = []\n",
+ " end\n",
+ "\n",
+ " % calculate fixed points if not given\n",
+ " if isempty(l) || isempty(r)\n",
+ " [l, r] = fixedPoints(A);\n",
+ " end\n",
+ "\n",
+ " % contract expectation value network\n",
+ " o = contract(l, [4, 1], r, [3, 6], A, [1, 2, 3], conj(A), [4, 5, 6], O, [2, 5]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "id": "ebc46431-810b-4aa6-9c00-3027c9d29a6b",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/expVal1Mixed.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file expVal1Mixed.m\n",
+ "function o = expVal1Mixed(O, Ac)\n",
+ " % Calculate the expectation value of a 1-site operator in mixed gauge.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % O : :class:`Tensor` (d, d)\n",
+ " % single-site operator.\n",
+ " % Ac : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauged.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % o : complex float\n",
+ " % expectation value of O.\n",
+ "\n",
+ " % contract expectation value network\n",
+ " o = contract(Ac, [2, 1, 3], conj(Ac), [2, 4, 3], O, [1, 4]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 29,
+ "id": "e51cbb59-4df2-4206-9c49-5e5c9d1c4c9f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "O = Tensor.randnc([d, d]);\n",
+ "A = createMPS(D, d);\n",
+ "A = normalizeMPS(A);\n",
+ "[Al, Ar, C, Ac] = mixedCanonical(A);\n",
+ "expVal = expVal1Uniform(O, A);\n",
+ "expValMix = expVal1Mixed(O, Ac);\n",
+ "assert(isapprox(expVal, expValMix, 'RelTol', relTol), 'different gauges give different values?')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "77a55b31-d9eb-4a5f-a198-92277186f122",
+ "metadata": {},
+ "source": [
+ "This procedure can be readily generalized to operators that act on multiple sites. In particular, a two-site operator such as a Hamiltonian term $h$ can be evaluated as\n",
+ "\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "id": "b885433f-df40-4612-b2b8-3a06a46fc1c1",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/expVal2Uniform.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file expVal2Uniform.m\n",
+ "function o = expVal2Uniform(O, A, l, r)\n",
+ " % Calculate the expectation value of a 2-site operator in uniform gauge.\n",
+ " %\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % O : :class:`Tensor` (d, d, d, d)\n",
+ " % two-site operator,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight.\n",
+ " % A : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right.\n",
+ " % l : :class:`Tensor` (D, D), optional\n",
+ " % left fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " % r : :class:`Tensor` (D, D), optional\n",
+ " % right fixed point of transfermatrix,\n",
+ " % normalized.\n",
+ " %\n",
+ " % Returns\n",
+ " % -------\n",
+ " % o : complex float\n",
+ " % expectation value of O.\n",
+ " \n",
+ " arguments\n",
+ " O\n",
+ " A\n",
+ " l = []\n",
+ " r = []\n",
+ " end\n",
+ " \n",
+ " % calculate fixed points if not given\n",
+ " if isempty(l) || isempty(r)\n",
+ " [l, r] = fixedPoints(A);\n",
+ " end\n",
+ "\n",
+ " % contract expectation value network\n",
+ " o = contract(l, [6, 1], r, [5, 10], A, [1, 2, 3], A, [3, 4, 5], conj(A), [6, 7, 8], conj(A), [8, 9, 10], O, [2, 4, 7, 9]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 31,
+ "id": "5bd59f0b-27d0-41e1-bc50-a85e6eabc4d8",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Created file '/home/leburgel/git/TensorTrack/docs/src/examples/uniformMps/expVal2Mixed.m'.\n"
+ ]
+ }
+ ],
+ "source": [
+ "%%file expVal2Mixed.m\n",
+ "function o = expVal2Mixed(O, Ac, Ar)\n",
+ " % Calculate the expectation value of a 2-site operator in mixed gauge.\n",
+ "\n",
+ " % Parameters\n",
+ " % ----------\n",
+ " % O : :class:`Tensor` (d, d, d, d)\n",
+ " % two-site operator,\n",
+ " % ordered topLeft-topRight-bottomLeft-bottomRight.\n",
+ " % Ac : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % center gauged.\n",
+ " % Ar : :class:`Tensor` (D, d, D)\n",
+ " % MPS tensor with 3 legs,\n",
+ " % ordered left-bottom-right,\n",
+ " % right gauged.\n",
+ "\n",
+ " % Returns\n",
+ " % -------\n",
+ " % o : complex float\n",
+ " % expectation value of O.\n",
+ "\n",
+ " % contract expectation value network\n",
+ " o = contract(Ac, [4, 2, 1], Ar, [1, 3, 6], conj(Ac), [4, 5, 8], conj(Ar), [8, 7, 6], O, [2, 3, 5, 7]);\n",
+ "end"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "id": "3b195a09-58c6-4db4-a3c8-85e17283fe0c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "O2 = Tensor.randnc([d, d, d, d]);\n",
+ "\n",
+ "expVal = expVal2Uniform(O2, A);\n",
+ "expValGauge = expVal2Mixed(O2, Ac, Ar);\n",
+ "expValGauge2 = expVal2Mixed(O2, Al, Ac);\n",
+ "\n",
+ "diff1 = abs(expVal - expValGauge);\n",
+ "diff2 = abs(expVal - expValGauge2);\n",
+ "assert(diff1 < relTol && diff2 < relTol, 'different gauges give different values?')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a80e078b-b63d-4858-ac28-c201955933c2",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Matlab",
+ "language": "matlab",
+ "name": "matlab"
+ },
+ "language_info": {
+ "codemirror_mode": "octave",
+ "file_extension": ".m",
+ "help_links": [
+ {
+ "text": "MetaKernel Magics",
+ "url": "https://metakernel.readthedocs.io/en/latest/source/README.html"
+ }
+ ],
+ "mimetype": "text/x-octave",
+ "name": "matlab",
+ "version": "0.17.1"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/src/img/Fmove.svg b/docs/src/img/Fmove.svg
new file mode 100644
index 0000000..a346b90
--- /dev/null
+++ b/docs/src/img/Fmove.svg
@@ -0,0 +1,301 @@
+
+
diff --git a/docs/src/img/Rmove.svg b/docs/src/img/Rmove.svg
new file mode 100644
index 0000000..fd76809
--- /dev/null
+++ b/docs/src/img/Rmove.svg
@@ -0,0 +1,175 @@
+
+
diff --git a/docs/src/img/fusiontensor.svg b/docs/src/img/fusiontensor.svg
new file mode 100644
index 0000000..3fdeadd
--- /dev/null
+++ b/docs/src/img/fusiontensor.svg
@@ -0,0 +1,52 @@
+
+
diff --git a/docs/src/img/ipe/Fmove.pdf b/docs/src/img/ipe/Fmove.pdf
new file mode 100644
index 0000000..8752498
Binary files /dev/null and b/docs/src/img/ipe/Fmove.pdf differ
diff --git a/docs/src/img/ipe/Rmove.pdf b/docs/src/img/ipe/Rmove.pdf
new file mode 100644
index 0000000..aa9fbcf
Binary files /dev/null and b/docs/src/img/ipe/Rmove.pdf differ
diff --git a/docs/src/img/ipe/fusiontensor.pdf b/docs/src/img/ipe/fusiontensor.pdf
new file mode 100644
index 0000000..3e0044a
Binary files /dev/null and b/docs/src/img/ipe/fusiontensor.pdf differ
diff --git a/docs/src/img/ipe/logo.ipe b/docs/src/img/ipe/logo.ipe
new file mode 100644
index 0000000..76508a9
--- /dev/null
+++ b/docs/src/img/ipe/logo.ipe
@@ -0,0 +1,872 @@
+
+
+
+
+
+
+
+0 0 m
+-1 0.333 l
+-1 -0.333 l
+h
+
+
+
+
+0 0 m
+-1 0.333 l
+-1 -0.333 l
+h
+
+
+
+
+0 0 m
+-1 0.333 l
+-0.8 0 l
+-1 -0.333 l
+h
+
+
+
+
+0 0 m
+-1 0.333 l
+-0.8 0 l
+-1 -0.333 l
+h
+
+
+
+
+0.6 0 0 0.6 0 0 e
+0.4 0 0 0.4 0 0 e
+
+
+
+
+0.6 0 0 0.6 0 0 e
+
+
+
+
+
+0.5 0 0 0.5 0 0 e
+
+
+0.6 0 0 0.6 0 0 e
+0.4 0 0 0.4 0 0 e
+
+
+
+
+
+-0.6 -0.6 m
+0.6 -0.6 l
+0.6 0.6 l
+-0.6 0.6 l
+h
+-0.4 -0.4 m
+0.4 -0.4 l
+0.4 0.4 l
+-0.4 0.4 l
+h
+
+
+
+
+-0.6 -0.6 m
+0.6 -0.6 l
+0.6 0.6 l
+-0.6 0.6 l
+h
+
+
+
+
+
+-0.5 -0.5 m
+0.5 -0.5 l
+0.5 0.5 l
+-0.5 0.5 l
+h
+
+
+-0.6 -0.6 m
+0.6 -0.6 l
+0.6 0.6 l
+-0.6 0.6 l
+h
+-0.4 -0.4 m
+0.4 -0.4 l
+0.4 0.4 l
+-0.4 0.4 l
+h
+
+
+
+
+
+
+-0.43 -0.57 m
+0.57 0.43 l
+0.43 0.57 l
+-0.57 -0.43 l
+h
+
+
+-0.43 0.57 m
+0.57 -0.43 l
+0.43 -0.57 l
+-0.57 0.43 l
+h
+
+
+
+
+
+0 0 m
+-1 0.333 l
+-1 -0.333 l
+h
+
+
+
+
+0 0 m
+-1 0.333 l
+-0.8 0 l
+-1 -0.333 l
+h
+
+
+
+
+0 0 m
+-1 0.333 l
+-0.8 0 l
+-1 -0.333 l
+h
+
+
+
+
+-1 0.333 m
+0 0 l
+-1 -0.333 l
+
+
+
+
+0 0 m
+-1 0.333 l
+-1 -0.333 l
+h
+-1 0 m
+-2 0.333 l
+-2 -0.333 l
+h
+
+
+
+
+0 0 m
+-1 0.333 l
+-1 -0.333 l
+h
+-1 0 m
+-2 0.333 l
+-2 -0.333 l
+h
+
+
+
+
+0.5 0 m
+-0.5 0.333 l
+-0.5 -0.333 l
+h
+
+
+
+
+0.5 0 m
+-0.5 0.333 l
+-0.5 -0.333 l
+h
+
+
+
+
+0.5 0 m
+-0.5 0.333 l
+-0.3 0 l
+-0.5 -0.333 l
+h
+
+
+
+
+0.5 0 m
+-0.5 0.333 l
+-0.3 0 l
+-0.5 -0.333 l
+h
+
+
+
+
+1 0 m
+0 0.333 l
+0 -0.333 l
+h
+0 0 m
+-1 0.333 l
+-1 -0.333 l
+h
+
+
+
+
+1 0 m
+0 0.333 l
+0 -0.333 l
+h
+0 0 m
+-1 0.333 l
+-1 -0.333 l
+h
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+64 768 m
+64 720 l
+112 720 l
+112 640 l
+144 640 l
+144 720 l
+192 720 l
+192 768 l
+64 768 l
+
+
+208 768 m
+208 704 l
+240 704 l
+
+
+208 736 m
+224 736 l
+
+
+208 768 m
+240 768 l
+
+
+272 768 m
+272 704 l
+
+
+272 768 m
+304 704 l
+
+
+304 704 m
+304 768 l
+
+
+368 768 m
+336 768 l
+336 736 l
+368 736 l
+368 704 l
+336 704 l
+
+
+400 768 m
+400 704 l
+
+
+400 704 m
+432 704 l
+432 768 l
+400 768 l
+
+
+464 768 m
+464 736 l
+496 736 l
+496 768 l
+464 768 l
+
+
+464 736 m
+464 704 l
+464 736 l
+496 704 l
+
+
+240 704 m
+272 704 l
+272 672 l
+240 672 l
+240 704 l
+
+
+240 672 m
+240 640 l
+240 672 l
+
+
+240 672 m
+272 640 l
+
+
+304 704 m
+304 640 l
+
+
+336 640 m
+336 672 l
+336 704 l
+304 704 l
+304 672 l
+336 672 l
+
+
+400 704 m
+368 704 l
+368 640 l
+400 640 l
+
+
+432 704 m
+432 640 l
+432 672 l
+464 640 l
+432 672 l
+464 704 l
+464 704 l
+
+
+96 640 m
+88 624 l
+96 608 l
+h
+
+
+96 608 m
+88 592 l
+96 576 l
+h
+
+
+84 624 m
+88 624 l
+
+
+84 592 m
+88 592 l
+
+
+76 624 m
+68 624
+68 632 c
+
+
+76 592 m
+68 592
+68 584 c
+
+
+68 632 m
+68 640 l
+
+
+68 584 m
+68 576 l
+
+
+96 636 m
+104 636 l
+
+
+96 612 m
+104 612 l
+
+
+96 604 m
+104 604 l
+
+
+96 580 m
+104 580 l
+
+
+80 632 m
+84 632 l
+84 584 l
+80 584 l
+80 584 l
+h
+
+
+4 0 0 4 468 736 e
+4 0 0 4 492 760 e
+4 0 0 4 492 712 e
+468 740 m
+468 752
+468 752 c
+468 752 m
+468 760
+476 760 c
+476 760 m
+488 760 l
+488 760 m
+488 760 l
+492 708 m
+492 704 l
+496 712 m
+500 712 l
+492 756 m
+492 752 l
+496 760 m
+500 760 l
+492 764 m
+492 768 l
+468 732 m
+468 728 l
+464 736 m
+460 736 l
+4 0 0 4 468 712 e
+468 708 m
+468 704 l
+464 712 m
+460 712 l
+472 736 m
+484 736 l
+484 736 m
+492 736
+492 744 c
+492 744 m
+492 752 l
+468 728 m
+468 716 l
+470.828 733.172 m
+489.172 714.828 l
+
+
+4 0 0 4 276 760 e
+4 0 0 4 276 712 e
+4 0 0 4 276 736 e
+4 0 0 4 300 712 e
+4 0 0 4 300 736 e
+4 0 0 4 300 760 e
+276 756 m
+276 740 l
+276 732 m
+276 716 l
+276 708 m
+276 704 l
+280 712 m
+284 712 l
+280 736 m
+284 736 l
+280 760 m
+284 760 l
+304 760 m
+308 760 l
+304 736 m
+308 736 l
+304 712 m
+308 712 l
+276 764 m
+276 768
+292 768
+284 704
+300 704
+300 708 c
+300 716 m
+300 732 l
+300 740 m
+300 756 l
+300 764 m
+300 768 l
+
+
+4 0 0 4 468 736 e
+4 0 0 4 492 760 e
+4 0 0 4 492 712 e
+468 740 m
+468 752
+468 752 c
+468 752 m
+468 760
+476 760 c
+476 760 m
+488 760 l
+488 760 m
+488 760 l
+492 708 m
+492 704 l
+496 712 m
+500 712 l
+492 756 m
+492 752 l
+496 760 m
+500 760 l
+492 764 m
+492 768 l
+468 732 m
+468 728 l
+464 736 m
+460 736 l
+4 0 0 4 468 712 e
+468 708 m
+468 704 l
+464 712 m
+460 712 l
+472 736 m
+484 736 l
+484 736 m
+492 736
+492 744 c
+492 744 m
+492 752 l
+468 728 m
+468 716 l
+470.828 733.172 m
+489.172 714.828 l
+
+
+4 0 0 4 320 696 e
+4 0 0 4 308 672 e
+4 0 0 4 332 672 e
+308 692 m
+308 676 l
+312 672 m
+328 672 l
+332 692 m
+332 676 l
+308 668 m
+308 652 l
+332 652 m
+332 668 l
+308 692 m
+308 696
+312 696 c
+328 696 m
+332 696
+332 692 c
+312 696 m
+316 696 l
+328 696 m
+324 696 l
+4 0 0 4 308 648 e
+4 0 0 4 332 648 e
+328 648 m
+324 648 l
+312 648 m
+316 648 l
+308 644 m
+308 640 l
+332 644 m
+332 640 l
+
+
+4 0 0 4 332 672 e
+4 0 0 4 332 696 e
+4 0 0 4 356 696 e
+4 0 0 4 332 644 e
+4 0 0 4 356 644 e
+353.397 647.037 m
+334.603 668.963 l
+334.603 668.963 l
+334.828 674.828 m
+353.172 693.172 l
+332 692 m
+332 676 l
+332 668 m
+332 648 l
+324 644 m
+328 644 l
+332 640 m
+332 636 l
+356 640 m
+356 636 l
+360 644 m
+364 644 l
+360 696 m
+364 696 l
+356 700 m
+356 704 l
+332 700 m
+332 704 l
+328 696 m
+324 696 l
+
+
+4 0 0 4 196 736 e
+4 0 0 4 216 760 e
+4 0 0 4 216 712 e
+196 740 m
+196 752
+196 752 c
+196 752 m
+196 760
+204 760 c
+196 732 m
+196 720 l
+196 720 m
+196 712
+204 712 c
+216 764 m
+216 768 l
+220 760 m
+224 760 l
+216 756 m
+216 752 l
+216 716 m
+216 720 l
+220 712 m
+224 712 l
+216 708 m
+216 704 l
+200 736 m
+212 736 l
+212 760 m
+204 760 l
+212 712 m
+204 712 l
+
+
+4 0 0 4 180 760 e
+4 0 0 4 148 760 e
+4 0 0 4 164 760 e
+4 0 0 4 164 736 e
+4 0 0 4 164 712 e
+164 756 m
+164 740 l
+156 760 m
+160 760 l
+168 760 m
+172 760 l
+180 756 m
+180 752 l
+148 756 m
+148 752 l
+164 732 m
+164 716 l
+164 708 m
+164 704 l
+168 712 m
+172 712 l
+160 712 m
+156 712 l
+176 760 m
+172 760 l
+156 760 m
+152 760 l
+
+
+4 0 0 4 180 760 e
+4 0 0 4 148 760 e
+4 0 0 4 164 760 e
+4 0 0 4 164 736 e
+4 0 0 4 164 712 e
+164 756 m
+164 740 l
+156 760 m
+160 760 l
+168 760 m
+172 760 l
+180 756 m
+180 752 l
+148 756 m
+148 752 l
+164 732 m
+164 716 l
+164 708 m
+164 704 l
+168 712 m
+172 712 l
+160 712 m
+156 712 l
+176 760 m
+172 760 l
+156 760 m
+152 760 l
+
+
+4 0 0 4 276 736 e
+4 0 0 4 300 760 e
+4 0 0 4 300 712 e
+276 740 m
+276 752
+276 752 c
+276 752 m
+276 760
+284 760 c
+284 760 m
+296 760 l
+296 760 m
+296 760 l
+280 736 m
+292 736
+292 736 c
+292 736 m
+300 736
+300 728 c
+300 728 m
+300 716
+300 716 c
+296 712 m
+292 712 l
+300 708 m
+300 704 l
+304 712 m
+308 712 l
+300 756 m
+300 752 l
+304 760 m
+308 760 l
+300 764 m
+300 768 l
+276 732 m
+276 728 l
+4 0 0 4 276 712 e
+280 712 m
+292 712 l
+276 708 m
+276 704 l
+
+
+4 0 0 4 308 696 e
+4 0 0 4 288 672 e
+4 0 0 4 308 644 e
+296 644 m
+288 644
+288 652 c
+288 652 m
+288 668
+288 668 c
+288 676 m
+288 688
+288 688 c
+288 688 m
+288 696
+296 696 c
+308 692 m
+308 688 l
+308 648 m
+308 652 l
+312 644 m
+316 644 l
+312 696 m
+316 696 l
+304 696 m
+296 696 l
+304 644 m
+296 644 l
+
+
+4 0 0 4 316 760 e
+4 0 0 4 340 760 e
+4 0 0 4 340 712 e
+4 0 0 4 316 712 e
+316 756 m
+316 716 l
+320 712 m
+336 712 l
+340 716 m
+340 756 l
+336 760 m
+320 760 l
+340 764 m
+340 768 l
+344 760 m
+348 760 l
+312 712 m
+308 712 l
+316 708 m
+316 704 l
+
+
+
diff --git a/docs/src/img/logo.png b/docs/src/img/logo.png
new file mode 100644
index 0000000..0b3ba76
Binary files /dev/null and b/docs/src/img/logo.png differ
diff --git a/docs/src/img/mpo.svg b/docs/src/img/mpo.svg
new file mode 100644
index 0000000..a66d1e9
--- /dev/null
+++ b/docs/src/img/mpo.svg
@@ -0,0 +1,128 @@
+
+
diff --git a/docs/src/img/mps.svg b/docs/src/img/mps.svg
new file mode 100644
index 0000000..4af2f7e
--- /dev/null
+++ b/docs/src/img/mps.svg
@@ -0,0 +1,60 @@
+
+
diff --git a/docs/src/index.rst b/docs/src/index.rst
new file mode 100644
index 0000000..4caff35
--- /dev/null
+++ b/docs/src/index.rst
@@ -0,0 +1,38 @@
+TensorTrack
+============
+
+*An open-source tensor network library for MATLAB.*
+
+
+Package summary
+----------------
+
+This is a package which aims to efficiently implement the various elementary tensor algorithms that arise in the context of tensor networks.
+This includes various index manipulations, as well as contractions, factorizations, eigen- and linear solvers, ...
+Additionally, for tensors which are invariant under general global symmetries, various mechanisms are in place to minimize both memory and cpu usage.
+
+
+.. toctree::
+ :caption: Manual
+ :maxdepth: 2
+
+ man/intro
+ man/tensors
+ man/symmetries
+
+
+.. toctree::
+ :caption: Tutorials
+ :maxdepth: 1
+
+ examples/examples.rst
+
+
+.. toctree::
+ :caption: Library
+ :maxdepth: 2
+
+ lib/tensors
+ lib/utility
+ lib/caches
+
diff --git a/docs/src/lib/caches.rst b/docs/src/lib/caches.rst
new file mode 100644
index 0000000..323cbc7
--- /dev/null
+++ b/docs/src/lib/caches.rst
@@ -0,0 +1,17 @@
+Caches
+======
+
+.. toctree::
+ :maxdepth: 2
+
+.. module:: src
+
+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/tensors.rst b/docs/src/lib/tensors.rst
new file mode 100644
index 0000000..3c1e106
--- /dev/null
+++ b/docs/src/lib/tensors.rst
@@ -0,0 +1,46 @@
+Tensors
+=======
+
+.. toctree::
+ :maxdepth: 2
+
+.. module:: src
+
+This section contains the API documentation for the :mod:`.tensors` module.
+
+
+Symmetry sectors
+----------------
+
+.. automodule:: src.tensors.charges
+
+
+Fusion trees
+------------
+
+.. automodule:: src.tensors.trees
+
+
+Spaces
+------
+
+.. automodule:: src.tensors.spaces
+
+
+Kernels
+-------
+
+.. automodule:: src.tensors.kernels
+
+
+Tensors
+-------
+
+.. automodule:: src.tensors.tensors
+
+
+Arrays
+------
+
+.. automodule:: src.tensors.arrays
+
diff --git a/docs/src/lib/utility.rst b/docs/src/lib/utility.rst
new file mode 100644
index 0000000..8c77b18
--- /dev/null
+++ b/docs/src/lib/utility.rst
@@ -0,0 +1,49 @@
+Utility
+=======
+
+.. toctree::
+ :maxdepth: 2
+
+.. module:: src
+
+This section contains the API documentation for the :mod:`.utility` module.
+
+.. automodule:: src.utility
+
+
+Indices
+-------
+
+.. automodule:: src.utility.indices
+
+
+Linear algebra
+--------------
+
+.. automodule:: src.utility.linalg
+
+
+Permutations
+------------
+
+.. automodule:: src.utility.permutations
+
+
+Uninit
+------
+
+.. automodule:: src.utility.uninit
+
+
+Validations
+-----------
+
+.. automodule:: src.utility.validations
+
+
+Wigner
+------
+
+.. automodule:: src.utility.wigner
+
+
diff --git a/docs/src/man/intro.rst b/docs/src/man/intro.rst
new file mode 100644
index 0000000..cbbdb1e
--- /dev/null
+++ b/docs/src/man/intro.rst
@@ -0,0 +1,87 @@
+Introduction
+============
+
+A note.
+
+.. note::
+
+ Something to be noted.
+
+A warning
+
+.. warning::
+
+ Something to be wary of.
+
+A reference
+
+.. seealso::
+
+ Something that should also be seen.
+
+A matlab code block.
+
+.. code-block:: matlab
+
+ function output = myFunction(input)
+ output = input;
+ end
+
+A python code block.
+
+.. code-block:: python
+
+ pip install myst-parser
+ extensions = ['myst_parser']
+
+A snippet: ``code-snippet``, `other-code-snippet`
+
+A command prompt block.
+
+.. prompt:: bash $
+
+ cd docs/
+ make html
+
+A button: :guilabel:`member`.
+
+
+Section 1
+---------
+
+Testing math: :math:`x_{hey}=it+is^{math}`
+
+
+Testing equation:
+
+.. math::
+ e^{i\pi} + 1 = 0
+ :label: euler
+
+This is the best equation :eq:`euler`
+
+Testing more equations:
+
+.. math::
+ \mathcal{Z} = \left (\prod_{i} \int \frac{\text{d}\Omega_i}{4\pi}\right ) \left (\prod_{\braket{ij}} \text{e}^{\beta\left(\vec{s}_i \cdot \vec{s}_j\right)^p}\right ).
+ :label: eqn:best
+
+This is the best equation :eq:`eqn:best`
+
+
+Section 2
+---------
+
+Testing ipe images: should be imported as .svg
+
+.. image:: ../img/mps.svg
+ :alt: mps
+ :scale: 5 %
+ :align: center
+
+.. image:: ../img/mpo.svg
+ :alt: mps
+ :scale: 5 %
+ :align: center
+
+
diff --git a/docs/src/man/symmetries.rst b/docs/src/man/symmetries.rst
new file mode 100644
index 0000000..07f3df8
--- /dev/null
+++ b/docs/src/man/symmetries.rst
@@ -0,0 +1,2 @@
+Symmetries
+==========
diff --git a/docs/src/man/tensors.rst b/docs/src/man/tensors.rst
new file mode 100644
index 0000000..945be55
--- /dev/null
+++ b/docs/src/man/tensors.rst
@@ -0,0 +1,51 @@
+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:
+
+* 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)``, ...
+We will use a graphical representation of such a tensor as an object with several legs, where each leg represents a tensor index. By convention we will order the indices from top-left to top-right, going around the object counter-clockwise.
+
+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)))``.
+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))``.
+
+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:
+
+.. math::
+ t \in V_1 \otimes V_2 \otimes \dots \otimes V_n.
+
+Then we can reinterpret this as a map, by partitioning some of the indices to the domain:
+
+.. math::
+ t^\prime : V_1 \otimes V_2 \otimes \dots \otimes V_{n_1} \leftarrow V_{n_2}^* \otimes V_{n_2-1}^* \otimes \dots \otimes V_{n_1+1}^*.
+
+However, in doing so, we had to reverse their order.
+
+In summary, we consider a tensor to be an object which can be represented in either one of the previously discussed forms, i.e. as a multidimensional array, or as a matrix.
+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.
+
+
+Index manipulations
+-------------------
+
+
+Contractions
+------------
+
+
+Factorizations
+--------------
diff --git a/src/caches/DLL.m b/src/caches/DLL.m
new file mode 100644
index 0000000..0c3a775
--- /dev/null
+++ b/src/caches/DLL.m
@@ -0,0 +1,124 @@
+classdef DLL < handle
+ % An element of a doubly-linked list. This is a list where each element consists of a
+ % value, a reference to the previous element and a reference to the next element.
+ %
+ % 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.
+
+ properties
+ val % data stored in this element
+ end
+
+ properties (Access = private)
+ next % reference to next DLL object
+ prev % reference to previous DLL object
+ end
+
+ methods
+ function obj = DLL(val)
+ % Construct an element of a doubly-linked list. By default, the detached links
+ % point to the object itself.
+ %
+ % Arguments
+ % ---------
+ % val : any
+ % data stored in this element.
+ %
+ % Returns
+ % -------
+ % dll : :class:`DLL`
+ % data wrapped in a doubly-linked list format.
+
+ obj.val = val;
+ obj.next = obj;
+ obj.prev = obj;
+ end
+
+ function obj = pop(obj)
+ % Remove an object from a doubly-linked list, updating the links on the next and
+ % previous element.
+ %
+ % Arguments
+ % ---------
+ % obj : :class:`DLL`
+ % object to remove from the list.
+ %
+ % Returns
+ % -------
+ % obj : :class:`DLL`
+ % removed object, with detached links.
+
+ obj.prev.next = obj.next;
+ obj.next.prev = obj.prev;
+ obj.next = obj;
+ obj.prev = obj;
+ end
+
+ function obj = append(obj, other)
+ % Append an object to a doubly-linked list, updating the links.
+ %
+ % Arguments
+ % ---------
+ % obj : :class:`DLL`
+ % list to append to.
+ %
+ % other : :class:`DLL`
+ % object to append.
+ %
+ % Returns
+ % -------
+ % obj : :class:`DLL`
+ % updated list.
+
+ other.next = obj.next;
+ other.prev = obj;
+ obj.next.prev = other;
+ obj.next = other;
+ end
+
+ function other = uplus(obj)
+ % Get the next element in the list.
+ %
+ % Usage
+ % -----
+ % :code:`other = +obj;`
+ %
+ % :code:`other = uplus(obj);`
+ %
+ % Arguments
+ % ---------
+ % obj : :class`DLL`
+ % current element in the list.
+ %
+ % Returns
+ % -------
+ % other : :class`DLL`
+ % next element in the list.
+
+ other = obj.next;
+ end
+
+ function other = uminus(obj)
+ % Get the previous element in the list.
+ %
+ % Usage
+ % -----
+ % :code:`other = -obj;`
+ %
+ % :code:`other = uminus(obj);`
+ %
+ % Arguments
+ % ---------
+ % obj : :class`DLL`
+ % current element in the list.
+ %
+ % Returns
+ % -------
+ % other : :class`DLL`
+ % previous element in the list.
+
+ other = obj.prev;
+ end
+ end
+end
\ No newline at end of file
diff --git a/src/caches/GetMD5/GetMD5.c b/src/caches/GetMD5/GetMD5.c
new file mode 100644
index 0000000..329c191
--- /dev/null
+++ b/src/caches/GetMD5/GetMD5.c
@@ -0,0 +1,919 @@
+// GetMD5.c
+// GetMD5 - 128 bit MD5 checksum: file, string, array, byte stream
+// This function calculates a 128 bit checksum for arrays or files.
+// Digest = GetMD5(Data, Mode, Format)
+// INPUT:
+// Data: File name or array.
+// Mode: CHAR to declare the type of the 1st input. Not case-sensitive.
+// 'File': Data is a file name as CHAR.
+// '8Bit': If Data is a CHAR array, only the 8 bit ASCII part is
+// used. Then the digest is the same as for a ASCII text
+// file e.g. created by: FWRITE(FID, Data, 'uchar').
+// This is ignored if Data is not of type CHAR.
+// 'Binary': The MD5 sum is obtained for the contents of Data.
+// This works for numerical, CHAR and LOGICAL arrays.
+// 'Array': Include the class and dimensions of Data in the MD5
+// sum. This can be applied for (nested) structs, cells
+// and sparse arrays also.
+// Optional. Default: '8Bit' for CHAR, 'Binary' otherwise.
+// Format: CHAR, format of the output. Only the first character matters.
+// The upper/lower case matters for 'hex' only.
+// 'hex': [1 x 32] lowercase hexadecimal CHAR.
+// 'HEX': [1 x 32] uppercase hexadecimal CHAR.
+// 'double': [1 x 16] double vector with UINT8 values.
+// 'uint8': [1 x 16] uint8 vector.
+// 'base64': [1 x 22] CHAR, encoded to base 64 (A:Z,a:z,0:9,+,/).
+// The CHAR is not padded to keep it short.
+// Optional, default: 'hex'.
+//
+// OUTPUT:
+// Digest: A 128 bit number is replied in the specified format.
+//
+// NOTE:
+// * The M-file GetMD5_helper is called for sparse arrays, function handles,
+// java and user-defined objects .
+// * This is at least 2 times faster than the Java method.
+//
+// EXAMPLES:
+// Three methods to get the MD5 of a file:
+// 1. Direct file access (recommended):
+// MD5 = GetMD5(which('GetMD5.m'), 'File')
+// 2. Import the file to a CHAR array (no text mode for exact line breaks!):
+// FID = fopen(which('GetMD5.m'), 'r');
+// S = fread(FID, inf, 'uchar=>char');
+// fclose(FID);
+// MD5 = GetMD5(S, '8bit')
+// 3. Import file as a byte stream:
+// FID = fopen(which('GetMD5.m'), 'r');
+// S = fread(FID, inf, 'uint8=>uint8');
+// fclose(FID);
+// MD5 = GetMD5(S, 'bin'); % 'bin' is optional here
+//
+// Test data:
+// GetMD5(char(0:511), '8bit', 'HEX') % Consider 8bit part only
+// % => F5C8E3C31C044BAE0E65569560B54332
+// GetMD5(char(0:511), 'bin') % Matlab's CHAR are 16 bit!
+// % => 3484769D4F7EBB88BBE942BB924834CD
+// GetMD5(char(0:511), 'array') % Consider 16 bit, type and size
+// % => b9a955ae730b25330d4f4ebb0a51e8f0
+// GetMD5('abc') % implicit: 8bit for CHAR input
+// % => 900150983cd24fb0d6963f7d28e17f72
+//
+// COMPILE:
+// On demand a C-compiler must be installed at first, see: "mex -setup"
+// Automatic: Call GetMD5 without inputs.
+// Manually:
+// mex -O GetMD5.c
+// Consider C99 comments under Linux:
+// mex -O CFLAGS="\$CFLAGS -std=c99" GetMD5.c
+// Pre-compiled MEX files can be downloaded: http:\\www.n-simon.de\mex
+// Run the unit-test uTest_GetMD5 after the compilation.
+//
+// Tested: Matlab 6.5, 7.7, 7.8, 7.13, 8.6, 9.1, 9.5, WinXP/32, Win7&10/64
+// Compiler: LCC2.4/3.8, BCC5.5, OWC1.8, MSVC2008/2010/2017
+// Assumed Compatibility: higher Matlab versions, Mac, Linux
+// Author: Jan Simon, Heidelberg, (C) 2006-2019 matlab.2010(a)n(MINUS)simon.de
+// License: BSD. This program is based on:
+// RFC 1321, MD5 Message-Digest Algorithm, April 1992
+// RSA Data Security, Inc. MD5 Message Digest Algorithm
+// Implementation: Alexander Peslyak
+//
+// See also: CalcCRC32, DataHash.
+//
+// Michael Kleder has published a Java call to compute the MD5 and SHA sums:
+// http://www.mathworks.com/matlabcentral/fileexchange/8944
+
+/*******************************************************************************
+ * The MD5-part is based on:
+ *
+ * This is an OpenSSL-compatible implementation of the RSA Data Security, Inc.
+ * MD5 Message-Digest Algorithm (RFC 1321).
+ *
+ * Homepage:
+ * http://openwall.info/wiki/people/solar/software/public-domain-source-code/md5
+ *
+ * Author:
+ * Alexander Peslyak, better known as Solar Designer
+ *
+ * This software was written by Alexander Peslyak in 2001. No copyright is
+ * claimed, and the software is hereby placed in the public domain.
+ *******************************************************************************
+ */
+
+/*
+% $JRev: R5k V:066 Sum:4vIkvzaOU7o4 Date:04-Mar-2019 16:20:23 $
+% $License: BSD (use/copy/change/redistribute on own risk, mention the author) $
+% $UnitTest: uTest_GetMD5 $
+% $File: Tools\Mex\Source\GetMD5.c $
+% History:
+% 011: 20-Oct-2006 20:50, [16 x 1] -> [1 x 16] replied as double.
+% 012: 01-Nov-2006 23:10, BUGFIX: hex output for 'Hex' input now.
+% 015: 02-Oct-2008 14:47, Base64 output.
+% 017: 19-Oct-2008 22:33, Accept numerical arrays as byte stream.
+% 023: 15-Dec-2009 16:53, BUGFIX: UINT32 has 32 bits on 64 bit systems now.
+% Thanks to Sebastiaan Breedveld!
+% 030: 17-Mar-2010 11:38, UINT8 output.
+% 032: 06-Jul-2010 23:23, Indirect CONST pointer in ToHex for BCC5.5.
+% 037: 14-May-2011 13:14, Default input type: char->byte.
+% 042: 27-Jan-2015 23:00, 64 bit arrays, nicer error messages, 10% faster.
+% "CalcMD5" -> "GetMD5".
+% 046: 16-Feb-2015 00:10, "Array" type: consider type and dimensions.
+% The "Array" type works for cells and structs also.
+% 050: 09-Mar-2015 23:08, Faster hash code of Alexander Peslyak.
+% 060: 04-Jun-2016 22:22, Fixed compile error on Macs.
+% Thanks to Jonas Zimmermann.
+% 061: 16-Oct-2017 23:56, Compilation failed if _LITTLE_ENDIAN is undefined.
+% 062: 30-Jan-2018 01:41, Crashed for NULL pointer in data array.
+% 064: 27-Aug-2018 00:53, String class caught in GetMD5_Helper.
+*/
+
+#define __STDC_WANT_LIB_EXT1__ 1
+
+// Headers:
+#include
+#include
+#include
+#include
+#include "mex.h"
+
+// Assume 32 bit addressing for Matlab 6.5:
+// See MEX option "compatibleArrayDims" for MEX in Matlab >= 7.7.
+#ifndef MWSIZE_MAX
+# define mwSize int32_T // Defined in tmwtypes.h
+# define mwIndex int32_T
+# define MWSIZE_MAX MAX_int32_T
+#endif
+
+// Directive for endianess:
+#if !defined(_LITTLE_ENDIAN)
+# if !defined(_BIG_ENDIAN)
+# define _LITTLE_ENDIAN 1
+# else
+# define _LITTLE_ENDIAN !_BIG_ENDIAN
+# endif
+#endif
+
+// Safe string length for fieldnames:
+#if defined _MSC_VER || \
+ (defined(__STDC_LIB_EXT1__) && __STDC_WANT_LIB_EXT1__ >= 1)
+# define STRING_LENGTH(s,n) strnlen_s(s,n)
+#else
+# define STRING_LENGTH(s,n) strlen(s)
+#endif
+
+// Strange objects can cause an infinite recursion and kill Matlab. So limit the
+// recursion depths for a useful error message:
+#define MAX_RECURSION 500
+static int RecursionCount;
+
+// Undocumented API function, required to forward string objects to the helper
+// function:
+// bool mxIsOpaque(const mxArray *);
+
+// MD5 part: -------------------------------------------------------------------
+typedef struct {
+ uint32_T lo, hi;
+ uint32_T a, b, c, d;
+ uchar_T buffer[64];
+ uint32_T block[16];
+} MD5_CTX;
+
+// Prototypes:
+void MD5_Init(MD5_CTX *ctx);
+void MD5_Update(MD5_CTX *ctx, uchar_T *data, mwSize size);
+void MD5_Final(uchar_T *digest, MD5_CTX *ctx);
+static uchar_T *MD5_Body(MD5_CTX *ctx, uchar_T *data, mwSize size);
+
+/* The basic MD5 functions.
+ *
+ * F and G are optimized compared to their RFC 1321 definitions for
+ * architectures that lack an AND-NOT instruction, just like in Colin Plumb's
+ * implementation.
+ */
+#define F(x, y, z) ((z) ^ ((x) & ((y) ^ (z))))
+#define G(x, y, z) ((y) ^ ((z) & ((x) ^ (y))))
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+#define I(x, y, z) ((y) ^ ((x) | ~(z)))
+
+/* The MD5 transformation for all four rounds. */
+#define STEP(f, a, b, c, d, x, t, s) \
+ (a) += f((b), (c), (d)) + (x) + (t); \
+ (a) = (((a) << (s)) | (((a) & 0xffffffff) >> (32 - (s)))); \
+ (a) += (b);
+
+/* SET reads 4 input bytes in little-endian byte order and stores them in a
+ * properly aligned word in host byte order.
+ *
+ * The check for little-endian architectures that tolerate unaligned memory
+ * accesses is just an optimization. Nothing will break if it doesn't work.
+ */
+#if _LITTLE_ENDIAN
+# define SET(n) (*(uint32_T *)&ptr[(n) * 4])
+# define GET(n) SET(n)
+#else
+# define SET(n) \
+ (ctx->block[(n)] = \
+ (uint32_T)ptr[(n) * 4] | \
+ ((uint32_T)ptr[(n) * 4 + 1] << 8) | \
+ ((uint32_T)ptr[(n) * 4 + 2] << 16) | \
+ ((uint32_T)ptr[(n) * 4 + 3] << 24))
+# define GET(n) (ctx->block[(n)])
+#endif
+
+// Matlab part: ----------------------------------------------------------------
+// Length of the file buffer (must be < 2^31 for 32 bit machines):
+#define BUFFER_LEN 1024
+static uchar_T buffer[BUFFER_LEN];
+
+typedef enum {ASCII_m, ARRAY_m, BINARY_m, FILE_m} Method_t;
+
+typedef struct StringRec {
+ const char_T *string;
+ int index;
+} StringRec;
+
+// Error and warning messages:
+#define ERR_HEAD "*** GetMD5[mex]: "
+#define ERR_ID "JSimon:GetMD5:"
+#define ERROR(id,msg) mexErrMsgIdAndTxt(ERR_ID id, ERR_HEAD msg);
+#define ERROR3(id,msg,arg) mexErrMsgIdAndTxt(ERR_ID id, ERR_HEAD msg, arg);
+
+#define WARN_HEAD "### GetMD5[mex]: "
+#define WARN(id,msg) mexWarnMsgIdAndTxt(ERR_ID id, WARN_HEAD msg);
+
+// Prototypes:
+void ToHex (const uchar_T In[16], char *Out, int LowerCase);
+void ToBase64(const uchar_T In[16], char *Out);
+
+void ProcessBin (uchar_T *data, mwSize N, uchar_T digest[16]);
+void ProcessFile (char *FileName, uchar_T digest[16]);
+void ProcessChar (mxChar *data, mwSize N, uchar_T digest[16]);
+void ProcessArray(const mxArray *V, uchar_T digest[16]);
+void ArrayCore (MD5_CTX *context, const mxArray *V);
+void StructCore (MD5_CTX *context, const mxArray *V, mwSize nElem);
+int CompareStringRec(const void *a, const void *b);
+
+// =============================== FUNCTIONS ===================================
+
+// *****************************************************************************
+// ** MD5 part:
+// *****************************************************************************
+
+// MD5 initialization. Begins an MD5 operation, writing a new context:
+void MD5_Init(MD5_CTX *ctx)
+{
+ // Load magic initialization constants:
+ ctx->a = 0x67452301;
+ ctx->b = 0xefcdab89;
+ ctx->c = 0x98badcfe;
+ ctx->d = 0x10325476;
+
+ ctx->lo = 0;
+ ctx->hi = 0;
+}
+
+// This processes one or more 64-byte data blocks, but does NOT update
+// the bit counters. There are no alignment requirements.
+static uchar_T *MD5_Body(MD5_CTX *ctx, uchar_T *data, mwSize size)
+{
+ uchar_T *ptr;
+ uint32_T a, b, c, d,
+ saved_a, saved_b, saved_c, saved_d;
+
+ ptr = data;
+
+ a = ctx->a;
+ b = ctx->b;
+ c = ctx->c;
+ d = ctx->d;
+
+ do {
+ saved_a = a;
+ saved_b = b;
+ saved_c = c;
+ saved_d = d;
+
+// Round 1
+ STEP(F, a, b, c, d, SET(0), 0xd76aa478, 7)
+ STEP(F, d, a, b, c, SET(1), 0xe8c7b756, 12)
+ STEP(F, c, d, a, b, SET(2), 0x242070db, 17)
+ STEP(F, b, c, d, a, SET(3), 0xc1bdceee, 22)
+ STEP(F, a, b, c, d, SET(4), 0xf57c0faf, 7)
+ STEP(F, d, a, b, c, SET(5), 0x4787c62a, 12)
+ STEP(F, c, d, a, b, SET(6), 0xa8304613, 17)
+ STEP(F, b, c, d, a, SET(7), 0xfd469501, 22)
+ STEP(F, a, b, c, d, SET(8), 0x698098d8, 7)
+ STEP(F, d, a, b, c, SET(9), 0x8b44f7af, 12)
+ STEP(F, c, d, a, b, SET(10), 0xffff5bb1, 17)
+ STEP(F, b, c, d, a, SET(11), 0x895cd7be, 22)
+ STEP(F, a, b, c, d, SET(12), 0x6b901122, 7)
+ STEP(F, d, a, b, c, SET(13), 0xfd987193, 12)
+ STEP(F, c, d, a, b, SET(14), 0xa679438e, 17)
+ STEP(F, b, c, d, a, SET(15), 0x49b40821, 22)
+
+// Round 2
+ STEP(G, a, b, c, d, GET(1), 0xf61e2562, 5)
+ STEP(G, d, a, b, c, GET(6), 0xc040b340, 9)
+ STEP(G, c, d, a, b, GET(11), 0x265e5a51, 14)
+ STEP(G, b, c, d, a, GET(0), 0xe9b6c7aa, 20)
+ STEP(G, a, b, c, d, GET(5), 0xd62f105d, 5)
+ STEP(G, d, a, b, c, GET(10), 0x02441453, 9)
+ STEP(G, c, d, a, b, GET(15), 0xd8a1e681, 14)
+ STEP(G, b, c, d, a, GET(4), 0xe7d3fbc8, 20)
+ STEP(G, a, b, c, d, GET(9), 0x21e1cde6, 5)
+ STEP(G, d, a, b, c, GET(14), 0xc33707d6, 9)
+ STEP(G, c, d, a, b, GET(3), 0xf4d50d87, 14)
+ STEP(G, b, c, d, a, GET(8), 0x455a14ed, 20)
+ STEP(G, a, b, c, d, GET(13), 0xa9e3e905, 5)
+ STEP(G, d, a, b, c, GET(2), 0xfcefa3f8, 9)
+ STEP(G, c, d, a, b, GET(7), 0x676f02d9, 14)
+ STEP(G, b, c, d, a, GET(12), 0x8d2a4c8a, 20)
+
+// Round 3
+ STEP(H, a, b, c, d, GET(5), 0xfffa3942, 4)
+ STEP(H, d, a, b, c, GET(8), 0x8771f681, 11)
+ STEP(H, c, d, a, b, GET(11), 0x6d9d6122, 16)
+ STEP(H, b, c, d, a, GET(14), 0xfde5380c, 23)
+ STEP(H, a, b, c, d, GET(1), 0xa4beea44, 4)
+ STEP(H, d, a, b, c, GET(4), 0x4bdecfa9, 11)
+ STEP(H, c, d, a, b, GET(7), 0xf6bb4b60, 16)
+ STEP(H, b, c, d, a, GET(10), 0xbebfbc70, 23)
+ STEP(H, a, b, c, d, GET(13), 0x289b7ec6, 4)
+ STEP(H, d, a, b, c, GET(0), 0xeaa127fa, 11)
+ STEP(H, c, d, a, b, GET(3), 0xd4ef3085, 16)
+ STEP(H, b, c, d, a, GET(6), 0x04881d05, 23)
+ STEP(H, a, b, c, d, GET(9), 0xd9d4d039, 4)
+ STEP(H, d, a, b, c, GET(12), 0xe6db99e5, 11)
+ STEP(H, c, d, a, b, GET(15), 0x1fa27cf8, 16)
+ STEP(H, b, c, d, a, GET(2), 0xc4ac5665, 23)
+
+// Round 4
+ STEP(I, a, b, c, d, GET(0), 0xf4292244, 6)
+ STEP(I, d, a, b, c, GET(7), 0x432aff97, 10)
+ STEP(I, c, d, a, b, GET(14), 0xab9423a7, 15)
+ STEP(I, b, c, d, a, GET(5), 0xfc93a039, 21)
+ STEP(I, a, b, c, d, GET(12), 0x655b59c3, 6)
+ STEP(I, d, a, b, c, GET(3), 0x8f0ccc92, 10)
+ STEP(I, c, d, a, b, GET(10), 0xffeff47d, 15)
+ STEP(I, b, c, d, a, GET(1), 0x85845dd1, 21)
+ STEP(I, a, b, c, d, GET(8), 0x6fa87e4f, 6)
+ STEP(I, d, a, b, c, GET(15), 0xfe2ce6e0, 10)
+ STEP(I, c, d, a, b, GET(6), 0xa3014314, 15)
+ STEP(I, b, c, d, a, GET(13), 0x4e0811a1, 21)
+ STEP(I, a, b, c, d, GET(4), 0xf7537e82, 6)
+ STEP(I, d, a, b, c, GET(11), 0xbd3af235, 10)
+ STEP(I, c, d, a, b, GET(2), 0x2ad7d2bb, 15)
+ STEP(I, b, c, d, a, GET(9), 0xeb86d391, 21)
+
+ a += saved_a;
+ b += saved_b;
+ c += saved_c;
+ d += saved_d;
+
+ ptr += 64;
+ } while (size -= 64);
+
+ ctx->a = a;
+ ctx->b = b;
+ ctx->c = c;
+ ctx->d = d;
+
+ return ptr;
+}
+
+void MD5_Update(MD5_CTX *ctx, uchar_T *data, mwSize size)
+{
+ uint32_T saved_lo;
+ mwSize used, free;
+
+ // Update number of bytes:
+ saved_lo = ctx->lo;
+ if ((ctx->lo = (saved_lo + size) & 0x1fffffff) < saved_lo) {
+ ctx->hi++;
+ }
+ ctx->hi += (uint32_T) (size >> 29);
+
+ // Process blocks with less than 64 bytes:
+ used = (saved_lo & 0x3f);
+ if (used) {
+ free = 64 - used;
+ if (size < free) {
+ memcpy(&ctx->buffer[used], data, size);
+ return;
+ }
+
+ memcpy(&ctx->buffer[used], data, free);
+ data = data + free;
+ size -= free;
+ MD5_Body(ctx, ctx->buffer, 64);
+ }
+
+ // Process the rest of the data in 64 byte blocks:
+ if (size >= 64) {
+ data = MD5_Body(ctx, data, size & ~(mwSize)0x3f);
+ size &= 0x3f;
+ }
+
+ // Copy remaining bytes to the buffer:
+ memcpy(ctx->buffer, data, size);
+}
+
+void MD5_Final(uchar_T *result, MD5_CTX *ctx)
+{
+ mwSize used, free;
+
+ // Padding:
+ used = ctx->lo & 0x3f;
+ ctx->buffer[used++] = 0x80;
+ free = 64 - used;
+ if (free < 8) {
+ memset(&ctx->buffer[used], 0, free);
+ MD5_Body(ctx, ctx->buffer, 64);
+ used = 0;
+ free = 64;
+ }
+
+ memset(&ctx->buffer[used], 0, free - 8);
+
+ // Encode number of bits:
+ ctx->lo <<= 3;
+ ctx->buffer[56] = ctx->lo;
+ ctx->buffer[57] = ctx->lo >> 8;
+ ctx->buffer[58] = ctx->lo >> 16;
+ ctx->buffer[59] = ctx->lo >> 24;
+ ctx->buffer[60] = ctx->hi;
+ ctx->buffer[61] = ctx->hi >> 8;
+ ctx->buffer[62] = ctx->hi >> 16;
+ ctx->buffer[63] = ctx->hi >> 24;
+
+ MD5_Body(ctx, ctx->buffer, 64);
+
+ // Copy hash to the output:
+ result[0] = ctx->a;
+ result[1] = ctx->a >> 8;
+ result[2] = ctx->a >> 16;
+ result[3] = ctx->a >> 24;
+ result[4] = ctx->b;
+ result[5] = ctx->b >> 8;
+ result[6] = ctx->b >> 16;
+ result[7] = ctx->b >> 24;
+ result[8] = ctx->c;
+ result[9] = ctx->c >> 8;
+ result[10] = ctx->c >> 16;
+ result[11] = ctx->c >> 24;
+ result[12] = ctx->d;
+ result[13] = ctx->d >> 8;
+ result[14] = ctx->d >> 16;
+ result[15] = ctx->d >> 24;
+
+ // Clean up sensitive data:
+ memset(ctx, 0, sizeof(*ctx));
+}
+
+// *****************************************************************************
+// ** Matlab part:
+// *****************************************************************************
+
+// 8Bit ASCII part of CHAR: ====================================================
+void ProcessChar(mxChar *array, mwSize inputLen, uchar_T digest[16])
+{
+ // Process string: Matlab stores strings as mxChar, which are 2 bytes per
+ // character. This function considers the first byte of each CHAR only, which
+ // is equivalent to calculate the sum after a conversion to a ASCII uchar_T
+ // string.
+ MD5_CTX context;
+ mwSize Chunk;
+ uchar_T *bufferP, *bufferEnd = buffer + BUFFER_LEN, *arrayP;
+
+ arrayP = (uchar_T *) array; // uchar_T *, not mxChar *!
+
+ MD5_Init(&context);
+
+ // Copy chunks of input data - only the first byte of each mxChar:
+ Chunk = inputLen / BUFFER_LEN;
+ while (Chunk--) {
+ bufferP = buffer;
+ while (bufferP < bufferEnd) {
+ *bufferP++ = *arrayP;
+ arrayP += 2;
+ }
+
+ MD5_Update(&context, buffer, (mwSize) BUFFER_LEN);
+ }
+
+ // Last chunk:
+ Chunk = inputLen % BUFFER_LEN;
+ if (Chunk != 0) {
+ bufferEnd = buffer + Chunk;
+ bufferP = buffer;
+ while (bufferP < bufferEnd) {
+ *bufferP++ = *arrayP;
+ arrayP += 2;
+ }
+
+ MD5_Update(&context, buffer, Chunk);
+ }
+
+ MD5_Final(digest, &context);
+}
+
+// Array of any type: ==========================================================
+void ProcessArray(const mxArray *V, uchar_T digest[16])
+{
+ // The type, dimension and contents of the array are considered. This works
+ // for cells and structs also.
+ // Here only the initialization and finalization of the context is performed,
+ // while the actual processing is done in ArrayCore, which allows recursion
+ // for cells and structs.
+ MD5_CTX context;
+
+ RecursionCount = 0; // Reset global recursion counter
+
+ MD5_Init(&context);
+ ArrayCore(&context, V);
+ MD5_Final(digest, &context);
+}
+
+// -----------------------------------------------------------------------------
+void ArrayCore(MD5_CTX *context, const mxArray *V)
+{
+ // Process an array considering the type and dimensions. Cells and Structs
+ // call this function recursively for each cell element or field.
+ // The header before the data block is: [ClassName, nDim, Dims]. The ClassName
+ // is a string, because the ClassID number has been changed during different
+ // Matlab versions in the past.
+ // Sparse arrays, function handles, java- and user-defined classes are
+ // forwarded to the M-function GetMD5_helper, where the user can defined how
+ // the data is converted to a byte stream.
+
+ uchar_T *dataReal, *dataImag;
+ mxClassID ClassID;
+ const mwSize *Dim, nullDim[2] = {0,0};
+ mwSize nDim, nElem, iElem, i, lenHeader;
+ int64_T *header;
+ size_t ElemSize, Len;
+ int nField, iField, ok;
+ bool dataOpaq;
+ mxArray *Arg[1];
+ const char *FieldName, *ClassName;
+
+ // Get header information of the array:
+ if (V != NULL) {
+ nDim = mxGetNumberOfDimensions(V);
+ nElem = mxGetNumberOfElements(V);
+ ClassID = mxGetClassID(V);
+ ClassName = mxGetClassName(V);
+ ElemSize = mxGetElementSize(V);
+ Dim = mxGetDimensions(V);
+ dataReal = (uchar_T *) mxGetData(V);
+ dataImag = (uchar_T *) mxGetImagData(V);
+ dataOpaq = mxIsOpaque(V);
+
+ // Forward sparse arrays to M-helper function:
+ if (mxIsSparse(V)) {
+ ClassID = mxUNKNOWN_CLASS;
+ }
+
+
+ } else { // NULL pointer is equivalent to [0 x 0] double matrix:
+ nDim = 2;
+ nElem = 0;
+ ClassID = mxDOUBLE_CLASS;
+ ClassName = "double";
+ ElemSize = sizeof(double);
+ Dim = nullDim;
+ dataReal = (uchar_T *) NULL;
+ dataImag = (uchar_T *) NULL;
+ dataOpaq = false;
+ }
+
+ // 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) {
+ // Consider class as name, not as ClassID, because the later might change
+ // with the Matlab version:
+ Len = strlen(ClassName);
+ MD5_Update(context, (uchar_T *) ClassName, Len * sizeof(char));
+
+ // Encode dimensions as [nDim, Dim]:
+ // Size as int64_T to get the same hash under 32 and 64 bit systems:
+ lenHeader = 1 + nDim;
+ header = (int64_T *) mxCalloc(lenHeader, sizeof(int64_T));
+ header[0] = (int64_T) nDim;
+ for (i = 0; i < nDim; i++) {
+ header[i + 1] = (int64_T) Dim[i];
+ }
+ MD5_Update(context, (uchar_T *) header, lenHeader * sizeof(int64_T));
+ mxFree(header);
+ }
+
+ // Include the contents of the array:
+ switch (ClassID) {
+ case mxLOGICAL_CLASS: // Elementary array: ------------------------------
+ case mxCHAR_CLASS:
+ case mxDOUBLE_CLASS:
+ case mxSINGLE_CLASS:
+ case mxINT8_CLASS:
+ case mxUINT8_CLASS:
+ case mxINT16_CLASS:
+ case mxUINT16_CLASS:
+ case mxINT32_CLASS:
+ case mxUINT32_CLASS:
+ case mxINT64_CLASS:
+ case mxUINT64_CLASS:
+ MD5_Update(context, dataReal, nElem * ElemSize);
+ if (dataImag != NULL) {
+ MD5_Update(context, dataImag, nElem * ElemSize);
+ }
+ break;
+
+ case mxCELL_CLASS: // Cell array - recursion: --------------------------
+ for (iElem = 0; iElem < nElem; iElem++) {
+ ArrayCore(context, mxGetCell(V, iElem));
+ }
+ break;
+
+ case mxSTRUCT_CLASS: // Struct array: Fieldnames + recursion: ------------
+ StructCore(context, V, nElem);
+ 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"
+ "Try: GetMD5(getByteStreamFromArray(Data))");
+ }
+
+ // Call the M-helper function to be more flexible:
+ ok = mexCallMATLAB(1, Arg, 1, &V, "GetMD5_helper");
+ if (ok != 0) {
+ ERROR("HelperFailed", "Calling GetMD5_helper failed.");
+ }
+
+ // Get hash for array replied by the helper:
+ if (mxIsUint8(Arg[0])) { // Byte stream:
+ MD5_Update(context, (uchar_T *) mxGetData(Arg[0]),
+ mxGetNumberOfElements(Arg[0]));
+ } else { // Not a byte stream, recursive call:
+ ArrayCore(context, Arg[0]);
+ }
+
+ // Clean up:
+ if (Arg[0] != NULL) {
+ mxDestroyArray(Arg[0]);
+ }
+ }
+}
+
+// Core function to process structs: ===========================================
+void StructCore(MD5_CTX *context, const mxArray *V, mwSize nElem)
+{
+ // Sort field names alphabetically to avoid effects of teh order of fields.
+ const char *FieldName;
+ int nField, iField, FieldIndex;
+ mwSize iElem;
+ size_t FieldNameLen;
+ StringRec *FieldList;
+
+ // Create list of field names and a handle array, which points to this list.
+ // Then sorting the handles allows to get the sorting index:
+ nField = mxGetNumberOfFields(V);
+ FieldList = (StringRec *) mxMalloc(nField * sizeof(StringRec));
+ for (iField = 0; iField < nField; iField++) {
+ FieldList[iField].string = mxGetFieldNameByNumber(V, iField);
+ FieldList[iField].index = iField;
+ }
+
+ // Sort the strings:
+ // (Sorting must not be stable, because the fieldnames are unique)
+ qsort(FieldList, nField, sizeof(StringRec), CompareStringRec);
+
+ // Loop over fields:
+ for (iField = 0; iField < nField; iField++) {
+ // Encode field name:
+ FieldName = FieldList[iField].string;
+ FieldIndex = FieldList[iField].index;
+ FieldNameLen = STRING_LENGTH(FieldName, 63);
+ MD5_Update(context, (uchar_T *) FieldName, FieldNameLen);
+
+ // Loop over struct array:
+ for (iElem = 0; iElem < nElem; iElem++) {
+ ArrayCore(context, mxGetFieldByNumber(V, iElem, FieldIndex));
+ }
+ }
+
+ // Release memory:
+ mxFree(FieldList);
+}
+
+// Comparison for qsort(): =====================================================
+int CompareStringRec(const void *va, const void *vb)
+{
+ const StringRec *a = (const StringRec *)va,
+ *b = (const StringRec *)vb;
+ return strcmp(a->string, b->string);
+}
+
+// Elementary array as byte stream: ============================================
+void ProcessBin(uchar_T *array, mwSize inputLen, uchar_T digest[16])
+{
+ // Only the contents of the array is considered. Therefore double(0) and
+ // single([0,0]) reply the same hash. This works for numeric, char and logical
+ // arrays only, neitehr cells nor structs.
+ MD5_CTX context;
+
+ MD5_Init(&context);
+ MD5_Update(&context, array, inputLen);
+ MD5_Final(digest, &context);
+}
+
+// File as byte stream: ========================================================
+void ProcessFile(char *filename, uchar_T digest[16])
+{
+ FILE *FID;
+ MD5_CTX context;
+ mwSize len;
+
+ // Open the file in binary mode:
+ if ((FID = fopen(filename, "rb")) == NULL) {
+ ERROR3("MissFile", "Cannot open file: [%s]", filename);
+ }
+
+ MD5_Init(&context);
+ while ((len = fread(buffer, 1, BUFFER_LEN, FID)) != 0) {
+ MD5_Update(&context, buffer, len);
+ }
+ MD5_Final(digest, &context);
+
+ fclose(FID);
+}
+
+// Output of 16 uchar_Ts as 32 character hexadecimals: ===========================
+void ToHex(const uchar_T digest[16], char *output, int LowerCase)
+{
+ char *outputEnd, *Fmt;
+ const uchar_T *s = digest;
+
+ Fmt = LowerCase ? "%02x" : "%02X";
+
+ for (outputEnd = output + 32; output < outputEnd; output += 2) {
+ sprintf(output, Fmt, *(s++));
+ }
+}
+
+// BASE64 encoded output: ======================================================
+void ToBase64(const uchar_T In[16], char *Out)
+{
+ // The base64 encoded string is shorter than the hex string.
+ // Needed length: ((len + 2) / 3 * 4) + 1, here fixed to 22+1 (trailing 0!).
+ static const uchar_T B64[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+
+ int i;
+ char *p;
+ const uchar_T *s;
+
+ p = Out;
+ s = In;
+ for (i = 0; i < 5; i++) {
+ *p++ = B64[(*s >> 2) & 0x3F];
+ *p++ = B64[((*s & 0x3) << 4) | ((s[1] & 0xF0) >> 4)];
+ *p++ = B64[((s[1] & 0xF) << 2) | ((s[2] & 0xC0) >> 6)];
+ *p++ = B64[s[2] & 0x3F];
+ s += 3;
+ }
+
+ *p++ = B64[(*s >> 2) & 0x3F];
+ *p++ = B64[((*s & 0x3) << 4)];
+ *p = '\0';
+}
+
+// Main function: ==============================================================
+void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[])
+{
+ // Mex interface:
+ // - Define default values of optional arguments.
+ // - Forward input data to different calculators according to the input type.
+ // - Convert digest to output format.
+
+ char *FileName, outString[33];
+ uchar_T digest[16], *digestP, OutType = 'h';
+ double *outP, *outEnd;
+ Method_t Method = BINARY_m;
+ mwSize nByte;
+
+ // Check number of inputs and outputs: ---------------------------------------
+ if (nrhs == 0 || nrhs > 3) {
+ ERROR("BadNInput", "1 to 3 inputs required.");
+ }
+ if (nlhs > 1) {
+ ERROR("BadNOutput", "Too many output arguments.");
+ }
+
+ // Check type of inputs:
+ if (mxIsChar(prhs[0])) {
+ Method = ASCII_m; // Default for CHAR arrays
+ }
+
+ // Evaluate 1st character of 2nd input:
+ if (nrhs >= 2 && mxGetNumberOfElements(prhs[1]) > 0) {
+ if (mxIsChar(prhs[1]) == 0) {
+ ERROR("BadTypeInput2", "2nd input [Method] must be a string.");
+ }
+
+ switch (*(uchar_T *) mxGetData(prhs[1])) {
+ case '8':
+ if (!mxIsChar(prhs[0])) {
+ WARN("NoASCII", "ASCII Mode ignored: Data is no CHAR.");
+ Method = BINARY_m;
+ }
+ break;
+ case 'b':
+ case 'B': Method = BINARY_m; break;
+ case 'a':
+ case 'A': Method = ARRAY_m; break;
+ case 'f':
+ case 'F': Method = FILE_m; break;
+ default: ERROR("BadInput2", "Mode not recognized.");
+ }
+ }
+
+ // Output type - default: hex:
+ if (nrhs == 3 && !mxIsEmpty(prhs[2])) {
+ if (mxIsChar(prhs[2]) == 0) {
+ ERROR("BadTypeInput3", "3rd input must be a string.");
+ }
+
+ OutType = *(uchar_T *) mxGetData(prhs[2]); // Just 1st character
+ }
+
+ // Calculate check sum: ------------------------------------------------------
+ switch (Method) {
+ case FILE_m: // Input is a file name:
+ if ((FileName = mxArrayToString(prhs[0])) == NULL) {
+ ERROR("StringFail", "Cannot get file name as string.");
+ }
+ ProcessFile(FileName, digest);
+ mxFree(FileName);
+ break;
+
+ case ARRAY_m: // Type, dimensions and contents of the array:
+ ProcessArray(prhs[0], digest);
+ break;
+
+ case ASCII_m: // Consider ASCII part of 16-bit mxChar only:
+ ProcessChar((mxChar *) mxGetData(prhs[0]),
+ mxGetNumberOfElements(prhs[0]), digest);
+ break;
+
+ case BINARY_m: // Contents of the variable:
+ if (!(mxIsNumeric(prhs[0]) || mxIsChar(prhs[0]) || mxIsLogical(prhs[0]))
+ || mxIsComplex(prhs[0]) || mxIsSparse(prhs[0])) {
+ ERROR("BadDataType",
+ "Binary mode requires: non-sparse, real, numeric or CHAR data.");
+ }
+ nByte = mxGetNumberOfElements(prhs[0]) * mxGetElementSize(prhs[0]);
+ ProcessBin((uchar_T *) mxGetData(prhs[0]), nByte, digest);
+ break;
+
+ default:
+ ERROR("BadSwitch", "Programming error: Unknown switch case?!");
+ }
+
+ // Create output: ------------------------------------------------------------
+ switch (OutType) {
+ case 'H':
+ case 'h': // Hexadecimal upper/lower case:
+ ToHex(digest, outString, OutType == 'h');
+ plhs[0] = mxCreateString(outString);
+ break;
+
+ case 'D':
+ case 'd': // DOUBLE with integer values:
+ plhs[0] = mxCreateDoubleMatrix(1, 16, mxREAL);
+ outP = mxGetPr(plhs[0]);
+ digestP = digest;
+ for (outEnd = outP + 16; outP < outEnd; outP++) {
+ *outP = (double) *digestP++;
+ }
+ break;
+
+ case 'B':
+ case 'b': // Base64:
+ ToBase64(digest, outString); // Locally implemented
+ plhs[0] = mxCreateString(outString);
+ break;
+
+ case 'U':
+ case 'u': // UINT8:
+ plhs[0] = mxCreateNumericMatrix(1, 16, mxUINT8_CLASS, mxREAL);
+ memcpy(mxGetData(plhs[0]), digest, 16 * sizeof(uchar_T));
+ break;
+
+ default:
+ ERROR("BadOutputType", "Unknown output type.");
+ }
+
+ return;
+}
diff --git a/src/caches/GetMD5/GetMD5.m b/src/caches/GetMD5/GetMD5.m
new file mode 100644
index 0000000..a7bb749
--- /dev/null
+++ b/src/caches/GetMD5/GetMD5.m
@@ -0,0 +1,114 @@
+function GetMD5(varargin)
+% GetMD5 - 128 bit MD5 checksum: file, string, array, byte stream
+% This function calculates a 128 bit checksum for arrays or files.
+% Digest = GetMD5(Data, Mode, Format)
+% INPUT:
+% Data: File name or array.
+% Mode: CHAR to declare the type of the 1st input. Not case-sensitive.
+% 'File': Data is a file name as CHAR.
+% '8Bit': If Data is a CHAR array, only the 8 bit ASCII part is
+% used. Then the digest is the same as for a ASCII text
+% file e.g. created by: FWRITE(FID, Data, 'uchar').
+% This is ignored if Data is not of type CHAR.
+% 'Binary': The MD5 sum is obtained for the contents of Data.
+% This works for numerical, CHAR and LOGICAL arrays.
+% 'Array': Include the class and dimensions of Data in the MD5
+% sum. This can be applied for (nested) structs, cells
+% and sparse arrays also.
+% Optional. Default: '8Bit' for CHAR, 'Binary' otherwise.
+% Format: CHAR, format of the output. Only the first character matters.
+% The upper/lower case matters for 'hex' only.
+% 'hex': [1 x 32] lowercase hexadecimal CHAR.
+% 'HEX': [1 x 32] uppercase hexadecimal CHAR.
+% 'double': [1 x 16] double vector with UINT8 values.
+% 'uint8': [1 x 16] uint8 vector.
+% 'base64': [1 x 22] CHAR, encoded to base 64 (A:Z,a:z,0:9,+,/).
+% The CHAR is not padded to keep it short.
+% Optional, default: 'hex'.
+%
+% OUTPUT:
+% Digest: A 128 bit number is replied in the specified format.
+%
+% NOTE:
+% * For sparse arrays, function handles, java and user-defined objects the
+% M-file GetMD5_helper is called.
+% * This replies the same MD5 hash as DataHash, but much faster.
+%
+% EXAMPLES:
+% Three methods to get the MD5 of a file:
+% 1. Direct file access (recommended):
+% MD5 = GetMD5(which('GetMD5.m'), 'File')
+% 2. Import the file to a CHAR array (no text mode for exact line breaks!):
+% FID = fopen(which('GetMD5.m'), 'r');
+% S = fread(FID, inf, 'uchar=>char');
+% fclose(FID);
+% MD5 = GetMD5(S, '8bit')
+% 3. Import file as a byte stream:
+% FID = fopen(which('GetMD5.m'), 'r');
+% S = fread(FID, inf, 'uint8=>uint8');
+% fclose(FID);
+% MD5 = GetMD5(S, 'bin'); % 'bin' is optional
+%
+% Test data:
+% GetMD5(char(0:511), '8bit', 'HEX')
+% % => F5C8E3C31C044BAE0E65569560B54332
+% GetMD5(char(0:511), 'bin')
+% % => 3484769D4F7EBB88BBE942BB924834CD
+% GetMD5(char(0:511), 'array')
+% % => b9a955ae730b25330d4f4ebb0a51e8f0
+% GetMD5('abc') % implicit '8bit' for CHAR
+% % => 900150983cd24fb0d6963f7d28e17f72
+%
+% COMPILATION:
+% The C-Mex-file is compiled automatically, when this function is called the
+% first time.
+% See GetMD5.c for details or a manual compilation.
+%
+% Tested: Matlab/64 7.8, 7.13, 8.6, 9.1, Win7/64
+% Author: Jan Simon, Heidelberg, (C) 2009-2019 matlab.2010(a)n(MINUS)simon.de
+% License: This program is derived from the RSA Data Security, Inc.
+% MD5 Message Digest Algorithm, RFC 1321, R. Rivest, April 1992
+% This implementation is published under the BSD license.
+%
+% See also CalcCRC32, DataHash.
+%
+% For more checksum methods see:
+% http://www.mathworks.com/matlabcentral/fileexchange/31272-datahash
+
+% $JRev: R5G V:038 Sum:yFie32x5VgiF Date:04-Mar-2019 16:20:44 $
+% $License: BSD (use/copy/change/redistribute on own risk, mention the author) $
+% $UnitTest: uTest_GetMD5 $
+% $File: Tools\GLFile\GetMD5.m $
+% History:
+% 015: 15-Dec-2009 16:53, BUGFIX: UINT32 has 32 bits on 64 bit systems now.
+% Thanks to Sebastiaan Breedveld!
+% 026: 30-Jan-2015 00:12, 64 bit arrays larger than 2.1GB accepted.
+% Successor of "CalcMD5".
+% 031: 04-Jun-2016 22:26, Structs and cells are handled.
+% 033: 03-May-2018 20:37, BUGFIX: Null pointer in data array.
+% 035: 27-Aug-2018 00:52, Consider string class.
+
+% Dummy code, which calls the auto-compilation only: ---------------------------
+% This M-function is not called, if the compiled MEX function is in the path.
+
+persistent FirstRun
+if isempty(FirstRun)
+ fprintf('### %s: M-verion is running. Compiling MEX file...\n', mfilename);
+
+ ok = InstallMex('GetMD5.c', 'uTest_GetMD5');
+ if ok
+ FirstRun = false;
+ end
+
+ try
+ dummy = GetMD5_helper([]); %#ok
+ catch ME
+ error(['JSimon:', mfilename, ':NoHelper'], ...
+ '### %s: GetMD5_Helper.m: %s', mfilename, ME.message);
+ end
+else
+ error(['JSimon:', mfilename, ':MissMEX'], ...
+ 'Cannot find Mex file: %s', [mfilename, '.', mexext]);
+end
+
+end
diff --git a/src/caches/GetMD5/GetMD5.mexa64 b/src/caches/GetMD5/GetMD5.mexa64
new file mode 100755
index 0000000..b80a80a
Binary files /dev/null and b/src/caches/GetMD5/GetMD5.mexa64 differ
diff --git a/src/caches/GetMD5/GetMD5_helper.m b/src/caches/GetMD5/GetMD5_helper.m
new file mode 100644
index 0000000..4442e41
--- /dev/null
+++ b/src/caches/GetMD5/GetMD5_helper.m
@@ -0,0 +1,112 @@
+function S = GetMD5_helper(V)
+% GetMD5_helper: 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).
+%
+% NOTE:
+% For objects the function 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
+% Author: Jan Simon, Heidelberg, (C) 2016-2019 matlab.2010(a)n(MINUS)simon.de
+
+% $JRev: R5n V:013 Sum:I9OCbLsTN/AG Date:17-Feb-2019 19:01:00 $
+% $License: BSD (use/copy/change/redistribute on own risk, mention the author) $
+% $File: Tools\GLFile\GetMD5_helper.m $
+% History:
+% 001: 28-Jun-2015 19:19, Helper for GetMD5.
+% 012: 27-Aug-2018 01:38, String class considered.
+% 013: 09-Feb-2019 18:08, ismethod(class(V),) to support R2018b.
+
+% Initialize: ==================================================================
+% Do the work: =================================================================
+% isa(V, 'class') is surprisingly expensive. Get the class as string once:
+classV = class(V);
+
+% The dimensions, number of dimensions and the name of the class is considered
+% already in the MEX function!
+
+
+
+if strcmp(classV, 'function_handle') %#ok<*STISA>
+ % Please adjust the subfunction ConvertFuncHandles to your needs.
+
+ % The Matlab version influences the conversion by FUNCTIONS:
+ % 1. The format of the struct replied FUNCTIONS is not fixed,
+ % 2. The full path of toolbox function e.g. for @mean differ.
+ S = functions(V);
+
+ % Include modification file time and file size. Suggested by Aslak Grinsted:
+ if ~isempty(S.file)
+ d = dir(S.file);
+ if ~isempty(d)
+ S.filebytes = d.bytes;
+ S.filedate = d.datenum;
+ end
+ end
+
+ % ALTERNATIVE: Use name and path. The part of the toolbox
+ % functions is replaced such that the hash for @mean does not depend on the
+ % Matlab version.
+ % Drawbacks: Anonymous functions, nested functions...
+ % funcStruct = functions(FuncH);
+ % funcfile = strrep(funcStruct.file, matlabroot, '');
+ % S = uint8([funcStruct.function, ' ', funcfile]);
+
+ % Finally I'm afraid there is no unique method to get a hash for a function
+ % handle. Please adjust this conversion to your needs.
+
+elseif (isobject(V) || isjava(V)) && ismethod(classV, 'hashCode') % ===========
+ % Java or user-defined objects might have a hash function:
+ S = char(V.hashCode);
+
+elseif issparse(V) % ==========================================================
+ % Create struct with indices and non-zero values:
+ [S.Index1, S.Index2, S.Value] = find(V);
+
+elseif strcmp(classV, 'string') % Since R2016b: ===============================
+ % Convert all strings to UINT8 byte stream including the dimensions:
+ classUint8 = uint8([117, 105, 110, 116, 49, 54]);
+ C = cell(1, numel(V));
+ for iC = 1:numel(V)
+ % Engine = CoreHash(uint16(Data{iS}), Engine);
+ aString = uint16(V{iC});
+ C{iC} = [classUint8, ...
+ typecast(uint64([ndims(aString), size(aString)]), 'uint8'), ...
+ typecast(uint16(aString), 'uint8')];
+ end
+ S = [uint8([115, 116, 114, 105, 110, 103]), ... % 'string'
+ typecast(uint64([ndims(V), size(V)]), 'uint8'), ...
+ cat(2, C{:})];
+
+else % Most likely this is a user-defined object: =============================
+ try % Perhaps a direct conversion is implemented:
+ S = uint8(V);
+
+ % Matt Raum had this excellent idea - unfortunately this function is
+ % undocumented and might not be supported in the future:
+ % S = getByteStreamFromArray(DataObj);
+
+ catch ME % Or perhaps this is better:
+ fprintf(2, ['### %s: Convert object to struct as fallback.', char(10), ...
+ ' %s\n'], ME.message);
+ WarnS = warning('off', 'MATLAB:structOnObject');
+ S = struct(V);
+ warning(WarnS);
+ end
+end
+
+% return;
diff --git a/src/caches/GetMD5/InstallMex.m b/src/caches/GetMD5/InstallMex.m
new file mode 100644
index 0000000..7c13880
--- /dev/null
+++ b/src/caches/GetMD5/InstallMex.m
@@ -0,0 +1,330 @@
+function Ok = InstallMex(SourceFile, varargin)
+% INSTALLMEX - 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.
+%
+% OUTPUT:
+% Ok: Logical flag, TRUE if compilation was successful. Optional.
+%
+% COMPATIBILITY:
+% - 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
+% -R2018a for C Data API (this is set, when the string "[R2018a API]"
+% appears anywhere inside the source file.
+%
+% EXAMPLES:
+% Compile func1.c with LAPACK libraries:
+% InstallMex('func1', {'libmwlapack.lib', 'libmwblas.lib'})
+% Compile func2.cpp, enable debugging and call a test function:
+% 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:
+% 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.
+%
+% Tested: Matlab/64 7.8, 7.13, 8.6, 9.1, Win7/64
+% Author: Jan Simon, Heidelberg, (C) 2012-2019 matlab.2010(a)n(MINUS)simon.de
+
+% $JRev: R5L V:037 Sum:jndyPgDqNKoN Date:17-Feb-2019 19:01:00 $
+% $License: BSD (use/copy/change/redistribute on own risk, mention the author) $
+% $File: Tools\GLSource\InstallMex.m $
+% History:
+% 001: 27-Jul-2012 09:06, First version.
+% 005: 29-Jul-2012 17:11, Run the unit-test instead of showing a link only.
+% 006: 11-Aug-2012 23:59, Inputs are accepted in free order.
+% 020: 30-Dec-2013 01:48, Show a question dialog if mex is existing already.
+% 027: 08-Mar-2015 22:15, Define _LITTLE_ENDIAN / _BIG_ENDIAN.
+% 028: 22-Aug-2015 19:16, Define HAS_HG2 for Matlab >= 2014b.
+% 029: 24-Dec-2015 17:46, CATCH MException: No Matlab6.5 support anymore.
+% 032: 24-Apr-2016 17:45, Bugs fixed: "cmd" instead of "cmdStr".
+% 037: 09-Feb-2019 17:49, -R2017b and -R2018a.
+
+% No warnings in Matlab versions without String support:
+%#ok<*ISCLSTR>
+%#ok<*STREMP>
+
+% Initialize: ==================================================================
+% Global Interface: ------------------------------------------------------------
+% URL to file or folder containing pre-compiled files, or the empty string if
+% pre-compiled files are not offered:
+% ### START: ADJUST TO USER NEEDS
+Precompiled = 'http://www.n-simon.de/mex';
+% ### END
+
+% Initial values: --------------------------------------------------------------
+Ok = false;
+bakCD = cd;
+matlabV = [100, 1] * sscanf(version, '%d.%d', 2); % Matlab version
+[Arch, MaxSize, Endian] = computer; %#ok
+
+hasHG2 = (matlabV >= 804); % R2014b
+hasLargeDims = (matlabV >= 705) & any(strfind(Arch, '64')); % R2007b & 64bit
+isLittleEndian = strncmpi(Endian, 'L', 1);
+hasR2018aAPI = (matlabV >= 904); % R2018a
+
+% Program Interface: -----------------------------------------------------------
+% Parse inputs:
+Param = {};
+UnitTestFcn = '';
+doDebug = false;
+debugFlag = {};
+force32 = false;
+replace = false;
+
+% First input is the name of the source file:
+if ~ischar(SourceFile)
+ error_L('BadTypeInput1', '1st input must be a string.');
+end
+
+% Additional inputs are identified by their type:
+% String: unit-test function or the flag to enable debugging.
+% Cell string: additional parameters for the MEX command
+for iArg = 1:numel(varargin)
+ Arg = varargin{iArg};
+ if ischar(Arg)
+ if strcmpi(Arg, '-debug')
+ doDebug = true;
+ debugFlag = {'-v'};
+ elseif strcmpi(Arg, '-force32')
+ force32 = true;
+ elseif strcmpi(Arg, '-replace')
+ replace = true;
+ elseif exist(Arg, 'file') == 2
+ UnitTestFcn = Arg;
+ else
+ error_L('MissFile', 'Unknown string or missing file: %s', Arg);
+ end
+ elseif iscellstr(Arg) % As row cell:
+ Param = Arg(:).';
+ else
+ error_L('BadInputType', 'Bad type of input.');
+ end
+end
+
+% User Interface: --------------------------------------------------------------
+hasHRef = usejava('jvm'); % Hyper-links in the command window?
+
+% Do the work: =================================================================
+% Search the source file, solve partial or relative path, get the real
+% upper/lower case:
+[dummy, dummy, Ext] = fileparts(SourceFile); %#ok
+if isempty(Ext)
+ SourceFile = [SourceFile, '.c'];
+end
+
+fullSource = which(SourceFile);
+if isempty(fullSource)
+ error_L('NoSource', 'Cannot find the source file: %s', SourceFile);
+end
+[SourcePath, SourceName, Ext] = fileparts(fullSource);
+Source = [SourceName, Ext];
+
+% Consider output name and outputdir:
+index = find(strcmpi(Param, '-output'), 1, 'last');
+if isempty(index)
+ mexName = SourceName;
+else
+ [dummy, mexName] = fileparts(Param{index + 1}); %#ok
+end
+mexFile = [mexName, '.', mexext];
+
+index = find(strcmpi(Param, '-outdir'), 1, 'last');
+if isempty(index)
+ mexPath = SourcePath;
+else
+ mexPath = Param{index + 1};
+end
+
+fprintf('== Compile: %s\n', fullfile(SourcePath, Source));
+
+% Check if the compiled file is existing already:
+whichMex = which(mexFile);
+if ~isempty(whichMex)
+ fprintf(' Existing already: %s\n', whichMex);
+
+ if ~replace
+ % Ask the user if a new compilation is wanted:
+ QuestReply = questdlg({ ...
+ ['\bf', mfilename, ': ', TeXFirm(SourceName), '\rm'], ...
+ '', 'The function is existing already:', ...
+ [' ', TeXFirm(whichMex)], '', ...
+ 'Do you want to compile it here:', ...
+ [' ', TeXFirm(fullfile(mexPath, mexFile))], ''}, ...
+ mfilename, 'Compile', 'Cancel', ...
+ struct('Default', 'Cancel', 'Interpreter', 'tex'));
+
+ % User does not want to recompile:
+ if strcmp(QuestReply, 'Cancel')
+ if nargout
+ Ok = false;
+ end
+ return;
+ end
+ end
+ fprintf(' Recompile in: %s\n\n', mexPath);
+end
+
+if ~ispc && strcmpi(Ext, '.c')
+ % C99 for the GCC and XCode compilers.
+ % Note: 'CFLAGS="\$CFLAGS -std=c99"' must be separated to 2 strings!!!
+ Opts = {'-O', 'CFLAGS="\$CFLAGS', '-std=c99"'};
+else
+ Opts = {'-O'};
+end
+
+% Define endianess directive:
+if isLittleEndian
+ Opts = cat(2, Opts, {'-D_LITTLE_ENDIAN'});
+else % Does Matlab run on any big endian machine currently?!
+ Opts = cat(2, Opts, {'-D_BIG_ENDIAN'});
+end
+
+% Define the new HG2 graphic handles:
+if hasHG2
+ Opts = cat(2, Opts, {'-DHAS_HG2'});
+end
+
+% Decide for the old R2017b or new R2018a API:
+matlabVDef = {sprintf('-DMATLABVER=%d', matlabV)};
+if hasR2018aAPI
+ % If the C file contains the string " mex -R2018a " anywhere, the modern API
+ % is enabled:
+ tmp = fileread(fullSource);
+ if isempty(strfind(tmp, '[R2018a API]'))
+ compatibleAPI = '-R2017b';
+ else
+ compatibleAPI = '-R2018a';
+ end
+elseif hasLargeDims && ~force32
+ % Large array dimensions under 64 bit, possible since R2007b:
+ compatibleAPI = '-largeArrayDims';
+else
+ % 32 bit addressing, possible since R2007b:
+ % Equivalent: -DMX_COMPAT_32
+ compatibleAPI = '-compatibleArrayDims';
+end
+Opts = cat(2, Opts, {compatibleAPI});
+
+% Compile: ---------------------------------------------------------------------
+% Display the compilation command:
+Opts = cat(1, Opts(:), debugFlag, matlabVDef, Param(:), {Source});
+cmdStr = ['mex', sprintf(' %s', Opts{:})];
+fprintf('%s\n\n', cmdStr);
+
+cd(SourcePath);
+try % Start the compilation:
+ mex(Opts{:});
+ compiled = true;
+ fprintf('Compiled:\n %s\n', which(mexFile));
+
+catch ME % Compilation failed - MException fails in Matlab 6.5!
+ compiled = false;
+ fprintf(2, '\n*** Compilation failed:\n%s\n\n', ME.message);
+ fprintf('Matlab version: %s\n', version);
+ if ~doDebug % Compile again in debug mode if not done already:
+ fprintf('Compile with debug flag to get details:\n');
+ try
+ mex(Opts{:}, '-v');
+ catch % Empty - it is known already that it fails
+ end
+ end
+
+ % Show commands for manual compilation and download pre-compiled files:
+ fprintf('\n== The compilation failed! Possible solutions:\n');
+ fprintf(' * Is a compiler installed and set up properly by: mex -setup?\n');
+ fprintf(' * Try to compile manually:\n cd(''%s'')\n', SourcePath);
+ fprintf(' %s -v\n', cmdStr);
+ fprintf(' * Or download the pre-compiled file %s:\n', mexFile);
+ if ~isempty(Precompiled)
+ if hasHRef
+ fprintf( ...
+ ' %s\n', ...
+ Precompiled, mexFile, Precompiled);
+ else % No hyper-references in the command window without Java:
+ fprintf(' %s\n', Precompiled);
+ end
+ end
+ fprintf(' * Please send this report to the author.\n');
+end
+
+% Restore original directory and check precedence: -----------------------------
+cd(bakCD);
+if compiled
+ allWhich = which(mexName, '-all');
+ if ~strcmpi(allWhich{1}, fullfile(mexPath, mexFile))
+ Spec = sprintf(' %%-%ds ', max(cellfun('length', allWhich)));
+ fprintf(2, '\n*** Failed: Compiled function is shadowed:\n');
+ fprintf(2, [Spec, '*USED*\n'], allWhich{1});
+ fprintf(2, [Spec, '*SHADOWED*\n'], allWhich{2:end});
+
+ compiled = false;
+ end
+
+ allWhichMex = which(mexFile, '-all');
+ if length(allWhichMex) > 1
+ fprintf(2, '\n::: Multiple instances of compiled file:\n');
+ fprintf(2, ' %s\n', allWhich{:});
+ end
+end
+
+% Run the unit-test: -----------------------------------------------------------
+if ~isempty(UnitTestFcn) && compiled
+ fprintf('\n\n== Post processing:\n');
+ [dum, UnitTestName] = fileparts(UnitTestFcn); %#ok % Remove extension
+ if ~isempty(which(UnitTestName))
+ fprintf(' Call: %s\n\n', UnitTestName);
+ feval(UnitTestName);
+ else
+ fprintf(2, '??? Cannot find function: %s\n', UnitTestFcn);
+ end
+end
+
+% Return success of compilation: -----------------------------------------------
+if nargout >= 1
+ Ok = compiled;
+end
+if compiled
+ fprintf('\n== %s: ready.\n', mfilename);
+else
+ fprintf('\n== %s: failed.\n', mfilename);
+end
+
+% end
+
+% ******************************************************************************
+function error_L(ID, Msg, varargin)
+% Automatic error ID and mfilename in the message:
+error(['JSimon:', mfilename, ':', ID], ...
+ ['*** %s: ', Msg], mfilename, varargin{:});
+
+% end
+
+% ******************************************************************************
+function S = TeXFirm(S)
+% Escape special characters for the TeX interpreter:
+S = strrep(strrep(S, '\', '\\'), '_', '\_');
+
+% end
diff --git a/src/caches/GetMD5/uTest_GetMD5.m b/src/caches/GetMD5/uTest_GetMD5.m
new file mode 100644
index 0000000..8c3f411
--- /dev/null
+++ b/src/caches/GetMD5/uTest_GetMD5.m
@@ -0,0 +1,334 @@
+function uTest_GetMD5(doSpeed)
+% Automatic test: GetMD5 (Mex)
+% 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.
+%
+% 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
+
+% $JRev: R5x V:030 Sum:w+RmN0vsgBgY Date:27-Aug-2018 01:18:47 $
+% $License: BSD (use/copy/change/redistribute on own risk, mention the author) $
+% $File: Tools\UnitTests_\uTest_GetMD5.m $
+% History:
+% 018: 01-Mar-2015 13:48, Compare results with Java hash.
+% 022: 28-Jun-2015 19:10, Check array input.
+% 028: 03-May-2018 20:37, Test NULL pointer in cell array.
+% 030: 27-Aug-2018 00:55, Consider string class.
+
+% Initialize: ==================================================================
+% Global Interface: ------------------------------------------------------------
+ErrID = ['JSimon:', mfilename];
+
+MatlabV = [100, 1] * sscanf(version, '%d.', 2);
+
+% Initial values: --------------------------------------------------------------
+if nargin == 0
+ doSpeed = true;
+end
+
+% Program Interface: -----------------------------------------------------------
+% User Interface: --------------------------------------------------------------
+% Do the work: =================================================================
+fprintf('==== Test GetMD5: %s\n', datestr(now, 0));
+
+% Check if MEX function is available - compile it otherwise:
+whichMEX = which(['GetMD5.', mexext]);
+if isempty(whichMEX)
+ GetMD5(); % Call without inputs
+ clear('GetMD5');
+
+ whichMEX = which(['GetMD5.', mexext]);
+ if isempty(whichMEX)
+ error([ErrID, ':Install'], 'Cannot find compiled function.');
+ end
+end
+
+whichFunc = which('GetMD5');
+fprintf('Version: %s\n\n', whichFunc);
+
+% Known answer tests - see RFC1321: --------------------------------------------
+disp('== Known answer tests:');
+TestData = { ...
+ '', 'd41d8cd98f00b204e9800998ecf8427e'; ...
+ 'a', '0cc175b9c0f1b6a831c399e269772661'; ...
+ 'abc', '900150983cd24fb0d6963f7d28e17f72'; ...
+ 'message digest', 'f96b697d7cb7938d525a2f31aaf161d0'; ...
+ ...
+ 'abcdefghijklmnopqrstuvwxyz', ...
+ 'c3fcd3d76192e4007dfb496cca67e13b'; ...
+ ...
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', ...
+ 'd174ab98d277d9f5a5611c2c9f419d9f'; ...
+ ...
+ ['123456789012345678901234567890123456789012345678901234567890123456', ...
+ '78901234567890'], ...
+ '57edf4a22be3c955ac49da2e2107b67a'; ...
+ ...
+ char(0:255), 'e2c865db4162bed963bfaa9ef6ac18f0'}; % Not in RFC1321
+
+TestFile = tempname;
+
+% Loop over test data:
+for iTest = 1:size(TestData, 1)
+ % Check char vector input considering ASCII range only:
+ Str = GetMD5(TestData{iTest, 1}, '8bit');
+ if strcmpi(Str, TestData{iTest, 2}) == 0
+ error([ErrID, ':KAT'], ['Failed for char vector:', ...
+ char(10), '[', TestData{iTest, 1}, ']']);
+ end
+
+ % Check file input:
+ FID = fopen(TestFile, 'w+');
+ if FID < 0
+ error([ErrID, ':KAT'], 'Cannot open test file [%s]', TestFile);
+ end
+ fwrite(FID, TestData{iTest, 1}, 'uchar');
+ fclose(FID);
+
+ Str2 = GetMD5(TestFile, 'file');
+ if strcmpi(Str2, TestData{iTest, 2}) == 0
+ error([ErrID, ':KAT'], 'Failed for file: [%s]', TestData{iTest, 1});
+ end
+
+ % Check char vector converted to UINT8:
+ Str3 = GetMD5(uint8(TestData{iTest, 1}), 'Binary');
+ if strcmpi(Str3, TestData{iTest, 2}) == 0
+ error([ErrID, ':KAT'], ['Failed for UINT8:', ...
+ char(10), '[', TestData{iTest, 1}, ']']);
+ end
+end
+disp(' ok: Test files');
+delete(TestFile);
+
+% Compare results with java implementation: ------------------------------------
+if usejava('jvm')
+ disp('== Compare mex and Java:');
+ Engine = java.security.MessageDigest.getInstance('MD5');
+ for k = 0:1026 % More or less arbitrary upper limit
+ S = uint8(rand(k, 1) * 256); % Random test data of growing length
+
+ if k > 0
+ Engine.update(S);
+ end
+ jHash = typecast(Engine.digest, 'uint8');
+
+ mHash = GetMD5(S, 'bin', 'uint8');
+
+ if ~isequal(jHash(:), mHash(:))
+ error([ErrID, ':JavaComp'], 'Result differs from Java!');
+ end
+ end
+ fprintf(' ok: MEX compared to Java\n');
+
+else % No Java VM:
+ fprintf('::: No comparison with Java: JVM not available.\n\n');
+end
+
+% Check different output types: ------------------------------------------------
+N = 1000;
+disp('== Check output types:');
+for i = 1:N
+ data = uint8(fix(rand(1, 1 + fix(rand * 100)) * 256));
+ lowHexOut = GetMD5(data, 'bin', 'hex');
+ upHexOut = GetMD5(data, 'bin', 'HEX');
+ decOut = GetMD5(data, 'bin', 'Double');
+ b64Out = GetMD5(data, 'bin', 'Base64');
+ uint8Out = GetMD5(data, 'bin', 'Uint8');
+
+ if not(strcmpi(lowHexOut, upHexOut) && ...
+ isequal(sscanf(lowHexOut, '%2x'), decOut(:)) && ...
+ isequal(Base64decode(b64Out), decOut)) && ...
+ isequal(decOut, double(uint8Out))
+ fprintf('\n');
+ error([ErrID, ':Output'], 'Different results for output types.');
+ end
+
+ % Check binary, e.g. if the data length is a multiple of 2:
+ if rem(length(data), 2) == 0
+ doubleData = double(data);
+ uniData = char(doubleData(1:2:end) + 256 * doubleData(2:2:end));
+ uniOut = GetMD5(uniData, 'binary', 'dec');
+ if not(isequal(uniOut, decOut))
+ error([ErrID, ':Output'], 'Different results for binary mode.');
+ end
+ end
+end
+fprintf([' ok: %d random tests with hex, HEX, double, uint8, base64 ', ...
+ 'output: \n'], N);
+
+% Check arrays as inputs: ------------------------------------------------------
+disp('== Test array input:');
+
+% Hash must depend on the type of the array:
+S1 = GetMD5([], 'Array');
+if ~isequal(S1, '5b302b7b2099a97ba2a276640a192485')
+ error([ErrID, ':Array'], 'Bad result for array: []');
+end
+
+S1 = GetMD5(uint8([]), 'Array');
+if ~isequal(S1, 'cb8a2273d1168a72b70833bb0d79be13')
+ error([ErrID, ':Array'], 'Bad result for array: uint8([])');
+end
+
+S1 = GetMD5(int8([]), 'Array');
+if ~isequal(S1, '0160dd4473fe1a952572be239e077ed3')
+ error([ErrID, ':Array'], 'Bad result for array: int8([])');
+end
+
+Data = struct('Field1', 'string', 'Field2', {{'Cell string', '2nd string'}});
+Data.Field3 = Data;
+S1 = GetMD5(Data, 'Array');
+if ~isequal(S1, '4fe320b06e3aaaf4ba712980d649e274')
+ error([ErrID, ':Array'], 'Bad result for array: .');
+end
+
+Data = sparse([1,0,2; 0,3,0; 4, 0,0]);
+S1 = GetMD5(Data, 'Array');
+if ~isequal(S1, 'f157bdc9173dff169c782dd639984c82')
+ error([ErrID, ':Array'], 'Bad result for array: .');
+end
+fprintf(' ok: Array\n');
+
+% Uninitialized cells contain NULL pointers:
+Data = cell(1, 2);
+S1 = GetMD5(Data, 'Array');
+if ~isequal(S1, '161842037bc65b9f3bceffdeb4a8d3bd')
+ error([ErrID, ':Array'], 'Bad result for {NULL, NULL}.');
+end
+fprintf(' ok: Null pointer\n');
+
+% Check string type:
+if MatlabV >= 901 % R2016b
+ Data = string('hello');
+ S1 = GetMD5(Data, 'Array');
+ if ~isequal(S1, '2614526bcbd4af5a8e7bf79d1d0d92ab')
+ error([ErrID, ':String'], 'Bad result for string.');
+ end
+
+ Data = string({'hello', 'world'});
+ S1 = GetMD5(Data, 'Array');
+ if ~isequal(S1, 'a1bdbbe9a15c249764847ead9bf47326')
+ error([ErrID, ':String'], 'Bad result for string.');
+ end
+ fprintf(' ok: String class\n');
+end
+
+% Speed test: ------------------------------------------------------------------
+if doSpeed
+ disp('== Test speed:');
+ disp(' * Access data in memory');
+ disp(' * Slower for short data due to overhead of calling a function!');
+ fprintf('\n');
+ Delay = 2;
+
+ fprintf(' Data size: Java: Mex: factor:\n');
+ for Len = [10, 100, 1000, 10000, 1e5, 1e6, 1e7, 1e8]
+ [Number, Unit] = UnitPrint(Len, false);
+ fprintf('%12s', [Number, ' ', Unit]);
+ data = uint8(fix(rand(1, Len) * 256));
+
+ % Check if Java heap size is large enough:
+ try
+ javaWorked = true;
+ x = java.security.MessageDigest.getInstance('MD5');
+ x.update(data);
+ catch ME
+ % This happens for 100MB data, if the heap space is too small. I've
+ % observed this on 32 bit versions of Matlab in the default setup
+ % only.
+ javaWorked = false;
+ msg = lower(ME.message);
+ if any(strfind(msg, 'outofmemoryerror')) || ...
+ any(strfind(msg, 'heap space'))
+ fprintf('## Insufficient Java heap space for processing.\n');
+ fprintf(' This is no problem of GetMD5.c, but of Java:\n');
+ fprintf(2, '%s\n', msg);
+ end
+ end
+
+ % Measure java time:
+ if javaWorked
+ iniTime = cputime;
+ finTime = iniTime + Delay;
+ javaLoop = 0;
+ while cputime < finTime
+ x = java.security.MessageDigest.getInstance('MD5');
+ x.update(data);
+ javaHash = double(typecast(x.digest, 'uint8'));
+ javaLoop = javaLoop + 1;
+ clear('x');
+ end
+ javaLoopPerSec = javaLoop / (cputime - iniTime);
+ [Number, Unit] = UnitPrint(javaLoopPerSec * Len, true);
+ fprintf('%9s %s/s', Number, Unit);
+ else
+ javaLoopPerSec = 0;
+ end
+
+ % Measure Mex time:
+ iniTime = cputime;
+ finTime = iniTime + Delay;
+ mexLoop = 0;
+ while cputime < finTime
+ mexHash = GetMD5(data, 'binary', 'dec');
+ mexLoop = mexLoop + 1;
+ end
+ mexLoopPerSec = mexLoop / (cputime - iniTime);
+ [Number, Unit] = UnitPrint(mexLoopPerSec * Len, true);
+ fprintf('%8s %s/s %4.1f\n', ...
+ Number, Unit, mexLoopPerSec / javaLoopPerSec);
+
+ % Compare the results:
+ if javaWorked && ~isequal(javaHash(:), mexHash(:))
+ error([ErrID, ':Compare'], 'Different results from java and Mex.');
+ end
+ end
+end
+
+fprintf('\n== GetMD5 passed the tests.\n');
+
+% return;
+
+% ******************************************************************************
+function Out = Base64decode(In)
+% Decode from base 64
+
+% Initialize: ==================================================================
+Pool = [65:90, 97:122, 48:57, 43, 47]; % [0:9, a:z, A:Z, +, /]
+v8 = [128, 64, 32, 16, 8, 4, 2, 1];
+v6 = [32; 16; 8; 4; 2; 1];
+
+% Do the work: =================================================================
+In = reshape(In, 1, []);
+Table = zeros(1, 256);
+Table(Pool) = 1:64;
+Value = Table(In) - 1;
+
+X = rem(floor(Value(ones(6, 1), :) ./ v6(:, ones(length(In), 1))), 2);
+Out = v8 * reshape(X(1:fix(numel(X) / 8) * 8), 8, []);
+
+% return;
+
+% ******************************************************************************
+function [Number, Unit] = UnitPrint(N, useMB)
+
+if N >= 1e6 || useMB
+ Number = sprintf('%.1f', N / 1e6);
+ Unit = 'MB';
+elseif N >= 1e3
+ Number = sprintf('%.1f', N / 1000);
+ Unit = 'kB';
+else
+ Number = sprintf('%g', N);
+ Unit = ' B';
+end
+
+% return;
diff --git a/src/caches/LRU.m b/src/caches/LRU.m
new file mode 100644
index 0000000..e5c3ab6
--- /dev/null
+++ b/src/caches/LRU.m
@@ -0,0 +1,118 @@
+classdef LRU < handle
+ % LRU 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 (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.
+ end
+
+ methods
+ function cache = LRU(itemlimit, memlimit)
+ % Construct a new LRU cache.
+ %
+ % Arguments
+ % ---------
+ % itemlimit : int
+ % maximum size of cache in number of items.
+ %
+ % memlimit : numeric
+ % maximum size of cache in number of bytes.
+ %
+ % Returns
+ % -------
+ % cache : :class:`LRU`
+ % empty LRU cache.
+
+ % Initialize data
+ cache.sentinel = DLL([]);
+ cache.map = containers.Map('KeyType', 'char', 'ValueType', 'any');
+
+ % Set config
+ if nargin > 0 && ~isempty(itemlimit)
+ cache.itemlimit = itemlimit;
+ end
+ if nargin > 1 && ~isempty(memlimit)
+ cache.memlimit = memlimit;
+ end
+ end
+
+ function val = get(cache, key)
+ % Retrieve a value from the cache.
+ %
+ % Arguments
+ % ---------
+ % cache : :class:`LRU`
+ % data cache.
+ %
+ % key : :class:`uint8`
+ % data key.
+ %
+ % Returns
+ % -------
+ % val : any
+ % value that is stored with a key, or empty if key not in cache.
+
+ try
+ dll = cache.map(key);
+ val = dll.val{2};
+
+ % Re-insert dll to the front of the list
+ pop(dll);
+ append(cache.sentinel, dll);
+ catch
+ val = [];
+ end
+ end
+
+ function cache = set(cache, key, val)
+ % Add a value to the cache.
+ %
+ % Arguments
+ % ---------
+ % cache : :class:`LRU`
+ % data cache.
+ %
+ % key : :class:`uint8`
+ % data key.
+ %
+ % val : any
+ % data value.
+ %
+ % Returns
+ % -------
+ % cache : :class:`LRU`
+ % updated cache.
+
+ % remove previously stored data
+ if isKey(cache.map, key)
+ dll_old = cache.map(key);
+ pop(dll_old);
+ cache.mem = cache.mem - memsize(dll_old.val{2});
+ cache.map.remove(key);
+ end
+
+ % add new data
+ dll_new = DLL({key, val});
+ append(cache.sentinel, dll_new);
+ cache.map(key) = dll_new;
+
+ % update memory
+ cache.mem = cache.mem + memsize(val);
+ 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});
+ cache.map.remove(key);
+ end
+ end
+ end
+end
+
+function b = memsize(x)
+b = numel(getByteStreamFromArray(x));
+end
\ No newline at end of file
diff --git a/src/tensors/FusionTree.m b/src/tensors/FusionTree.m
new file mode 100644
index 0000000..b366d20
--- /dev/null
+++ b/src/tensors/FusionTree.m
@@ -0,0 +1,901 @@
+classdef FusionTree < matlab.mixin.CustomDisplay
+% Splitting and fusion tree pair in canonical form.
+%
+% Properties
+% ----------
+% charges : AbstractCharge
+% labels for the edges of the fusion trees
+%
+% vertices : int
+% labels for the vertices of the fusion trees
+%
+% isdual : logical
+% indicator of duality transform on the external edges.
+%
+% rank : int
+% amount of splitting tree and fusion tree legs.
+
+ properties
+ charges (:, :)
+ vertices (:, :) uint8
+ isdual (1, :) logical
+ rank (1, 2) int16
+ end
+
+ properties (Dependent, Hidden)
+ coupled
+ inner
+ uncoupled
+ end
+
+ %% Constructors
+ methods
+ function f = FusionTree(charges, vertices, isdual, rank)
+ % Usage
+ % -----
+ % ```f = FusionTree(charges, vertices, isdual, rank)```
+ %
+ % Arguments
+ % ---------
+ % charges : AbstractCharge
+ % array of charges, where each row represents an allowed fusion channel.
+ %
+ % vertices : 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
+ % mask that shows the presence of duality transformations.
+ %
+ % rank : int
+ % number of splitting and fusing legs.
+ if nargin == 0
+ return;
+ end
+
+ %% Trivial tree with no external legs
+ if sum(rank) == 0
+ if isempty(charges) && isempty(vertices) && isempty(isdual)
+ f.rank = rank;
+ return
+ end
+ error('tensors:tree:argError', 'Incompatible input sizes.');
+ end
+
+
+ %% General tree
+ N = length(isdual);
+ if N ~= sum(rank) || ...
+ size(charges, 2) ~= sum(treeindsequence(rank)) + 1 || ...
+ (N > 2 && hasmultiplicity(fusionstyle(charges)) && ...
+ size(vertices, 2) ~= max(N - 2, 0))
+ error('tensors:tree:argError', 'Incompatible input sizes.');
+ end
+
+ f.charges = charges;
+ f.vertices = vertices;
+ f.isdual = isdual;
+ f.rank = rank;
+ end
+ end
+
+ methods (Static)
+ function f = new(rank, charges, isdual)
+ % Generate all allowed fusion trees for a set of external charges.
+ %
+ % Arguments
+ % ---------
+ % rank : int
+ % number of legs for the splitting and fusion tree.
+ %
+ % Repeating Arguments
+ % -------------------
+ % charges : AbstractCharge
+ % set of charges for a given leg
+ %
+ % isdual : logical
+ % presence of a duality transform
+ arguments
+ rank (1, 2) {mustBeNonnegative, mustBeInteger}
+ end
+
+ arguments (Repeating)
+ charges (1, :) {mustBeA(charges, 'AbstractCharge')}
+ isdual (1, 1) logical
+ end
+
+ assert(sum(rank) == length(isdual), 'tensors:trees:ArgError', ...
+ 'Rank incompatible with the amount of legs specified.');
+
+ if sum(rank) == 0
+ f = FusionTree([], [], [], rank);
+ return
+ end
+
+
+ %% Splitting tree
+ if rank(1) == 0
+ charges_l = one(charges{1});
+ elseif rank(1) == 1
+ charges_l = [charges{1}(:) charges{1}(:)];
+ else
+ switch fusionstyle(charges{1})
+ case FusionStyle.Unique
+ unc_l = combvec(charges{1:rank(1)});
+ inn_l = cumprod(unc_l);
+ charges_l = reshape([unc_l(:).'; inn_l(:).'], [2 1] .* size(unc_l));
+ charges_l = charges_l(2:end, :).';
+ case FusionStyle.Simple
+ charges_l = [];
+
+ for unc_l = combvec(charges{1:rank(1)})
+ inn_l = cumprod(unc_l);
+ temp = reshape( ...
+ [repmat(unc_l(:).', 1, size(inn_l, 2)); inn_l(:).'], ...
+ [2 1] .* size(inn_l));
+ charges_l = [charges_l; temp(2:end, :).']; %#ok
+ end
+ otherwise
+ error('TBA');
+ end
+ end
+
+
+ %% Fusion tree
+ if rank(2) == 0
+ charges_r = one(charges{1});
+ elseif rank(2) == 1
+ charges_r = [charges{end}(:) charges{end}(:)];
+ else
+ switch fusionstyle(charges{end})
+ case FusionStyle.Unique
+ unc_r = combvec(charges{end:-1:rank(1) + 1});
+ inn_r = cumprod(unc_r);
+ charges_r = reshape([unc_r(:).'; inn_r(:).'], [2 1] .* size(unc_r));
+ charges_r = charges_r(2:end, :).';
+ case FusionStyle.Simple
+ charges_r = [];
+ for unc_r = combvec(charges{end:-1:rank(1) + 1})
+ inn_r = cumprod(unc_r);
+ temp = reshape( ...
+ [repmat(unc_r(:).', 1, size(inn_r, 2)); inn_r(:).'], ...
+ [2 1] .* size(inn_r));
+ charges_r = [charges_r; temp(2:end, :).']; %#ok
+ end
+ otherwise
+ error('TBA');
+ end
+ end
+
+
+ %% Combine
+ coupled = unique(intersect(charges_l(:, end), charges_r(:, end)));
+ if isempty(coupled)
+ f = FusionTree;
+ return
+ end
+
+ charge_cell = cell(size(coupled));
+
+ for i = 1:length(coupled)
+ ind_l = coupled(i) == charges_l(:, end);
+ nnz_l = nnz(ind_l);
+ ind_r = coupled(i) == charges_r(:, end);
+ nnz_r = nnz(ind_r);
+ charge_cell{i} = [reshape(repmat(reshape( ...
+ charges_l(ind_l, 1:end - 1), 1, []), nnz_r, 1), nnz_r * nnz_l, []), ...
+ repmat(coupled(i), nnz_r * nnz_l, 1), ...
+ repmat(charges_r(ind_r, end - 1:-1:1), nnz_l, 1)];
+ end
+
+ f = FusionTree(vertcat(charge_cell{:}), [], [isdual{:}], rank);
+ f = sort(f);
+ end
+ end
+
+
+ %% Tree manipulations
+ methods
+ function [c, f] = artinbraid(f, i, inv)
+ % Compute the coefficients for braiding a set of two neighbouring strands.
+ %
+ % Arguments
+ % ---------
+ % f : FusionTree
+ % tree to swap.
+ %
+ % i : int
+ % `i` and `i+1` are the braided strands.
+ %
+ % inv : logical = false
+ % flag to indicate whether to perform an overcrossing (false) or an
+ % undercrossing (true).
+ %
+ % Returns
+ % -------
+ % c : sparse double
+ % matrix of coefficients that transform input to output trees.
+ % f(i) --> c(i,j) * f(j)
+ %
+ % f : FusionTree
+ % braided trees in canonical form.
+ if nargin < 3, inv = false; end
+
+ if hasmultiplicity(fusionstyle(f))
+ error('trees:TBA', 'Not implemented yet.');
+ end
+
+ assert(f.rank(2) == 0, 'Only defined for splitting trees.');
+
+ %% Special case - Abelian
+ if braidingstyle(f) == BraidingStyle.Abelian
+
+ if i == 1
+ f.charges(:, 1:2) = f.charges(:, [2 1]);
+ f.isdual(1:2) = f.isdual([2 1]);
+ [f, p] = sort(f);
+ c = sparse(1:length(f), invperm(p), ones(1, length(f)));
+ return
+ end
+
+ f.charges(:, [2 * i - 2 2 * i]) = f.charges(:, [2 * i 2 * i - 2]);
+ f.charges(:, 2 * i - 1) = f.charges(:, 2 * i - 3) * f.charges(:, 2 * i - 2);
+ f.isdual(i:i + 1) = f.isdual([i + 1 i]);
+ [f, p] = sort(f);
+ c = sparse(1:length(f), invperm(p), ones(1, length(f)));
+ return
+ end
+
+ %% Special case - first legs
+ % braiding by only an R-move
+ if i == 1
+ if inv
+ vals = conj(Rsymbol(f.charges(:, 2), f.charges(:, 1), f.charges(:, 3)));
+ else
+ vals = Rsymbol(f.charges(:, 1), f.charges(:, 2), f.charges(:, 3));
+ end
+ f.charges(:, 1:2) = f.charges(:, [2 1]);
+ f.isdual(1:2) = f.isdual([2 1]);
+ [f, p] = sort(f);
+ c = sparse(1:length(f), invperm(p), vals);
+ return
+ end
+
+ %% General case
+ % braiding by R-move, F-move, R-move
+ charges = f.charges; %#ok<*PROPLC>
+ mask = true(1, size(charges, 2)); mask(2 * i - 1) = false;
+ [diagcharges, order] = sortrows(charges(:, mask));
+ charges = charges(order, :);
+ [~, ia1, ic1] = unique(diagcharges, 'rows');
+ charges1 = charges(ia1, :);
+ blocks = cell(1, length(ia1));
+ newcharges = cell(1, length(ia1));
+
+ [~, ia2, ic2] = unique(charges1(:, [2 * i - 3 2 * i - 2 2 * i 2 * i + 1]), 'rows');
+
+ for j = 1:length(ia2)
+ a = charges1(ia2(j), 2 * i - 3);
+ b = charges1(ia2(j), 2 * i - 2);
+ cs = charges(ic1 == ia2(j), 2 * i - 1);
+ d = charges1(ia2(j), 2 * i);
+ e = charges1(ia2(j), 2 * i + 1);
+ fs = intersect(a * d, e * conj(b)).';
+ blocks{j} = braidingmatrix(a, b, cs, d, e, fs, inv);
+
+ for k = find(j == ic2).'
+ newcharges{k} = repmat(charges1(k, :), length(fs), 1);
+ newcharges{k}(:, 2 * i - 2:2 * i) = [newcharges{k}(:, 2 * i) fs(:) ...
+ newcharges{k}(:, 2 * i - 2)];
+ end
+
+ end
+
+ f.charges = vertcat(newcharges{:});
+ f.isdual(i:i + 1) = f.isdual([i + 1 i]);
+ c = sparse(blkdiag(blocks{ic2}));
+ [f, p] = sort(f);
+ c = c(invperm(order), p);
+ end
+
+ function [c, f] = bendleft(f)
+ % Compute the coefficients for bending a fusing leg to a splitting leg.
+ %
+ % Arguments
+ % ---------
+ % f : FusionTree
+ % tree to bend.
+ %
+ % Returns
+ % -------
+ % c : sparse double
+ % matrix of coefficients that transform input to output trees.
+ % f(i) --> c(i,j) * f(j)
+ %
+ % f : FusionTree
+ % bent trees in canonical form.
+ f = flip(f);
+ [c, f] = bendright(f);
+ f = flip(f);
+ c = conj(c);
+ return
+ end
+
+ function [c, f] = bendright(f)
+ % Compute the coefficients for bending a splitting leg to a fusing leg.
+ %
+ % Arguments
+ % ---------
+ % f : FusionTree
+ % tree to bend.
+ %
+ % Returns
+ % -------
+ % c : sparse double
+ % matrix of coefficients that transform input to output trees.
+ % f(i) --> c(i,j) * f(j)
+ %
+ % f : FusionTree
+ % bent trees in canonical form.
+ if hasmultiplicity(fusionstyle(f))
+ error('TBA');
+ end
+
+ if f.rank(1) == 0
+ error('tensors:trees:ArgError', 'Invalid rank [%d %d]', f.rank(1), f.rank(2));
+ elseif f.rank(1) == 1
+ [bc, ~, ic] = unique(f.charges(:, 1:2), 'rows');
+ b = bc(:, 1); a = repmat(one(b), size(b)); c = bc(:, 2);
+ else
+ ind = treeindsequence(f.rank(1)) - 1:treeindsequence(f.rank(1)) + 1;
+ [abc, ~, ic] = unique(f.charges(:, ind), 'rows');
+ a = abc(:, 1); b = abc(:, 2); c = abc(:, 3);
+ end
+
+ vals = sqrt(qdim(c) ./ qdim(a)) .* Bsymbol(a, b, c);
+ if f.isdual(f.rank(1)), vals = vals .* conj(frobeniusschur(conj(b))); end
+
+ f.isdual(f.rank(1)) = ~f.isdual(f.rank(1));
+
+ if f.rank(2) < 2
+ chargesR = [conj(f.charges(:, end - f.rank(2) - 1)) f.charges(:, end - f.rank(2) + 1:end)];
+ else
+ chargesR = [conj(f.charges(:, treeindsequence(f.rank(1)))) ...
+ f.charges(:, end - treeindsequence(f.rank(2)):end)];
+ end
+
+ chargesC = a(ic);
+
+ if f.rank(1) == 1
+ chargesL = [];
+ elseif f.rank(1) == 2
+ chargesL = f.charges(:, 1);
+ else
+ chargesL = f.charges(:, 1:treeindsequence(f.rank(1) - 1));
+ end
+
+ f.charges = [chargesL chargesC chargesR];
+
+ f.rank = f.rank + int16([-1 +1]);
+ c = sparse(1:length(f), 1:length(f), vals(ic));
+ end
+
+ 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.
+ %
+ % Arguments
+ % ---------
+ % f : FusionTree
+ % tree to braid.
+ %
+ % p : int
+ % permutation indices.
+ %
+ % lvl : int
+ % height of the strands, indicating over- and undercrossings.
+ %
+ % rank : int
+ % final number of splitting and fusing legs.
+ %
+ % Returns
+ % -------
+ % c : sparse double
+ % matrix of coefficients that transform input to output trees.
+ % f(i) --> c(i,j) * f(j)
+ %
+ % f : FusionTree
+ % braided trees in canonical form.
+ %
+ % Todo
+ % ----
+ % Currently this is done by first bending all legs to the splitting tree, this
+ % step is not necessary when no splitting legs are braided with fusing legs.
+ % Additionally this can be sped up significantly for charges with Abelian
+ % braiding style.
+ arguments
+ f
+ p (1, :) = 1:f.legs
+ lvl (1, :) = 1:f.legs
+ rank (1, 2) = f.rank
+ end
+ N = length(p);
+ assert(isperm(p), 'tensors:trees:ArgError', 'p is not a valid permutation.');
+ assert(N == f.legs, 'tensors:trees:ArgError', ...
+ 'p has the wrong number of elements.');
+
+ if all(p == 1:f.legs)
+ [c, f] = repartition(f, rank);
+ return
+ 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]);
+ end
+ [c_, f] = repartition(f, rank);
+ c = c * c_;
+ end
+
+ function [c, f] = permute(f, p, r)
+ % Compute the coefficients that bring a permuted tree into canonical form.
+ %
+ % Arguments
+ % ---------
+ % f : FusionTree
+ % tree to permute.
+ %
+ % p : int
+ % permutation indices.
+ %
+ % r : int
+ % final number of splitting and fusing legs.
+ %
+ % Returns
+ % -------
+ % c : sparse double
+ % matrix of coefficients that transform input to output trees.
+ % f(i) --> c(i,j) * f(j)
+ %
+ % f : FusionTree
+ % permuted trees in canonical form.
+ arguments
+ f
+ p (1,:)
+ r (1,2) = rank(f)
+ end
+
+ if isempty(f), c = []; return; end
+
+ assert(issymmetric(braidingstyle(f)));
+ [c, f] = braid(f, p, 1:f.legs, r);
+ end
+
+ function [c, f] = repartition(f, newrank)
+ % Compute the coefficients that bend legs to a desired rank.
+ %
+ % Arguments
+ % ---------
+ % f : FusionTree
+ % tree to repartition.
+ %
+ % newrank : int
+ % new rank of the fusion tree.
+ %
+ % Returns
+ % -------
+ % c : sparse double
+ % matrix of coefficients that transform input to output trees.
+ % f(i) --> c(i,j) * f(j)
+ %
+ % f : FusionTree
+ % repartitioned trees in canonical form.
+ arguments
+ f
+ newrank (1, 2) int16
+ end
+ oldrank = f.rank;
+ assert(sum(newrank) == sum(oldrank), 'tensors:trees:ArgError', ...
+ 'New rank is incompatible with old rank.');
+
+ if all(newrank == oldrank)
+ c = speye(length(f));
+ return
+ end
+
+ if newrank(1) > oldrank(1)
+ [c, f] = bendleft(f);
+ for i = 2:(newrank(1) - oldrank(1))
+ [c_, f] = bendleft(f);
+ c = c * c_;
+ end
+ else
+ [c, f] = bendright(f);
+ for i = 2:(oldrank(1) - newrank(1))
+ [c_, f] = bendright(f);
+ c = c * c_;
+ end
+ end
+ [f, p] = sort(f);
+ c = c(:, p);
+ end
+ end
+
+ %% Utility
+ methods
+ function style = braidingstyle(f)
+ style = f.charges.braidingstyle;
+ end
+ function bool = isallowed(f)
+ if ~hasmultiplicity(fusionstyle(f))
+ if f.rank(1) == 0
+ bool = f.charges(:, 1) == one(f.charges);
+ elseif f.rank(1) == 1
+ bool = f.charges(:, 1) == f.charges(:, 2);
+ else
+ bool = true(length(f), 1);
+ for i = 1:f.rank(1)-1
+ bool = bool & ...
+ Nsymbol(f.charges(:, 2*i-1), f.charges(:, 2*i), f.charges(:, 2*i+1));
+ end
+ end
+ if f.rank(2) == 0
+ bool = bool & f.charges(:, end) == one(f.charges);
+ elseif f.rank(2) == 1
+ bool = bool & f.charges(:, end) == f.charges(:, end-1);
+ else
+ for i = 1:f.rank(2)-1
+ bool = bool & ...
+ Nsymbol(f.charges(:, end-2*i+2), f.charges(:, end-2*i+1), f.charges(:, end-2*i));
+ end
+ end
+ return
+ end
+ error('TBA');
+ end
+
+ function f = flip(f)
+ f.charges = fliplr(f.charges);
+ f.isdual = fliplr(f.isdual);
+ f.rank = fliplr(f.rank);
+ if hasmultiplicity(fusionstyle(f))
+ f.vertices = fliplr(f.vertices);
+ end
+ end
+
+ function style = fusionstyle(f)
+ style = f.charges.fusionstyle;
+ end
+
+
+ function [f, p] = sort(f)
+ % sort - Sort the fusion trees into canonical order.
+ % [f, p] = sort(f)
+ if isempty(f), p = []; return; end
+ if hasmultiplicity(fusionstyle(f)), error('TBA'); end
+
+ cols = [treeindsequence(f.rank(1)) + 1 ...
+ (1:treeindsequence(f.rank(2))) + treeindsequence(f.rank(1)) + 1 ...
+ fliplr(1:treeindsequence(f.rank(1)))];
+ if nargout > 1
+ [f.charges, p] = sortrows(f.charges, cols);
+ else
+ f.charges = sortrows(f.charges, cols);
+ end
+ end
+ end
+ %% Setters and Getters
+ methods
+ function c = get.coupled(f)
+ c = f.charges(:, treeindsequence(f.rank(1)) + 1);
+ end
+
+ function unc = get.uncoupled(f)
+ unc = f.charges(:, ...
+ [treeindsequence(1:f.rank(1)) ...
+ end + 1 - treeindsequence(f.rank(2):-1:1)]);
+ end
+
+ function f = split(f)
+ f.isdual(f.rank(1)+1:end) = [];
+ f.charges(:, treeindsequence(f.rank(1)) + 2:end) = [];
+ if f.legs > 2 && hasmultiplicity(fusionstyle(f))
+ f.vertices(:, f.rank(1):end) = [];
+ end
+ f.rank(2) = 0;
+ end
+
+ function f = fuse(f)
+ f.isdual(1:f.rank(1)) = [];
+ f.charges(:, 1:treeindsequence(f.rank(1))) = [];
+ if f.legs > 2 && hasmultiplicity(fusionstyle(f))
+ f.vertices(:, 1:f.rank(1)-1) = [];
+ end
+ f.rank(1) = 0;
+ end
+ end
+
+
+ %% Utility
+ methods
+ function bool = isempty(f)
+ bool = size(f, 2) == 0;
+ end
+ function l = legs(f)
+ l = sum(f.rank);
+ end
+ function l = length(f)
+ l = size(f.charges, 1);
+ end
+ function varargout = size(f, varargin)
+ if nargin == 1
+ if nargout < 2
+ varargout{1} = [1, size(f.charges, 1)];
+ elseif nargout == 2
+ varargout = {1, size(f.charges, 1)};
+ else
+ error('wrong input');
+ end
+ return
+ end
+
+ sz = size(f);
+ if nargout == 1
+ varargout{1} = sz([varargin{:}]);
+ return
+ end
+
+ assert(nargout == length(varargin))
+ for i = 1:nargout
+ varargout{i} = sz(varargin{i});
+ end
+ end
+ function n = numArgumentsFromSubscript(~, ~, ~)
+ n = 1;
+ end
+ function varargout = subsref(f, s)
+ % subsref - Custom indexing behavior.
+ % varargout = subsref(f, s)
+ % overloads the syntax f(i), f{i} or f.i
+ switch s(1).type
+ case '()'
+ % assert(isscalar(s(1).subs), 'tensors:tree:indexing', ...
+ % 'Not a valid indexing expression.');
+ if length(s(1).subs) == 1
+ i = s(1).subs{1};
+ elseif length(s(1).subs) == 2 && strcmp(s(1).subs{1}, ':')
+ i = s(1).subs{2};
+ else
+ error('Invalid indexing');
+ end
+
+ if length(s) == 1
+ % f(i)
+ f.charges = f.charges(i, :);
+ if hasmultiplicity(f.fusionstyle)
+ f.vertices = f.vertices(i, :);
+ end
+ varargout{1:numArgumentsFromSubscript(f)} = f;
+
+ elseif length(s) == 2 && strcmp(s(2).type, '.')
+ % f(i).property
+ switch s(2).subs
+ case {'charges', 'vertices', 'coupled', ...
+ 'uncoupled', 'inner'}
+ prop = f.(s(2).subs);
+ [varargout{1:nargout}] = prop(i, :);
+
+ case {'isdual', 'rank'}
+ [varargout{1:nargout}] = subsref(f, s(2));
+
+ otherwise
+ error('tensors:tree:indexing', ...
+ 'Not a valid indexing expression.');
+ end
+
+ else
+ [varargout{1:nargout}] = builtin('subsref', f, s);
+ end
+
+ otherwise
+ [varargout{1:nargout}] = builtin('subsref', f, s);
+ end
+ end
+ function n = numel(f)
+ n = size(f.charges, 1);
+ end
+ function bool = ne(f1, f2)
+ bool = any(f1.charges ~= f2.charges, 2);
+ if hasmultiplicity(fusionstyle(f1))
+ bool = bool | any(f1.vertices ~= f2.vertices, 2);
+ end
+ end
+ function bool = eq(f1, f2)
+ bool = all(f1.charges == f2.charges, 2);
+ if hasmultiplicity(fusionstyle(f1))
+ bool = bool & all(f1.vertices == f2.vertices, 2);
+ end
+ end
+ end
+
+
+ %% Display
+ methods (Access = protected)
+ function header = getHeader(f)
+ if isscalar(f)
+ headerFormat = ' (%d, %d) %s %s:\n';
+ else
+ headerFormat = ' (%d, %d) %s %s array:\n';
+ end
+ header = sprintf(headerFormat, f.rank(1), f.rank(2), ...
+ class(f.charges), matlab.mixin.CustomDisplay.getClassNameForHeader(f));
+ end
+ function displayScalarObject(f)
+ displayNonScalarObject(f);
+ end
+ function displayNonScalarObject(f)
+ header = getHeader(f);
+ disp(header);
+ fprintf(' isdual:\n %s\n', num2str(f.isdual));
+ fprintf(' charges:\n');
+ chargeStr = strjust(pad(string(f.charges)), 'center');
+ chargeFormat = [' ', ...
+ repmat('%s ', 1, max(f.rank(1), 2*f.rank(1)-2)), ...
+ '| %s |', ...
+ repmat(' %s', 1, max(f.rank(2), 2*f.rank(2)-2)) '\n'];
+ fprintf(chargeFormat, chargeStr.');
+ end
+ end
+
+
+ %% Constructors and converters
+ methods
+ function A = double(f)
+ % double - Return the tensor representation of a fusion tree.
+ % A = double(f)
+ assert(issymmetric(braidingstyle(f)), ...
+ 'tensors:tree:invalidCharge', ...
+ 'Tensor representation is only defined for charges with bosonic braiding.');
+
+ sz = zeros(1, f.legs);
+ uncs = cell(1, f.legs);
+ for i = 1:length(sz)
+ uncs{i} = unique(f.uncoupled(:, i));
+ sz(i) = length(uncs{i});
+ end
+ A_cell = cell(sz);
+
+ inds = arrayfun(@(x) 1:x, sz, 'UniformOutput', false);
+ for ind = num2cell(combvec(inds{:}))
+ unc = cellfun(@(x, i) x(i), uncs, ind.');
+ locb = all(unc == f.uncoupled, 2);
+ if any(locb)
+ A_cell{ind{:}} = fusiontensor(subsref(f, substruct('()', {locb})));
+ else
+ A_cell{ind{:}} = zeros(qdim(unc));
+ end
+ end
+
+ A = cell2mat(A_cell);
+ end
+ function C = fusiontensor(f)
+ % Construct an array representation of a fusion tree.
+ %
+ % Arguments
+ % ---------
+ % f : FusionTree
+ % an array of fusion trees to convert.
+ %
+ % Returns
+ % -------
+ % C : cell
+ % a cell array containing the array representations of f.
+
+ if fusionstyle(f) == FusionStyle.Unique
+ C = num2cell(ones(size(f)));
+ return;
+ end
+
+
+ if length(f) > 1
+ C = arrayfun(@fusiontensor, f);
+% C = cell(size(f));
+% for i = 1:length(f)
+% C(i) = fusiontensor(f(i));
+% end
+ return
+ end
+
+ multi = hasmultiplicity(fusionstyle(f));
+ switch f.rank(1)
+ case 0
+ C_split = 1;
+ case 1
+ if f.isdual(1)
+ C_split = flipper(f.charges(1));
+ else
+ C_split = squeeze(fusiontensor(one(f.charges), ...
+ f.charges(1), f.charges(2)));
+ end
+ otherwise
+ C_split = fusiontensor(f.charges(1), f.charges(2), f.charges(3));
+ if multi
+ C_split = C_split(:, :, :, f.vertices(1));
+ end
+ if f.isdual(1)
+ C_split = contract(C_split, [1 -2 -3], ...
+ flipper(f.charges(1)), [-1 1]);
+ end
+ if f.isdual(2)
+ C_split = contract(C_split, [-1 1 -3], ...
+ flipper(f.charges(2)), [-2 1]);
+ end
+
+ for i = 3:f.rank(1)
+ C_split_ = fusiontensor(f.charges(2*i-3), f.charges(2*i-2), ...
+ f.charges(2*i-1));
+ if multi
+ C_split_ = C_split_(:, :, :, f.vertices(i));
+ end
+ if f.isdual(i)
+ C_split_ = contract(C_split_, [-1 1 -3], ...
+ flipper(f.charges(2*i-2)), [-2 1]);
+ end
+ C_split = contract(C_split, [-(1:i-1) 1], ...
+ C_split_, [1 -i -i-1]);
+ end
+ end
+ switch f.rank(2)
+ case 0
+ C_fuse = 1;
+ case 1
+ if f.isdual(end)
+ C_fuse = flipper(f.charges(end));
+ else
+ C_fuse = squeeze(fusiontensor(one(f.charges), ...
+ f.charges(end), f.charges(end-1)));
+ end
+ otherwise
+ C_fuse = fusiontensor(f.charges(end), f.charges(end-1), ...
+ f.charges(end-2));
+ if multi
+ C_fuse = C_fuse(:, :, :, f.vertices(end));
+ end
+ if f.isdual(end)
+ C_fuse = contract(C_fuse, [1 -2 -3], flipper(f.charges(end)), [-1 1]);
+ end
+ if f.isdual(end-1)
+ C_fuse = contract(C_fuse, [-1 1 -3], flipper(f.charges(end-1)), [-2 1]);
+ end
+
+ for i = 3:f.rank(2)
+ C_fuse_ = fusiontensor(f.charges(end-2*i+4), f.charges(end-2*i+3), ...
+ f.charges(end-2*i+2));
+ if multi
+ C_fuse_ = C_fuse_(:, :, :, f.vertices(end-i+1));
+ end
+ if f.isdual(end-i+1)
+ C_fuse_ = contract(C_fuse_, [-1 1 -3], ...
+ flipper(f.charges(end-2*i+3)), [-2 1]);
+ end
+ C_fuse = contract(C_fuse, [-(1:i-1) 1], ...
+ C_fuse_, [1 -i -i-1]);
+ end
+ end
+ if f.rank(1) == 0
+ C = permute(conj(C_fuse), f.rank(2):-1:1);
+ elseif f.rank(2) == 0
+ C = C_split;
+ else
+ C = contract(C_split, [-(1:f.rank(1)) 1], ...
+ conj(C_fuse), [-(f.rank(2):-1:1)-f.rank(1) 1]);
+ end
+ C = {C};
+ end
+ end
+end
diff --git a/src/tensors/Tensor.m b/src/tensors/Tensor.m
new file mode 100644
index 0000000..f69f6c8
--- /dev/null
+++ b/src/tensors/Tensor.m
@@ -0,0 +1,2050 @@
+classdef Tensor
+ %TENSOR Summary of this class goes here
+ % Detailed explanation goes here
+
+ properties
+ codomain
+ domain
+ end
+
+ properties
+ var
+ end
+
+ %% Constructors
+ methods
+ function t = Tensor(varargin)
+ % Create a tensor object.
+ %
+ % Usage
+ % -----
+ % :code:`t = Tensor(array)`
+ %
+ % :code:`t = Tensor(codomain, domain)`
+ %
+ % Arguments
+ % ---------
+ % array : numeric
+ % numeric input array to convert to a :class:`Tensor`
+ %
+ % codomain, domain : :class:`AbstractSpace`
+ % spaces that define the structure of the output tensor.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % new tensor object.
+
+ if nargin == 0, return; end
+
+ % t = Tensor(array)
+ if nargin == 1 && isnumeric(varargin{1})
+ t.domain = [];
+ t.codomain = CartesianSpace.new(size(varargin{1}));
+ t.var = fill_tensor_data(AbstractBlock.new(t.codomain, t.domain), ...
+ varargin);
+ return;
+ end
+
+ % t = Tensor(codomain, domain)
+ if nargin == 2
+ codomain = varargin{1};
+ domain = varargin{2};
+
+ assert(~isempty(codomain) || ~isempty(domain), ...
+ 'Cannot create (0,0) tensors.');
+ t.domain = domain;
+ t.codomain = codomain;
+
+ t.var = AbstractBlock.new(codomain, domain);
+ return
+ end
+
+ error('tensors:SyntaxError', 'Invalid constructor syntax.');
+ end
+
+ function t = fill_matrix(t, data, charges)
+ % Fill the matrix blocks of a tensor.
+ %
+ % Usage
+ % -----
+ % :code:`t = fill_matrix(t, matrices, charges)`
+ %
+ % :code:`t = fill_matrix(t, fun, charges)`
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor to fill into.
+ %
+ % matrices : cell or 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`
+ % optional list of charges to identify the matrix blocks.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % filled tensor.
+
+ arguments
+ t
+ data
+ charges = [];
+ end
+
+ if isnumeric(data), data = {data}; end
+
+ if iscell(data)
+ t.var = fill_matrix_data(t.var, data, charges);
+ else
+ t.var = fill_matrix_fun(t.var, data, charges);
+ end
+ end
+
+ function t = fill_tensor(t, data, trees)
+ % Fill the tensor blocks of a tensor.
+ %
+ % Usage
+ % -----
+ % :code:`t = fill_tensor(t, tensors, trees)`
+ %
+ % :code:`t = fill_tensor(t, fun, trees)`
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor to fill into.
+ %
+ % tensors : cell or 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`
+ % 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);
+ else
+ t.var = fill_tensor_fun(t.var, data, trees);
+ end
+ end
+ end
+
+ methods (Static)
+ function t = new(fun, varargin, kwargs)
+ % Create a tensor with data using a function handle.
+ %
+ % Usage
+ % -----
+ % :code:`Tensor.new(fun, dims)`
+ %
+ % :code:`Tensor.new(fun, dims, arrows)`
+ %
+ % :code:`Tensor.new(fun, charges, degeneracies, arrow)` TODO
+ %
+ % :code:`Tensor.new(fun, tensor)`
+ %
+ % :code:'Tensor.new(..., 'Rank', r, 'Mode', mode)
+ %
+ % Arguments
+ % ---------
+ % fun : :class:`function_handle`
+ % 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
+ % list of dimensions for non-symmetric tensors.
+ %
+ % arrows : logical
+ % optional list of arrows for tensor legs.
+ %
+ % tensor : :class:`Tensor`
+ % input tensor to copy structure.
+ %
+ % Repeating Arguments
+ % -------------------
+ % charges : cell
+ % list of charges for each tensor index.
+ %
+ % degeneracies : cell
+ % list of degeneracies for each tensor index.
+ %
+ % arrow : logical
+ % arrow for each tensor index.
+ %
+ % Keyword Arguments
+ % -----------------
+ % Rank : int (1, 2)
+ % rank of the constructed tensor. By default this is [nspaces(t) 0].
+ %
+ % Mode : '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`
+ % output tensor.
+
+ arguments
+ fun = []
+ end
+
+ arguments (Repeating)
+ varargin
+ end
+
+ arguments
+ kwargs.Rank
+ kwargs.Mode = 'matrix'
+ end
+
+ % Parse special constructors
+ if ~isempty(varargin{1}) && isnumeric(varargin{1})
+ if length(varargin) == 1
+ 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});
+ end
+
+ if ~isfield(kwargs, 'Rank')
+ kwargs.Rank = [length(spaces) 0];
+ else
+ assert(sum(kwargs.Rank) == length(spaces), 'tensors:ArgumentError', ...
+ 'User supplied invalid rank.');
+ end
+
+ codomain = spaces(1:kwargs.Rank(1));
+ domain = spaces(kwargs.Rank(1) + (1:kwargs.Rank(2)))';
+
+ elseif length(varargin) == 1 && isa(varargin{1}, 'Tensor')
+ codomain = varargin{1}.codomain;
+ domain = varargin{1}.domain;
+
+ if isfield(kwargs, 'Rank')
+ assert(isequal(kwargs.Rank, [length(codomain) length(domain)]), ...
+ 'tensors:ArgumentError', 'Rank and spaces incompatible.');
+ end
+
+ elseif isa(varargin{1}, 'AbstractSpace') || isa(varargin{2}, 'AbstractSpace')
+ codomain = varargin{1};
+ domain = varargin{2};
+
+ if isfield(kwargs, 'Rank')
+ assert(isequal(kwargs.Rank, [length(codomain) length(domain)]), ...
+ 'tensors:ArgumentError', 'Rank and spaces incompatible.');
+ end
+ end
+
+ t = Tensor(codomain, domain);
+
+ % 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');
+ end
+ end
+ end
+
+ function t = zeros(varargin)
+ t = Tensor.new(@(dims, charge) zeros(dims), varargin{:});
+ end
+
+ function t = ones(varargin)
+ t = Tensor.new(@(dims, charge) ones(dims), varargin{:});
+ end
+
+ function t = eye(varargin)
+ t = Tensor.new(@(dims, charge) eye(dims), varargin{:});
+ end
+
+ function t = rand(varargin)
+ t = Tensor.new(@(dims, charge) rand(dims), varargin{:});
+ end
+
+ function t = randc(varargin)
+ t = Tensor.new(@(dims, charge) randc(dims), varargin{:});
+ end
+
+ function t = randnc(varargin)
+ t = Tensor.new(@(dims, charge) randnc(dims), varargin{:});
+ end
+ end
+
+
+
+
+ %% Structure
+ methods
+ function n = indin(t)
+ n = length(t.domain);
+ end
+
+ function n = indout(t)
+ n = length(t.codomain);
+ end
+
+ function n = nspaces(t)
+ n = length(t.domain) + length(t.codomain);
+ end
+
+ function r = rank(t, i)
+ r = [length(t.codomain) length(t.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 nargin > 1
+ sp = sp(inds);
+ end
+ end
+
+ function f = fusiontrees(t)
+ f = fusiontrees(t.codomain, t.domain);
+ end
+
+ function b = tensorblocks(t)
+ b = tensorblocks(t.var);
+ 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.
+ %
+ % Arguments
+ % ---------
+ % A, B : :class:`Tensor`
+ %
+ % Returns
+ % -------
+ % d : numeric
+ % Euclidean distance, defined as the norm of the distance.
+
+ assert(isequal(size(A), size(B)) || isscalar(A) || isscalar(B), ...
+ 'tensors:SizeError', 'Incompatible sizes for vectorized function.');
+
+ % make everything a vector
+ A = arrayfun(@repartition, A);
+ B = arrayfun(@repartition, B);
+
+ d = norm(A - B);
+ end
+ end
+
+
+ %% Linear algebra
+ methods
+ function t = conj(t)
+ % Compute the conjugate of a tensor. This is defined as taking an element-wise
+ % conjugate, but implemented as taking the adjoint and reversing the order of
+ % the indices.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % conjugate tensor.
+
+ t = permute(t', nspaces(t):-1:1, rank(t));
+ 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
+ % -----
+ % t = ctranspose(t)
+ % t = t'
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % adjoint tensor.
+
+ [t.codomain, t.domain] = swapvars(t.codomain, t.domain);
+ t.var = t.var';
+ end
+
+ function d = dot(t1, t2)
+ % 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`
+ % tensors of equal structure.
+ %
+ % Returns
+ % -------
+ % d : 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.');
+
+ 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');
+ end
+ end
+
+ function t1 = minus(t1, t2)
+ if isnumeric(t1), t1 = t1 + (-t2); return; end
+ assert(isequal(size(t1), size(t2)), 'Incompatible sizes for vectorized minus.');
+
+ for i = 1:numel(t1)
+ if isnumeric(t2(i))
+ assert(isequal(t1(i).codomain, t1(i).domain), 'tensors:NonSquare', ...
+ '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);
+ else
+ assert(isequal(t1(i).domain, t2(i).domain) && ...
+ isequal(t1(i).codomain, t2(i).codomain), 'tensors:SpaceMismatch', ...
+ 'Cannot subtract tensors of different structures.');
+ t1(i).var = t1(i).var - t2(i).var;
+ end
+ end
+ end
+
+ function t = mrdivide(t, a)
+ % Scalar division of a tensors.
+ %
+ % .. todo::
+ % Implement for tensors.
+ %
+ % Usage
+ % -----
+ % t = rdivide(t, a)
+ %
+ % t = t ./ a
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % a : numeric
+ % input scalar.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % output tensor.
+
+ assert(isnumeric(a) && isscalar(a), 'Only implemented for scalar numeric a.');
+ t = t ./ a;
+ end
+
+ function C = mtimes(A, B)
+ % Compose tensors, interpreted as maps from domain to codomain.
+ %
+ % Usage
+ % -----
+ % A = mtimes(A, B)
+ %
+ % Arguments
+ % ---------
+ % A, B : :class:`Tensor`
+ % input tensors, satisfying A.domain = B.codomain.
+ %
+ % Returns
+ % -------
+ % C : :class:`Tensor`
+ % output tensor.
+
+ 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
+ end
+ end
+ return
+ end
+
+ % 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
+ end
+ end
+ end
+
+ function n = norm(t, p)
+ % Compute the matrix p-norm of a tensor.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor, considered as a matrix from domain to codomain.
+ %
+ % p : 1, 2, inf or 'fro'
+ % type of norm to compute
+ %
+ % Returns
+ % -------
+ % n : :class:`double`
+ % matrix norm of the tensor.
+
+ arguments
+ t
+ p = 'fro'
+ end
+
+ switch p
+ case {1, 2, Inf}
+ n = 0;
+ for i = 1:numel(t)
+ mblocks = matrixblocks(t(i).var);
+ for j = 1:length(mblocks)
+ n = max(norm(mblocks{j}, p), n);
+ end
+ end
+
+ case 'fro'
+ n = 0;
+ for i = 1:numel(t)
+ [mblocks, mcharges] = matrixblocks(t(i).var);
+ qdims = qdim(mcharges);
+ for j = 1:length(mblocks)
+ n = n + qdims(j) * sum(conj(mblocks{j}) .* mblocks{j}, 'all');
+ end
+ end
+ n = sqrt(n);
+
+ otherwise
+ error('tensors:ArgumentError', 'Invalid norm selected.');
+ end
+ end
+
+ function t = normalize(t)
+ for i = 1:numel(t)
+ t(i) = t(i) .* (1 / norm(t(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.');
+
+ for i = 1:numel(t1)
+ if isnumeric(t2(i))
+ assert(isequal(t1(i).codomain, t1(i).domain), 'tensors:NonSquare', ...
+ '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);
+ else
+ assert(isequal(t1(i).domain, t2(i).domain) && ...
+ isequal(t1(i).codomain, t2(i).codomain), 'tensors:SpaceMismatch', ...
+ 'Cannot subtract tensors of different structures.');
+ t1(i).var = t1(i).var + t2(i).var;
+ end
+ end
+ end
+
+ function t = permute(t, p, r)
+ % Permute the spaces of a tensor.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % p : (1, :) int
+ % permutation vector, by default a trivial permutation.
+ %
+ % r : (1, 2) int
+ % rank of the output tensor, by default equal to the rank of the input tensor.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % permuted tensor with desired rank.
+
+ arguments
+ t
+ p = 1:nspaces(t)
+ r = rank(t)
+ end
+
+ if isempty(p), p = 1:nspaces(t); end
+ if isempty(r), r = rank(t); end
+ if (all(p == 1:nspaces(t)) && all(rank(t) == r)), return; end
+
+ persistent cache
+ if isempty(cache), cache = LRU; end
+
+ if Options.CacheEnabled()
+ key = GetMD5({GetMD5_helper(t.codomain), GetMD5_helper(t.domain), p, r}, ...
+ 'Array', 'hex');
+ med = get(cache, key);
+ if isempty(med)
+ med = struct;
+ med.structure = similar(t, invperm(p), 'Rank', r);
+ med.map = permute(fusiontrees(t), p, r);
+ cache = set(cache, key, med);
+ end
+
+ tdst = med.structure;
+ map = med.map;
+ else
+ tdst = similar(t, invperm(p), 'Rank', r);
+ map = permute(fusiontrees(t.codomain, t.domain), p, r);
+ end
+
+ tdst.var = axpby(1, t.var, 0, tdst.var, p, map);
+ t = tdst;
+ end
+
+ function t = repartition(t, r)
+ % Permute the spaces of a tensor.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % r : (1, 2) int
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % repartitioned tensor with desired rank.
+
+ arguments
+ t
+ 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);
+ end
+
+ function t = rdivide(t, a)
+ % Scalar division of a tensor and a scalar.
+ %
+ % Usage
+ % -----
+ % t = rdivide(t, a)
+ %
+ % t = t ./ a
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % a : numeric
+ % input scalar.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % output tensor.
+
+ t.var = rdivide(t.var, a);
+ end
+
+ function C = tensorprod(A, B, dimA, dimB, ca, cb, options)
+ % Compute the contraction of two tensors through the selected spaces.
+ %
+ % Arguments
+ % ---------
+ % A, B : :class:`Tensor`
+ % input tensors, must satisfy space(A, dimA) = conj(space(B, dimB)).
+ %
+ % dimA, dimB : (1, :) int
+ % selected indices to contract.
+ %
+ % Keyword Arguments
+ % -----------------
+ % NumDimensionsA : int
+ % number of spaces of A, to satisfy builtin tensorprod syntax.
+ %
+ % Returns
+ % -------
+ % C : :class:`Tensor` or 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.
+
+ arguments
+ A
+ B
+ dimA
+ dimB
+ ca = false
+ cb = false
+ options.NumDimensionsA
+ end
+
+ uncA = 1:nspaces(A); uncA(dimA) = [];
+ iA = [uncA dimA]; rA = [length(uncA) length(dimA)];
+
+ uncB = 1:nspaces(B); uncB(dimB) = [];
+ iB = [flip(dimB) uncB]; rB = [length(dimB) length(uncB)];
+
+ if ca
+ A = A';
+ idx = length(iA):-1:1;
+ iA = idx(iA);
+ end
+
+ if cb
+ B = B';
+ idx = length(iB):-1:1;
+ iB = idx(iB);
+ end
+
+ persistent 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), ...
+ dimA, dimB, ca, cb}, 'Array', 'hex');
+ med = get(cache, key);
+ if isempty(med)
+ med = struct;
+ A_ = similar(A, invperm(iA), 'Rank', rA);
+ med.varA = A_.var;
+ med.mapA = permute(fusiontrees(A), iA, rA);
+
+ B_ = similar(B, invperm(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);
+ else
+ med.C = [];
+ end
+ cache = set(cache, key, med);
+ end
+
+ varA = axpby(1, A.var, 0, med.varA, iA, med.mapA);
+ varB = axpby(1, B.var, 0, med.varB, iB, med.mapB);
+ C = med.C;
+ if isempty(C)
+ Ablocks = matrixblocks(varA);
+ Bblocks = matrixblocks(varB);
+ C = 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);
+
+ assert(isequal(A.domain, B.codomain), 'tensors:SpaceMismatch', ...
+ 'Contracted spaces incompatible.');
+
+ % scalar result
+ if isempty(uncA) && isempty(uncB)
+ Ablocks = matrixblocks(A);
+ Bblocks = matrixblocks(B);
+ C = horzcat(Ablocks{:}) * vertcat(Bblocks{:});
+ return
+ end
+
+ % tensor results
+ C = Tensor(A.codomain, B.domain);
+ C.var = mul(C.var, A.var, B.var);
+ end
+
+ function t = times(t, a)
+ % Scalar product of a tensor and a scalar.
+ %
+ % Usage
+ % -----
+ % t = times(t, a)
+ % t = t .* a
+ % t = a .* t
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % a : numeric
+ % input scalar.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % output tensor.
+
+ if isnumeric(t), [t, a] = swapvars(t, a); end
+ t.var = times(t.var, a);
+ end
+
+ function tr = trace(t)
+ % Compute the matrix trace of a tensor.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor, considered as a matrix from domain to codomain.
+ %
+ % Returns
+ % -------
+ % tr : double
+ % matrix trace of the tensor.
+
+ tr = 0;
+ for i = 1:numel(t)
+ [mblocks, mcharges] = matrixblocks(t(i).var);
+ qdims = qdim(mcharges);
+ for j = 1:length(mblocks)
+ tr = tr + qdims(j) * trace(mblocks{j});
+ end
+ end
+ end
+
+ 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.
+ %
+ % Usage
+ % -----
+ % t = transpose(t, p, rank)
+ % t = t.'
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % p : (1, :) int
+ % permutation vector, which must be cyclic. By default this is no permutation.
+ %
+ % r : (1, 2) int
+ % rank of the output tensor, by default equal to the rank of the input tensor.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % transposed output tensor.
+
+ error('TBA');
+ end
+
+ function t = uplus(t)
+
+ end
+
+ function t = uminus(t)
+ t.var = -t.var;
+ end
+ end
+
+
+ %% Factorizations
+ methods
+ function [V, D, W] = eig(A)
+ % Compute the eigenvalues and eigenvectors of a square tensor.
+ %
+ % Usage
+ % -----
+ % D = eig(A)
+ %
+ % [V, D] = eig(A)
+ %
+ % [V, D, W] = eig(A)
+ %
+ % Arguments
+ % ---------
+ % A : :class:`Tensor`
+ % square input tensor.
+ %
+ % Returns
+ % -------
+ % D : (:,:) :class:`Tensor`
+ % diagonal matrix of eigenvalues.
+ %
+ % V : (1,:) :class:`Tensor`
+ % row vector of right eigenvectors such that A * V = V * D.
+ %
+ % W : (1,:) :class:`Tensor`
+ % row vector of left eigenvectors such that W' * A = D * W'.
+
+ assert(isequal(A.codomain, A.domain), 'tensors:ArgumentError', ...
+ 'Input should be square.');
+
+ dims = struct;
+ [mblocks, dims.charges] = matrixblocks(A);
+ Ds = cell(size(mblocks));
+ dims.degeneracies = zeros(size(mblocks));
+
+ if nargout > 2
+ Vs = cell(size(mblocks));
+ Ws = cell(size(mblocks));
+ for i = 1:length(mblocks)
+ [Vs{i}, Ds{i}, Ws{i}] = eig(mblocks{i});
+ dims.degeneracies(i) = size(Ds{i}, 1);
+ end
+ elseif nargout > 1
+ Vs = cell(size(mblocks));
+ for i = 1:length(mblocks)
+ [Vs{i}, Ds{i}] = eig(mblocks{i});
+ dims.degeneracies(i) = size(Ds{i}, 1);
+ end
+
+ else
+ for i = 1:length(mblocks)
+ Ds{i} = diag(eig(mblocks{i}));
+ dims.degeneracies(i) = size(Ds{i}, 1);
+ end
+ end
+
+ space = A.domain.new(dims, false);
+ D = A.zeros(space, space);
+ D.var = fill_matrix_data(D.var, Ds, dims.charges);
+
+ if nargout <= 1
+ V = D;
+ return
+ end
+ if nargout > 1
+ V = A.eye(A.domain, space);
+ V.var = fill_matrix_data(V.var, Vs, dims.charges);
+ end
+ if nargout > 2
+ W = A.eye(A.domain, space);
+ W.var = fill_matrix_data(W.var, Ws, dims.charges);
+ end
+ end
+
+ function [Q, R] = leftorth(t, p1, p2, alg)
+ % Factorize a tensor into an orthonormal basis `Q` and remainder `R`, such that
+ % permute(t, [p1 p2], [length(p1) length(p2)]) = Q * R.
+ %
+ % Usage
+ % -----
+ % [Q, R] = leftorth(t, p1, p2, alg)
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor to factorize.
+ %
+ % p1, p2 : int
+ % partition of left and right indices, by default this is the partition of the
+ % input tensor.
+ %
+ % alg : char or 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:`Tensor`
+ % Orthonormal basis tensor
+ %
+ % R : :class:`Tensor`
+ % Remainder tensor, depends on selected algorithm.
+
+ arguments
+ t
+ p1 = 1:t.rank(1)
+ p2 = t.rank(1) + (1:t.rank(2))
+ alg {mustBeMember(alg, {'qr', 'qrpos', 'ql', 'qlpos', 'polar', 'svd'})} ...
+ = 'qrpos'
+ 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)]);
+
+ dims = struct;
+ [mblocks, dims.charges] = matrixblocks(t);
+
+ Qs = cell(size(mblocks));
+ Rs = cell(size(mblocks));
+ dims.degeneracies = zeros(size(mblocks));
+
+ for i = 1:length(mblocks)
+ [Qs{i}, Rs{i}] = leftorth(mblocks{i}, alg);
+ dims.degeneracies(i) = size(Qs{i}, 2);
+ end
+
+ V = t.codomain.new(dims, false);
+
+ if strcmp(alg, 'polar')
+ assert(isequal(V, prod(t.domain)));
+ W = t.domain;
+ elseif length(p1) == 1 && V == t.codomain
+ W = t.codomain;
+ elseif length(p2) == 1 && V == t.domain
+ W = t.domain;
+ else
+ W = V;
+ end
+
+ Q = Tensor.eye(t.codomain, W);
+ Q.var = fill_matrix_data(Q.var, Qs, dims.charges);
+
+ R = Tensor.zeros(W, t.domain);
+ R.var = fill_matrix_data(R.var, Rs, dims.charges);
+ end
+
+ function [R, Q] = rightorth(t, p1, p2, alg)
+ % Factorize a tensor into an orthonormal basis `Q` and remainder `L`, such that
+ % permute(t, [p1 p2], [length(p1) length(p2)]) = L * Q.
+ %
+ % Usage
+ % -----
+ % [R, Q] = rightorth(t, p1, p2, alg)
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor to factorize.
+ %
+ % p1, p2 : int
+ % partition of left and right indices, by default this is the partition of the
+ % input tensor.
+ %
+ % alg : char or 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:`Tensor`
+ % Remainder tensor, depends on selected algorithm.
+ %
+ % Q : :class:`Tensor`
+ % Orthonormal basis tensor.
+
+ arguments
+ t
+ p1 = 1:t.rank(1)
+ p2 = t.rank(1) + (1:t.rank(2))
+ 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)]);
+
+ dims = struct;
+ [mblocks, dims.charges] = matrixblocks(t);
+ Qs = cell(size(mblocks));
+ Rs = cell(size(mblocks));
+ dims.degeneracies = zeros(size(mblocks));
+
+ for i = 1:length(mblocks)
+ [Rs{i}, Qs{i}] = rightorth(mblocks{i}, alg);
+ dims.degeneracies(i) = size(Qs{i}, 1);
+ end
+
+ V = t.domain.new(dims, false);
+
+ if strcmp(alg, 'polar')
+ assert(isequal(V, prod(t.codomain)));
+ W = t.codomain;
+ elseif length(p1) == 1 && V == t.codomain
+ W = t.codomain;
+ elseif length(p2) == 1 && V == t.domain
+ W = t.domain;
+ else
+ W = V;
+ end
+
+ Q = Tensor.eye(W, t.domain);
+ Q.var = fill_matrix_data(Q.var, Qs, dims.charges);
+
+ R = Tensor.zeros(t.codomain, W);
+ R.var = fill_matrix_data(R.var, Rs, dims.charges);
+ end
+
+ function N = leftnull(t, p1, p2, alg, atol)
+
+ arguments
+ t
+ p1 = 1:t.rank(1)
+ p2 = t.rank(1) + (1:t.rank(2))
+ alg = 'svd'
+ atol = norm(t) * eps(underlyingType(t))
+ 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)]);
+
+ dims = struct;
+ [mblocks, dims.charges] = matrixblocks(t);
+ Ns = cell(size(mblocks));
+ dims.degeneracies = zeros(size(mblocks));
+
+ for i = 1:length(mblocks)
+ Ns{i} = leftnull(mblocks{i}, alg, atol);
+ dims.degeneracies(i) = size(Ns{i}, 2);
+ end
+
+ N = Tensor.eye(t.codomain, t.codomain.new(dims, false));
+ N.var = fill_matrix_data(N.var, Ns, dims.charges);
+ end
+
+ function N = rightnull(t, p1, p2, alg, atol)
+
+ arguments
+ t
+ p1 = 1:t.rank(1)
+ p2 = t.rank(1) + (1:t.rank(2))
+ alg = 'svd'
+ atol = norm(t) * eps(underlyingType(t))
+ 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)]);
+
+ 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
+
+ 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
+ % 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, ...)
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % p1, p2 : 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.
+ %
+ % TruncBelow : numeric
+ % truncate such that there are no singular values below this value.
+ %
+ % TruncSpace : :class:`AbstractSpace`
+ % truncate such that the space of S is smaller than this value.
+ %
+ % Returns
+ % -------
+ % 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)]).
+ %
+ % eta : numeric
+ % truncation error.
+
+ arguments
+ t
+ p1 = 1:t.rank(1)
+ p2 = t.rank(1) + (1:t.rank(2))
+ trunc.TruncDim
+ trunc.TruncBelow
+ trunc.TruncSpace
+ end
+
+ t = permute(t, [p1 p2], [length(p1) length(p2)]);
+
+ dims = struct;
+ [mblocks, dims.charges] = matrixblocks(t);
+ Us = cell(size(mblocks));
+ Ss = cell(size(mblocks));
+ Vs = cell(size(mblocks));
+ dims.degeneracies = zeros(size(mblocks));
+
+ doTrunc = ~isempty(fieldnames(trunc));
+ for i = 1:length(mblocks)
+ if doTrunc
+ [Us{i}, Ss{i}, Vs{i}] = svd(mblocks{i}, 'econ');
+ dims.degeneracies(i) = size(Ss{i}, 1);
+ else
+ [Us{i}, Ss{i}, Vs{i}] = svd(mblocks{i});
+ end
+ Vs{i} = Vs{i}';
+ end
+
+ if isfield(trunc, 'TruncBelow')
+ for i = 1:length(mblocks)
+ dims.degeneracies(i) = sum(diag(Ss{i}) > trunc.TruncBelow);
+
+ Us{i} = Us{i}(:, 1:dims.degeneracies(i));
+ Ss{i} = Ss{i}(1:dims.degeneracies(i), 1:dims.degeneracies(i));
+ Vs{i} = Vs{i}(1:dims.degeneracies(i), :);
+ end
+ end
+ if isfield(trunc, 'TruncDim')
+ for i = 1:length(mblocks)
+ dims.degeneracies(i) = min(dims.degeneracies(i), trunc.TruncDim);
+
+ Us{i} = Us{i}(:, 1:dims.degeneracies(i));
+ Ss{i} = Ss{i}(1:1:dims.degeneracies(i), 1:1:dims.degeneracies(i));
+ Vs{i} = Vs{i}(1:1:dims.degeneracies(i), :);
+ end
+ end
+ if isfield(trunc, 'TruncSpace')
+ error('TBA');
+ end
+
+
+
+ if ~doTrunc
+ W1 = prod(t.codomain);
+ W2 = prod(t.domain);
+
+ U = Tensor.eye(t.codomain, W1);
+ S = Tensor.zeros(W1, W2);
+ V = Tensor.eye(W2, t.domain);
+ else
+ W = t.domain.new(dims, false);
+
+ U = Tensor.eye(t.codomain, W);
+ S = Tensor.zeros(W, W);
+ V = Tensor.eye(W, t.domain);
+ 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);
+ end
+ end
+
+
+ %% Matrix functions
+ methods
+ function t = expm(t)
+ % Compute the matrix exponential of a square tensor. This is done via a scaling
+ % and squaring algorithm with a Pade approximation, block-wise.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % output tensor.
+
+ assert(isequal(t.codomain, t.domain), 'tensors:ArgumentError', ...
+ 'Input should be square.');
+
+ mblocks = matrixblocks(t);
+ for i = 1:length(mblocks)
+ mblocks{i} = expm(mblocks{i});
+ end
+ t.var = fill_matrix_data(t.var, mblocks);
+ end
+
+ function t = inv(t)
+ % Compute the matrix inverse of a square tensor, such that t * inv(t) = I.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % Returns
+ % -------
+ % t : :class:`Tensor`
+ % output tensor.
+
+ assert(isequal(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);
+ end
+
+ function A = mpower(X, Y)
+ % Raise a square tensor to a scalar power, or a scalar to a square tensor power.
+ %
+ % Usage
+ % -----
+ % A = x^Y
+ %
+ % A = X^y
+ %
+ % Arguments
+ % ---------
+ % X, Y : :class:`Tensor`
+ % Square input tensor.
+ %
+ % x, y : numeric
+ % Input scalars.
+ %
+ % Returns
+ % -------
+ % A : :class:`Tensor`
+ % Output tensor.
+
+ % tensor to a scalar power
+ if isnumeric(Y) && isscalar(Y)
+ assert(isequal(X.codomain, X.domain), 'tensors:ArgumentError', ...
+ 'Input tensor should be square.');
+ mblocks = matrixblocks(X);
+ for i = 1:length(mblocks)
+ mblocks{i} = mblocks{i}^Y;
+ end
+ A = X;
+ A.var = fill_matrix_data(A.var, mblocks);
+ return
+ end
+
+ % scalar to a tensor power
+ if isnumeric(X) && isscalar(X)
+ assert(isequal(Y.codomain, Y.domain), 'tensors:ArgumentError', ...
+ 'Input tensor should be square.');
+ mblocks = matrixblocks(Y);
+ for i = 1:length(mblocks)
+ mblocks{i} = X^mblocks{i};
+ end
+ A = Y;
+ A.var = fill_matrix_data(A.var, mblocks);
+ return
+ end
+
+ error('tensors:ArgumentError', ...
+ 'At least one of the arguments should be scalar.');
+ end
+
+ 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.
+ %
+ % Arguments
+ % ---------
+ % A : :class:`Tensor`
+ % input tensor.
+ %
+ % Returns
+ % -------
+ % X : :class:`Tensor`
+ % principal square root of the input, which has 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.
+
+ assert(isequal(A.codomain, A.domain), 'tensors:ArgumentError', ...
+ 'Input should be square.');
+
+ mblocks = matrixblocks(A);
+ if nargout > 1
+ resnorms = zeros(size(mblocks));
+ for i = 1:length(mblocks)
+ [mblocks{i}, resnorms(i)] = sqrtm(mblocks{i});
+ end
+ resnorm = sum(resnorms);
+ else
+ for i = 1:length(mblocks)
+ mblocks{i} = sqrtm(mblocks{i});
+ end
+ end
+ A.var = fill_matrix_data(A.var, mblocks);
+ end
+ end
+
+
+ %%
+ methods
+ function bool = isposdef(t)
+ % Test if a tensor is a positive-definite map. Generally, a Hermitian matrix M
+ % is positive-definite if the real number z' M z is positive for every 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.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % Returns
+ % -------
+ % bool : logical
+ % true if t is positive definite.
+
+ mblocks = matrixblocks(t);
+ for i = 1:length(mblocks)
+ if ~isposdef(mblocks{i})
+ bool = false;
+ return
+ end
+ end
+ bool = true;
+ end
+
+ function bool = isisometry(t, side, tol)
+ % Test if a tensor is an isometric map. Generally, a matrix M is left or right
+ % isometric if M' * M = I or M * M' = I.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % side : 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.
+ %
+ % Returns
+ % -------
+ % bool : logical
+ % true if t is isometric.
+
+ arguments
+ t
+ side {mustBeMember(side, {'left', 'right', 'both'})} = 'both'
+ tol.RelTol = sqrt(eps(underlyingType(t)))
+ tol.AbsTol = 0
+ end
+
+ mblocks = matrixblocks(t);
+ for i = 1:length(mblocks)
+ if ~isisometry(mblocks{i}, side, 'RelTol', tol.RelTol, 'AbsTol', tol.AbsTol)
+ bool = false;
+ return
+ end
+ end
+ bool = true;
+ end
+
+ function bool = istriu(t)
+ mblocks = matrixblocks(t);
+ for i = 1:length(mblocks)
+ if ~istriu(mblocks{i})
+ bool = false;
+ return
+ end
+ end
+ bool = true;
+ end
+
+ function bool = istril(t)
+ mblocks = matrixblocks(t);
+ for i = 1:length(mblocks)
+ if ~istril(mblocks{i})
+ bool = false;
+ return
+ end
+ end
+ bool = true;
+ end
+
+ function bool = isdiag(t)
+ mblocks = matrixblocks(t);
+ for i = 1:length(mblocks)
+ if ~isdiag(mblocks{i})
+ bool = false;
+ return
+ end
+ end
+ bool = true;
+ end
+
+ function bool = isreal(t)
+ mblocks = matrixblocks(t);
+ for i = 1:length(mblocks)
+ if ~isreal(mblocks{i})
+ bool = false;
+ return
+ end
+ end
+ end
+
+ function r = cond(t, p)
+ % Condition number with respect to inversion. This is defined as
+ % :math:`||t|| * ||t^{-1}||` in the p-norm. For well conditioned
+ % tensors, the value is near 1.0, while for badly conditioned tensors the value
+ % diverges.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % input tensor.
+ %
+ % p : 1, 2, inf or 'fro'
+ % kind of norm to use. Default is the 2-norm.
+ %
+ % Returns
+ % -------
+ % r : :class:`double`
+ % Condition number.
+
+ arguments
+ t
+ p = 2
+ end
+
+ r = norm(t, p) * norm(inv(t), p);
+ end
+ end
+
+
+ %% Copy constructors
+ methods
+ function t = similar(tensors, indices, options)
+
+ arguments (Repeating)
+ tensors
+ indices
+ end
+ arguments
+ options.Rank (1, 2) = [sum(cellfun(@length, indices)) 0]
+ options.Conj = false(size(tensors))
+ end
+
+ for i = 1:length(tensors)
+ inds = find(indices{i} > 0);
+ if any(inds)
+ if options.Conj(i)
+ spaces(indices{i}(inds)) = conj(space(tensors{i}, ...
+ adjointindices(tensors{i}, inds)));
+ else
+ spaces(indices{i}(inds)) = space(tensors{i}, inds);
+ end
+ end
+ end
+
+ t = Tensor(spaces(1:options.Rank(1)), ...
+ spaces((1:options.Rank(2)) + options.Rank(1))');
+ end
+ end
+
+
+ %% 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
+ % -----
+ % [V, D, flag] = eigsolve(A, x0, howmany, sigma, kwargs)
+ % 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.
+ %
+ % Arguments
+ % ---------
+ % t : :class:`Tensor`
+ % 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 : 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 : numeric
+ % real or complex vector containing the parameters of the tensor.
+ %
+ % t : :class:`Tensor`
+ % 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
+ % -------
+ % t : :class:`Tensor`
+ % output tensor, filled with the parameters.
+
+ arguments
+ v
+ t
+ type = 'complex'
+ end
+
+ t.var = devectorize(v, t.var, type);
+ end
+ end
+
+
+ %% Converters
+ methods
+ function a = double(t)
+ % Convert tensor to array of double.
+ %
+ % Arguments
+ % ---------
+ % t : Tensor
+ %
+ % Returns
+ % -------
+ % a : double
+
+ trees = fusiontrees(t);
+ blocks = tensorblocks(t);
+ if isempty(trees)
+ a = blocks{1};
+ return
+ end
+
+ % Initialize output
+ a = zeros(dims(t));
+ s = [t.codomain flip(t.domain)];
+ dimsizes = cell(size(s));
+ for i = 1:length(s)
+ dimsizes{i} = qdim(charges(s(i))) .* degeneracies(s(i));
+ end
+ a_cell = mat2cell(a, dimsizes{:});
+
+ % Locate non-empty blocks
+ [lia, locb] = ismember(trees.uncoupled, ...
+ charges(s).', 'rows');
+ assert(all(lia));
+
+ % Fill output
+ if fusionstyle(trees) == FusionStyle.Unique
+ a_cell(locb) = blocks;
+ else
+ tree_array = fusiontensor(trees);
+ for i = 1:length(locb)
+ tree_double = tree_array{i};
+ tree_size = size(tree_double, 1:nspaces(t));
+ block_size = size(blocks{i}, 1:nspaces(t));
+ assert(isequal(tree_size .* block_size, size(a_cell{locb(i)}, 1:nspaces(t))));
+ a_cell{locb(i)} = a_cell{locb(i)} + ...
+ reshape(contract(tree_double, -(1:2:2*nspaces(t)), ...
+ blocks{i}, -(2:2:2*nspaces(t))), tree_size .* block_size);
+ end
+ end
+
+ a = cell2mat(a_cell);
+ end
+ end
+
+
+ %% Utility
+ methods
+ function s = GetMD5_helper(t)
+ s = {t.codomain t.domain};
+ end
+
+ function type = underlyingType(t)
+ type = underlyingType(t(1).var);
+ end
+
+ function disp(t)
+ if isscalar(t)
+ r = t.rank;
+ fprintf('Rank (%d, %d) Tensor:\n\n', r(1), r(2));
+ s = space(t);
+ for i = 1:length(s)
+ fprintf('%d.\t', i);
+ disp(s(i));
+ fprintf('\n');
+ end
+ else
+ builtin('disp', t);
+ end
+ end
+ end
+end
diff --git a/src/tensors/charges/AbstractCharge.m b/src/tensors/charges/AbstractCharge.m
new file mode 100644
index 0000000..d48320c
--- /dev/null
+++ b/src/tensors/charges/AbstractCharge.m
@@ -0,0 +1,646 @@
+classdef (Abstract) AbstractCharge
+ % AbstractCharge - Abstract base class for objects in a fusion category.
+
+ %% Required categorical data.
+ methods
+ function style = braidingstyle(a)
+ % Trait that describes the braiding style.
+ %
+ % Arguments
+ % --------
+ % a : :class:`.AbstractCharge`
+ % input charge
+ %
+ % Returns
+ % -------
+ % style : :class:`.BraidingStyle`
+ % braiding style of given charge
+ %
+ % See Also
+ % -------
+ % :class:`.BraidingStyle`
+ error('AbstractCharge:requiredMethod', ...
+ 'Error. \nMethod must be overloaded.')
+ end
+
+ function abar = conj(a)
+ % Compute the dual charge.
+ %
+ % Arguments
+ % --------
+ % a : :class:`AbstractCharge`
+ % input charge
+ %
+ % Returns
+ % -------
+ % abar : :class:`AbstractCharge`
+ % conjugate charge suche that :code:`one(a)` is an element of :code:`a * abar`
+ error('AbstractCharge:requiredMethod', ...
+ 'Error. \nMethod must be overloaded.')
+ end
+
+ function style = fusionstyle(a)
+ % Trait that describes the fusion style.
+ %
+ % Arguments
+ % --------
+ % a : :class:`.AbstractCharge`
+ % input charge
+ %
+ % Returns
+ % -------
+ % style : :class:`.FusionStyle`
+ % fusion style of given charge
+ %
+ % See Also
+ % -------
+ % :class:`.FusionStyle`
+ error('AbstractCharge:requiredMethod', ...
+ 'Error. \nMethod must be overloaded.')
+ end
+
+ function c = mtimes(a, b)
+ % Implement the fusion rules.
+ %
+ % Arguments
+ % ---------
+ % a, b : :class:`.AbstractCharge`
+ % charges to be fused
+ %
+ % Returns
+ % -------
+ % c : :class:`.AbstractCharge` (1, \*)
+ % the unique elements in the decomposition of the tensor product of ``a`` and
+ % ``b``
+ error('AbstractCharge:requiredMethod', ...
+ 'Error. \nMethod must be overloaded.')
+ end
+
+ function F = Fsymbol(a, b, c, d, e, f)
+ % Compute the recoupling coefficients.
+ %
+ % Usage
+ % -----
+ % :code:`F = Fsymbol(a, b, c, d, e, f)` computes the isomorphism between the
+ % following fusion diagrams:
+ %
+ % |
+ %
+ % .. image:: ../img/Fmove.svg
+ % :alt: Fmove
+ % :scale: 6 %
+ % :align: center
+ %
+ % |
+ %
+ % Arguments
+ % ---------
+ % a, b, c : :class:`.AbstractCharge`
+ % charges being fused
+ %
+ % d : :class:`.AbstractCharge`
+ % total charges
+ %
+ % e : :class:`.AbstractCharge`
+ % intermediate charge before recoupling
+ %
+ % f : :class:`.AbstractCharge`
+ % intermediate charge after recoupling
+ %
+ % Returns
+ % -------
+ % F : :class:`double` (\*, \*, \*, \*)
+ % recoupling coefficients
+ error('AbstractCharge:requiredMethod', ...
+ 'Error. \nMethod must be overloaded.')
+ end
+
+ function N = Nsymbol(a, b, c)
+ % Compute the fusion multiplicities.
+ %
+ % Arguments
+ % ---------
+ % a, b : :class:`.AbstractCharge`
+ % charges being fused
+ % c : :class:`.AbstractCharge`
+ % resulting charge
+ %
+ % Returns
+ % -------
+ % N : :class:`int`
+ % amount of times c appears in the fusion product of a and b
+ end
+
+ function e = one(a)
+ % Compute the trivial charge.
+ %
+ % Arguments
+ % ---------
+ % a : :class:`.AbstractCharge`
+ % input charge
+ %
+ % Returns
+ % -------
+ % e : :class:`.AbstractCharge`
+ % trivial charge
+ error('AbstractCharge:requiredMethod', ...
+ 'Error. \nMethod must be overloaded.')
+ end
+
+ end
+
+ %% Optional categorical data
+ methods
+ function C = fusiontensor(a, b, c)
+ % Compute a tensor for the fusion vertex.
+ %
+ % Usage
+ % -----
+ % :code:`C = fusiontensor(a, b, c)` a tensor representation for the following
+ % diagram:
+ %
+ % |
+ %
+ % .. image:: ../img/fusiontensor.svg
+ % :alt: fusiontensor
+ % :scale: 6 %
+ % :align: center
+ %
+ % |
+ %
+ % Arguments
+ % ---------
+ % a, b : :class:`.AbstractCharge`
+ % charges being fused
+ % c : :class:`.AbstractCharge`
+ % resulting charge
+ %
+ % Returns
+ % -------
+ % C : :class:`double` (\*, \*, \*, \*)
+ % fusion tensor
+ error('AbstractCharge:optionalMethod', ...
+ 'Error. \nMethod must be overloaded.')
+ end
+
+ function R = Rsymbol(a, b, c, inv)
+ % Compute the braiding coefficients.
+ %
+ % Usage
+ % -----
+ % :code:`R = Rsymbol(a, b, c, inv)` computes the isomorphism between the
+ % following fusion diagrams:
+ %
+ % |
+ %
+ % .. image:: ../img/Rmove.svg
+ % :alt: Rmove
+ % :scale: 6 %
+ % :align: center
+ %
+ % |
+ %
+ % Arguments
+ % ---------
+ % a, b : :class:`.AbstractCharge`
+ % charges being fused
+ % c : :class:`.AbstractCharge`
+ % resulting charge
+ % inv : :class:`logical` , optional
+ % compute inverse braiding coefficient (default false)
+ %
+ % Returns
+ % -------
+ % R : :class:`double` (\*, \*)
+ % braiding coefficients
+ error('AbstractCharge:optionalMethod', ...
+ 'Error. \nMethod must be overloaded.')
+ end
+ end
+
+ %% Categorical data that can be derived.
+ methods
+ function A = Asymbol(a, b, c)
+ % Compute the fusion to splitting coefficient.
+ %
+ % .. todo::
+ % Add diagram?
+ %
+ % Arguments
+ % ---------
+ % a, b : :class:`.AbstractCharge`
+ % charges being fused
+ % c : :class:`.AbstractCharge`
+ % resulting charge
+ %
+ % Returns
+ % -------
+ % A : :class:`double`
+ % fusion to splitting coefficient
+ A = sqrt(qdim(a) .* qdim(b) ./ qdim(c)) .* ...
+ conj(frobeniusschur(a) .* ...
+ Fsymbol(conj(a), a, b, b, repmat(one(a), size(a)), c));
+ end
+
+ function B = Bsymbol(a, b, c)
+ % Compute the splitting to fusion coefficient.
+ %
+ % .. todo::
+ % Add diagram?
+ %
+ % Arguments
+ % ---------
+ % a, b : :class:`.AbstractCharge`
+ % charges being fused
+ % c : :class:`.AbstractCharge`
+ % resulting charge
+ %
+ % Returns
+ % -------
+ % B : :class:`double`
+ % splitting to fusion coefficient
+ B = sqrt(qdim(a) .* qdim(b) ./ qdim(c)) .* ...
+ Fsymbol(a, b, conj(b), a, c, repmat(one(a), size(a)));
+ end
+
+ function B = braidingmatrix(a, b, c, d, e, f, inv)
+ % Compute the matrix for general Artin braids.
+ %
+ % .. todo::
+ % Complete docstring.
+ %
+ if hasmultiplicity(a.fusionstyle)
+ error('Not implemented.');
+ end
+ if inv
+ R1 = conj(Rsymbol(repmat(d, length(c), 1), c, repmat(e, length(c), 1)));
+ R2 = Rsymbol(repmat(d, length(f), 1), repmat(a, length(f), 1), f);
+ else
+ R1 = Rsymbol(c, repmat(d, length(c), 1), repmat(e, length(c), 1));
+ R2 = conj(Rsymbol(repmat(a, length(f), 1), repmat(d, length(f), 1), f));
+ end
+ R1 = reshape(R1, [], 1);
+ R2 = reshape(R2, 1, []);
+
+ F = Fmatrix(d, a, b, e, f, c);
+ B = R1 .* F .* R2;
+ end
+
+ function F = flipper(a)
+ % Create a matrix-representation of an arrowflip.
+ %
+ % .. todo::
+ % Add diagram or definition?
+ %
+ % Arguments
+ % ---------
+ % a : :class:`.AbstractCharge`
+ %
+ % Returns
+ % -------
+ % F : :class:`double` (\*, \*)
+ % matrix-representation of an arrowflip
+ F = conj(sqrt(qdim(a)) .* fusiontensor(conj(a), a, one(a)));
+ end
+
+ function F = Fmatrix(a, b, c, d, e, f)
+ % Compute the full recoupling matrix from ``e`` to ``f``.
+ %
+ % .. todo::
+ % Add proper definition?
+ %
+ % Usage
+ % -----
+ % :code:`F = Fmatrix(a, b, c, d, e, f)` computes the matrix between all allowed
+ % channels.
+ %
+ % Arguments
+ % ---------
+ % a, b, c : :class:`.AbstractCharge`
+ % charges being fused
+ % d : :class:`.AbstractCharge`
+ % total charges
+ % e : :class:`.AbstractCharge` (1, \*)
+ % intermediate charges before recoupling
+ % f : :class:`.AbstractCharge` (1, \*)
+ % intermediate charge after recoupling
+ %
+ % Returns
+ % -------
+ % F : :class:`double` (\*, \*, \*, \*)
+ % recoupling matrix between all allowed channels
+ 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, e(i), f(j));
+ Fblocks{j, i} = reshape(Fblocks{j, i}, ...
+ size(Fblocks{j, i}, 1) * size(Fblocks{j, i}, 2), ...
+ size(Fblocks{j, i}, 3) * size(Fblocks{j, i}, 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, e(i), f(j));
+ end
+ end
+ end
+
+ function nu = frobeniusschur(a)
+ % Compute the Frobenius-Schur indicator.
+ %
+ % Arguments
+ % ---------
+ % a : :class:`.AbstractCharge`
+ %
+ % Returns
+ % -------
+ % nu : :class:`double`
+ % Frobenius-Schur indicator of ``a``
+ nu = sign(Fsymbol(a, conj(a), a, a, one(a), one(a)));
+ end
+
+ function d = qdim(a)
+ % Compute the quantum dimension of a charge.
+ %
+ % Arguments
+ % ---------
+ % a : :class:`.AbstractCharge`
+ %
+ % Returns
+ % -------
+ % d : :class:`double`
+ % quantum dimension of ``a``
+ if fusionstyle(a) == FusionStyle.Unique
+ d = ones(size(a));
+ else
+ F = Fsymbol(a, conj(a), a, a, one(a), one(a));
+ d = abs(1 / F(1));
+ end
+ end
+
+ function theta = twist(a)
+ % Compute the coefficient obtained by twisting a charge.
+ %
+ % .. todo::
+ % Add diagram/definition?
+ %
+ % Arguments
+ % ---------
+ % a : :class:`.AbstractCharge`
+ %
+ % Returns
+ % -------
+ % theta : :class:`double`
+ % twist coefficient of ``a``
+ if braidingstyle(a) == BraidingStyle.Bosonic
+ theta = ones(size(a));
+ return
+ end
+ if numel(a) > 1, theta = arrayfun(@twist, a); return; end
+
+ theta = 0;
+ for b = a * a
+ theta = theta + qdim(b) / qdim(a) * trace(Rsymbol(a, a, b));
+ end
+ end
+ end
+
+ %% Utility functions
+ methods
+ function [d, N] = prod(a, dim)
+ % Total fusion product of charges.
+ %
+ % .. todo::
+ % Complete docstring
+ %
+ % Arguments
+ % ---------
+ % a, b, c, ... : :class:`.AbstractCharge`
+ %
+ % Returns
+ % -------
+ % c : :class:`.AbstractCharge`
+ % total fusion product determined by subsequently multiplying input charges
+ arguments
+ a
+ dim = find(size(a) ~= 1, 1)
+ end
+
+ switch fusionstyle(a)
+ case FusionStyle.Unique
+ if dim == 1
+ d = a(1, :);
+ for i = 2:size(a, 1)
+ d = d * a(i, :);
+ end
+ else
+ d = a(:, 1);
+ for i = 2:size(a, 2)
+ d = d * a(:, i);
+ end
+ end
+ if nargout > 1
+ N = ones(size(d));
+ end
+
+ case FusionStyle.Simple
+ d = a(1);
+ N = 1;
+ for i = 2:length(a)
+ d_ = d(1) * a(i);
+ if nargout > 1
+ N_(1:length(d_)) = N(1);
+ 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);
+ 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');
+ end
+ end
+
+ function [charges, vertices] = cumprod(a)
+ % Cumulative fusion product of elements.
+ %
+ % .. todo::
+ % Complete docstring
+ %
+ % Usage
+ % -----
+ % :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
+ % an arbitrarily-sized matrix.
+ % For other fusion styles, ``X`` should be a vector, and ``Y`` is a matrix
+ % containing all different combinations.
+ %
+ % Arguments
+ % ---------
+ % a: :class:`.AbstractCharge` (\*, \*)
+ %
+ % Returns
+ % -------
+ % charges : :class:`.AbstractCharge`
+ % explanation
+ % vertices : type
+ % explanation
+ if a.fusionstyle == FusionStyle.Unique
+ charges = a;
+ for i = 2:size(charges, 1)
+% charges(i, :) = charges(i-1, :) * charges(i, :);
+ charges = subsasgn(charges, substruct('()', {i, ':'}), ...
+ subsref(charges, substruct('()', {i-1, ':'})) * ...
+ subsref(charges, substruct('()', {i, ':'})));
+ end
+ vertices = [];
+ return
+ end
+
+ % TODO avoid loop allocations, ideas: vertcat cell + unique?
+ % Not so simple for non-unique fusion style.
+ if size(a, 2) > 1
+ charges = [];
+ vertices = [];
+ for i = 1:size(a, 2)
+ [chargepart, vertexpart] = cumprod(a(:, i));
+ charges = horzcat(charges, chargepart);
+ vertices = horzcat(vertices, vertexpart);
+ end
+ return
+ end
+
+ if a.fusionstyle == FusionStyle.Simple
+ if length(a) == 1
+ charges = a;
+ vertices = [];
+ return
+ end
+
+ if length(a) == 2
+ f = a(1) * a(2);
+ charges = [repmat(a(1), 1, length(f)); f];
+ vertices = [];
+ return
+ end
+
+ part = cumprod(a(1:end-1));
+ charges = [];
+ for i = 1:size(part, 2)
+ f = part(end, i) * a(end);
+ charges = [charges [repmat(part(:, i), 1, length(f)); f]];
+ end
+ vertices = [];
+ return
+ end
+
+ if length(a) == 1
+ charges = a;
+ vertices = [];
+ return
+ end
+
+ if length(a) == 2
+ charges = [];
+ vertices = [];
+ for f = a(1) * a(2)
+ N = Nsymbol(a(1), a(2), f);
+ charges = vertcat(charges, ...
+ repmat([a(1) f], N, 1));
+ vertices = vertcat(vertices, (1:N)');
+ end
+ return
+ end
+
+ [chargepart, vertexpart] = cumprod(a(1:end-1));
+ charges = [];
+ vertices = [];
+ for i = 1:size(chargepart, 1)
+ for f = chargepart(i, end) * a(end)
+ N = Nsymbol(chargepart(i, end), a(end), f);
+ charges = vertcat(charges, ...
+ repmat([chargepart(i, :) f], N, 1));
+ vertices = vertcat(vertices, ...
+ [repmat(vertexpart(i,:), N, 1) (1:N)']);
+ end
+ end
+ end
+
+ function Y = combvec(X)
+ % Create all combinations of vectors.
+ %
+ % .. todo::
+ % Complete docstring
+ %
+ % Usage
+ % -----
+ % :code:`Y = combvec(X1, X2, ...)` takes any number of inputs ``X``, where
+ % each ``Xi`` has ``Ni`` columns, and returns a matrix of ``prod(N)`` column
+ % vectors, where the columns consist of all combinations found by
+ % combining one column vector from each ``Xi``.
+ %
+ % Arguments
+ % ---------
+ % X: :class:`.AbstractCharge`, repeating
+ % input charges
+ %
+ % Returns
+ % -------
+ % Y : :class:`.AbstractCharge`
+ % explanation
+ %
+ % See Also
+ % --------
+ % :func:`combvec`
+ arguments (Repeating)
+ X
+ end
+
+ Y = X{1};
+ for i = 2:length(X)
+ Y = [repmat(Y, 1, size(X{i}, 2))
+ 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)
+ end
+end
diff --git a/src/tensors/charges/BraidingStyle.m b/src/tensors/charges/BraidingStyle.m
new file mode 100644
index 0000000..0efbe90
--- /dev/null
+++ b/src/tensors/charges/BraidingStyle.m
@@ -0,0 +1,43 @@
+classdef BraidingStyle
+ % BraidingStyle - The braiding behaviour of charges.
+ % This represents the possibilities for the braiding of two 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
+ Abelian
+ Bosonic
+ Fermionic
+ Anyonic
+ None
+ end
+
+ methods
+ function bool = issymmetric(style)
+ bool = style == BraidingStyle.Abelian || style == BraidingStyle.Bosonic;
+ end
+
+ function c = and(a, b)
+ if any([a b] == BraidingStyle.None)
+ c = BraidingStyle.None;
+
+ elseif any([a b] == BraidingStyle.Anyonic)
+ c = BraidingStyle.Anyonic;
+
+ elseif any([a b] == BraidingStyle.Fermionic)
+ c = BraidingStyle.Fermionic;
+
+ elseif any([a b] == BraidingStyle.Bosonic)
+ c = BraidingStyle.Bosonic;
+
+ else
+ c = BraidingStyle.Abelian;
+
+ end
+ end
+ end
+end
\ No newline at end of file
diff --git a/src/tensors/charges/FusionStyle.m b/src/tensors/charges/FusionStyle.m
new file mode 100644
index 0000000..d4cc419
--- /dev/null
+++ b/src/tensors/charges/FusionStyle.m
@@ -0,0 +1,37 @@
+classdef FusionStyle
+ % FusionStyle - The fusion product behaviour of charges.
+ % This represents the possibilities for the decomposition of the fusion product of two charges.
+ %
+ % Unique - Single unique output.
+ % Simple - Multiple unique outputs.
+ % Generic - Multiple outputs.
+
+ enumeration
+ Unique
+ Simple
+ Generic
+ end
+
+ methods
+ function bool = hasmultiplicity(style)
+ bool = style == FusionStyle.Generic;
+ end
+
+ function style = and(style1, style2)
+ % Determine the fusionstyle for a direct product of charges. This effectively
+ % boils down to returning the least specific style.
+
+ if style1 == FusionStyle.Generic || style2 == FusionStyle.Generic
+ style = FusionStyle.Generic;
+ return
+ end
+
+ if style1 == FusionStyle.Simple || style2 == FusionStyle.Simple
+ style = FusionStyle.Simple;
+ return
+ end
+
+ style = FusionStyle.Unique;
+ end
+ end
+end
\ No newline at end of file
diff --git a/src/tensors/charges/O2.m b/src/tensors/charges/O2.m
new file mode 100644
index 0000000..c45ec88
--- /dev/null
+++ b/src/tensors/charges/O2.m
@@ -0,0 +1,301 @@
+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.
+ %
+ % 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).
+
+ properties
+ j uint8 % (uint8) U1 label.
+ s uint8 % (uint8) indicator for type of representation.
+ end
+
+ methods
+ function style = braidingstyle(~)
+ style = BraidingStyle.Bosonic;
+ end
+
+ function a = conj(a), end
+
+ function bool = eq(a, b)
+ bool = reshape([a.j], size(a)) == reshape([b.j], size(b)) & ...
+ reshape([a.s], size(a)) == reshape([b.s], size(b));
+ end
+
+ function nu = frobeniusschur(a)
+ nu = ones(size(a));
+ end
+
+ function style = fusionstyle(~)
+ style = FusionStyle.Simple;
+ end
+
+ function F = Fsymbol(a, b, c, d, e, f)
+ if numel(a) > 1
+ F = reshape(arrayfun(@Fsymbol, a, b, c, d, e, f), size(a));
+ return
+ end
+
+ if ~(Nsymbol(a, b, e) && Nsymbol(e, c, d) && ...
+ Nsymbol(b, c, f) && Nsymbol(a, f, d))
+ F = 0;
+ return
+ end
+
+ % Cases where a, b, c = 0+ or 0-
+ if (a.j == 0 && a.s == 0) || (b.j == 0 && b.s == 0) || (c.j == 0 && c.s == 0)
+ F = 1;
+ return
+ end
+
+ if ((a.j == 0 && a.s == 1) && (b.j == 0 && b.s == 1)) || ...
+ ((a.j == 0 && a.s == 1) && (c.j == 0 && c.s == 1)) || ...
+ ((b.j == 0 && b.s == 1) && (c.j == 0 && c.s == 1))
+ F = 1;
+ return
+ end
+
+ if a.j == 0 && a.s == 1
+ if d.j == 0
+ F = 1;
+ elseif d.j == c.j - b.j
+ F = -1;
+ else
+ F = 1;
+ end
+ return
+ end
+
+ if b.j == 0 && b.s == 1
+ if d.j == max(a.j - c.j, c.j - a.j)
+ F = -1;
+ else
+ F = 1;
+ end
+ return
+ end
+
+ if c.j == 0 && c.s == 1
+ if d.j == a.j - b.j
+ F = -1;
+ else
+ F = 1;
+ end
+ return
+ end
+
+ % Other cases
+ isqrt2 = sqrt(2) / 2;
+ if a == b && b == c
+ if d == a
+ if e.j == 0
+ if f.j == 0
+ if f.s == 1
+ F = -0.5;
+ else
+ F = 0.5;
+ end
+ else
+ if e.s == 1
+ F = -isqrt2;
+ else
+ F = isqrt2;
+ end
+ end
+ else
+ if f.j == 0
+ F = isqrt2;
+ else
+ F = 0;
+ end
+ end
+ else
+ F = 1;
+ end
+ return
+ end
+
+ if a == b
+ if d == c
+ if f.j == b.j + c.j
+ if e.s == 1
+ F = -isqrt2;
+ else
+ F = isqrt2;
+ end
+ else
+ F = isqrt2;
+ end
+ else
+ F = 1;
+ end
+ return
+ end
+
+ if b == c
+ if d == a
+ if e.j == a.j + b.j
+ F = isqrt2;
+ else
+ if f.s == 1
+ F = -isqrt2;
+ else
+ F = isqrt2;
+ end
+ end
+ else
+ F = 1;
+ end
+ return
+ end
+
+ if a == c
+ if d == b
+ if e.j == f.j
+ F = 0;
+ else
+ F = 1;
+ end
+ else
+ if d.s == 1
+ F = -1;
+ else
+ F = 1;
+ end
+ end
+ return
+ end
+
+ if d.j == 0 && d.s == 1
+ if b.j == a.j + c.j
+ F = -1;
+ else
+ F = 1;
+ end
+ return
+ end
+ F = 1;
+ end
+
+ function C = fusiontensor(a, b, c)
+ C = zeros(qdim(a), qdim(b), qdim(c), 1);
+ if ~Nsymbol(a, b, c), return; end
+ if c.j == 0
+ if a.j == 0 && b.j == 0
+ C(1, 1, 1, 1) = 1;
+ else
+ if c.s == 0
+ C(1, 2, 1, 1) = 1 / sqrt(2);
+ C(2, 1, 1, 1) = 1 / sqrt(2);
+ else
+ C(1, 2, 1, 1) = 1 / sqrt(2);
+ C(2, 1, 1, 1) = -1 / sqrt(2);
+ end
+ end
+ elseif a.j == 0
+ C(1, 1, 1, 1) = 1;
+ if a.s == 1
+ C(1, 2, 2, 1) = -1;
+ else
+ C(1, 2, 2, 1) = 1;
+ end
+ elseif b.j == 0
+ C(1, 1, 1, 1) = 1;
+ if b.s == 1
+ C(2, 1, 2, 1) = -1;
+ else
+ C(2, 1, 2, 1) = 1;
+ end
+ elseif c.j == a.j + b.j
+ C(1, 1, 1, 1) = 1;
+ C(2, 2, 2, 1) = 1;
+ elseif c.j == a.j - b.j
+ C(1, 2, 1, 1) = 1;
+ C(2, 1, 2, 1) = 1;
+ elseif c.j == b.j - a.j
+ C(2, 1, 1, 1) = 1;
+ C(1, 2, 2, 1) = 1;
+ end
+ end
+
+ function bool = issortedrows(A)
+ bool = issortedrows(reshape([A.j] + [A.s], size(A)));
+ end
+
+ function c = mtimes(a, b)
+ assert(isscalar(a) && isscalar(b), ...
+ 'Simple fusion cannot be vectorised.');
+ if a.j == 0 && b.j == 0
+ c = O2(0, xor(a.s, b.s));
+ elseif a.j == 0
+ c = b;
+ elseif b.j == 0
+ c = a;
+ elseif a.j == b.j
+ c = O2([0 0 a.j + b.j], [0 1 2]);
+ else
+ c = O2([a.j + b.j, max(a.j - b.j, b.j - a.j)], [2 2]);
+ end
+ end
+
+ function bool = ne(a, b)
+ bool = reshape([a.j], size(a)) ~= reshape([b.j], size(b)) | ...
+ reshape([a.s], size(a)) ~= reshape([b.s], size(b));
+ end
+
+ function N = Nsymbol(a, b, c)
+ N = double(reshape(...
+ ([c.s] == 0 & [a.j] == [b.j] & [a.s] == [b.s]) | ...
+ ([c.s] == 1 & [a.j] == [b.j] & ([a.s] ~= [b.s] | [a.s] == 2)) | ...
+ ([c.s] == 2 & ([c.j] == ([a.j] + [b.j]) | ...
+ ([c.j] == max([a.j] - [b.j], [b.j] - [a.j])))), ...
+ size(a)));
+ end
+
+ function charges = O2(j, s)
+ if nargin == 0, return; end
+ assert(all(size(j) == size(s)));
+ if ~true
+ for i = numel(j):-1:1
+ charges(i).j = j(i);
+ charges(i).s = s(i);
+ end
+ else
+ j_cell = num2cell(j);
+ s_cell = num2cell(s);
+ charges = repmat(O2, size(j));
+ [charges.j] = deal(j_cell{:});
+ [charges.s] = deal(s_cell{:});
+ end
+ charges = reshape(charges, size(j));
+ end
+
+ function d = qdim(a)
+ d = ones(size(a));
+ d([a.j] ~= 0) = 2;
+ end
+
+ function R = Rsymbol(a, b, c)
+ R = Nsymbol(a, b, c);
+ R([c.s] == 1 & [a.j] > 0) = -R([c.s] == 1 & [a.j] > 0);
+ end
+
+ function [B, I] = sort(A, varargin)
+ [~, I] = sort(reshape([A.j] + [A.s], size(A)), varargin{:});
+ B = A(I);
+ end
+
+ function str = string(a)
+ str = reshape(string([a.j]), size(a));
+ str([a.s] == 0) = str([a.s] == 0) + "+";
+ str([a.s] == 1) = str([a.s] == 1) + "-";
+ end
+
+ function e = one(~)
+ e = O2(0, 0);
+ end
+ end
+end
diff --git a/src/tensors/charges/ProductCharge.m b/src/tensors/charges/ProductCharge.m
new file mode 100644
index 0000000..1afd14a
--- /dev/null
+++ b/src/tensors/charges/ProductCharge.m
@@ -0,0 +1,385 @@
+classdef ProductCharge < AbstractCharge
+% Irreducible representations of a direct product of groups.
+
+ properties
+ charges (1,:) = {}
+ end
+
+ methods
+ function prodcharge = ProductCharge(charges)
+ % Construct a product group charge.
+ %
+ % Usage
+ % -----
+ % charges = ProductCharge(charges1, charges2, ...)
+ % creates an (array of) charges that are representations of the direct product
+ % group group1 x group2 x ...
+ %
+ % Arguments
+ % ---------
+ % charges1, charges2, ... : AbstractCharge
+ % charges of the separate groups.
+
+ arguments (Repeating)
+ charges
+ end
+
+ if nargin == 0
+ return
+ end
+
+ for i = 2:nargin
+ assert(all(size(charges{1}) == size(charges{i})), ...
+ 'Charges should have equal size.');
+ end
+ prodcharge.charges = charges;
+ end
+
+ function a = cat(dim, a, varargin)
+ % Concatenate charges.
+
+ 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
+ end
+ end
+
+ function a = horzcat(a, varargin)
+ a = cat(2, a, varargin{:});
+ end
+
+ function a = vertcat(a, varargin)
+ a = cat(1, a, varargin{:});
+ end
+
+ function varargout = size(prodcharge, varargin)
+ [varargout{1:nargout}] = size(prodcharge.charges{1}, varargin{:});
+ end
+
+ function a = reshape(a, varargin)
+ for i = 1:numel(a.charges)
+ a.charges{i} = reshape(a.charges{i}, varargin{:});
+ end
+ end
+
+ function n = ndims(a)
+ n = ndims(a.charges{1});
+ end
+
+ function n = numel(a)
+ n = prod(size(a));
+ end
+
+ function bool = isempty(a)
+ bool = isempty(a.charges) || isempty(a.charges{1});
+ end
+
+ function l = length(a)
+ if isempty(a.charges)
+ l = 0;
+ return
+ end
+ l = length(a.charges{1});
+ end
+
+ function [d, N] = prod(a, dim)
+ % Total fusion product of charges.
+
+ arguments
+ a
+ dim = find(size(a) ~= 1, 1);
+ end
+
+ switch fusionstyle(a)
+ case FusionStyle.Unique
+ fusedcharges = cell(size(a.charges));
+ for i = 1:length(fusedcharges)
+ fusedcharges{i} = prod(a.charges{i}, dim);
+ end
+ d = ProductCharge(fusedcharges{:});
+
+ if nargout > 1
+ N = ones(size(d));
+ end
+
+ otherwise
+ if nargout > 1
+ [d, N] = prod@AbstractCharge(a, dim);
+ else
+ d = prod@AbstractCharge(a, dim);
+ end
+ end
+ end
+
+
+ function varargout = subsref(prodcharge, s)
+ % Overload indexing.
+ %
+ % Usage
+ % -----
+ % charges_slice = charges(i1, i2, ...)
+ % extracts elements out of the charge array.
+ %
+ % product_slice = charges{i}
+ % separate out the direct product factors.
+ %
+ % Arguments
+ % ---------
+ % charges : ProductCharge
+ % array of charges.
+ %
+ % s : substruct
+ % structure containing indexing data.
+ %
+ % Returns
+ % -------
+ % charges_slice : ProductCharge
+ % sliced array of product charges.
+ %
+ % product_slice : AbstractCharge
+ % array of factor charges.
+
+ switch s(1).type
+ case '.'
+ [varargout{1:nargout}] = builtin('subsref', prodcharge, s);
+
+ case '()'
+ for i = 1:numel(prodcharge.charges)
+ prodcharge.charges{i} = prodcharge.charges{i}(s(1).subs{:});
+ end
+ if length(s) == 1
+ varargout = {prodcharge};
+ return
+ end
+
+ [varargout{1:nargout}] = subsref(prodcharge, 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{:});
+
+ otherwise
+ error('Undefined behaviour');
+ end
+ end
+
+ function a = subsasgn(a, s, b)
+ % Overload indexed assignment.
+ %
+ % 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.
+ %
+ % Arguments
+ % ---------
+ % a : ProductCharge
+ % array of charges to assign to.
+ %
+ % s : substruct
+ % structure containing indexing data.
+ %
+ % b : ProductCharge
+ % slice to assign.
+ %
+ % c : AbstractCharge
+ % factor to assign.
+ %
+ % Returns
+ % -------
+ % a : ProductCharge
+ % assigned array
+
+ switch s(1).type
+ case '()'
+ assert(length(s) == 1, 'Undefined assignment syntax.');
+ for i = 1:length(a.charges)
+ if isempty(b)
+ a.charges{i}(s(1).subs{:}) = [];
+ else
+ a.charges{i}(s(1).subs{:}) = b.charges{i};
+ end
+ end
+
+ case '{}'
+ if length(s) == 1
+ a.charges{s(1).subs{:}} = b;
+ else
+ a.charges{s(1).subs{:}} = subsasgn(a.charges{s(1).subs{:}}, ...
+ s(2:end), b);
+ end
+
+ case '.'
+ a = builtin('subsasgn', a, s, b);
+ otherwise
+ error('Undefined behaviour.');
+ end
+ end
+
+ function ind = end(a, k, n)
+ ind = builtin('end', a.charges{1}, k, n);
+ end
+
+ function a = transpose(a)
+ if isempty(a), return; end
+ for i = 1:numel(a.charges)
+ a.charges{i} = a.charges{i}.';
+ end
+ end
+
+ function a = ctranspose(a)
+ if isempty(a), return; end
+ for i = 1:numel(a.charges)
+ a.charges{i} = a.charges{i}.';
+ end
+ end
+
+ function style = braidingstyle(prodcharge)
+ style = braidingstyle(prodcharge.charges{1});
+ for i = 2:length(prodcharge.charges)
+ style = style & braidingstyle(prodcharge.charges{i});
+ end
+ end
+
+ function a = conj(a)
+ for i = 1:length(a.charges)
+ a.charges{i} = conj(a.charges{i});
+ end
+ end
+
+ function bool = eq(a, b)
+ assert(length(a.charges) == length(b.charges));
+ bool = eq(a.charges{1}, b.charges{1});
+ for i = 2:length(a.charges)
+ bool = bool & eq(a.charges{i}, b.charges{i});
+ end
+ end
+
+ function nu = frobeniusschur(a)
+ nu = frobeniusschur(a.charges{1});
+ for i = 2:length(a.charges)
+ nu = nu .* frobeniusschur(a.charges{i});
+ end
+ end
+
+ function style = fusionstyle(a)
+ style = fusionstyle(a.charges{1});
+ for i = 2:length(a.charges)
+ style = style & fusionstyle(a.charges{i});
+ end
+ end
+
+ function C = fusiontensor(a, b, c)
+ C = fusiontensor(a.charges{1}, b.charges{1}, c.charges{1});
+ for i = 2:length(a.charges)
+ sz = size(C, 1:4);
+ C_ = fusiontensor(a.charges{i}, b.charges{i}, c.charges{i});
+ sz_ = size(C_, 1:4);
+ C = reshape(contract(C, -(1:2:8), C_, -(2:2:8)), sz .* sz_);
+ end
+ end
+
+ function F = Fsymbol(a, b, c, d, e, f)
+ if hasmultiplicity(fusionstyle(a))
+ error('Not implemented yet.');
+ end
+
+ F = Fsymbol(a.charges{1}, b.charges{1}, c.charges{1}, ...
+ d.charges{1}, e.charges{1}, f.charges{1});
+ for i = 2:length(a.charges)
+ F = F .* Fsymbol(a.charges{i}, b.charges{i}, c.charges{i}, ...
+ d.charges{i}, e.charges{i}, f.charges{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)
+ N = N .* Nsymbol(a.charges{i}, b.charges{i}, c.charges{i});
+ end
+ end
+
+ function c = mtimes(a, b)
+ if fusionstyle(a) == FusionStyle.Unique
+ charges = cell(size(a.charges));
+ for i = 1:numel(charges)
+ charges{i} = mtimes(a.charges{i}, b.charges{i});
+ end
+ c = ProductCharge(charges{:});
+ return
+ end
+
+ assert(isscalar(a) && isscalar(b))
+ charges = cell(size(a.charges));
+ charges{1} = a.charges{1} * b.charges{1};
+ for i = 1:length(charges)
+ charges{i} = a.charges{i} * b.charges{i};
+ 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
+ end
+
+ function bool = ne(a, b)
+ bool = a.charges{1} ~= b.charges{1};
+ for i = 2:length(a.charges)
+ bool = bool | a.charges{i} ~= b.charges{i};
+ end
+ end
+
+ function R = Rsymbol(a, b, c, inv)
+ if nargin < 4, inv = []; end
+ if hasmultiplicity(fusionstyle(a))
+ error('TBA');
+ end
+
+ R = Rsymbol(a.charges{1}, b.charges{1}, c.charges{1}, inv);
+ for i = 2:length(a.charges)
+ R = R .* Rsymbol(a.charges{i}, b.charges{i}, c.charges{i}, inv);
+ end
+ end
+
+ function [a, I] = sort(a, varargin)
+ [I, a.charges{:}] = simulsort(a.charges{:}, varargin{:});
+ end
+
+ function [a, I] = sortrows(a, col, direction)
+ arguments
+ a
+ col = 1:size(a, 2)
+ direction = 'ascend'
+ end
+ [I, a.charges{:}] = simulsortrows(a.charges{:}, ...
+ 'Col', col, '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);
+ end
+
+ function a = one(a)
+ for i = 1:length(a.charges)
+ a.charges{i} = one(a.charges{i});
+ end
+ end
+ end
+end
+
diff --git a/src/tensors/charges/SU2.m b/src/tensors/charges/SU2.m
new file mode 100644
index 0000000..33136f2
--- /dev/null
+++ b/src/tensors/charges/SU2.m
@@ -0,0 +1,133 @@
+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.
+ %
+ % See also AbstractCharge
+
+ methods
+ function style = braidingstyle(~)
+ style = BraidingStyle.Bosonic;
+ end
+
+ function B = Bsymbol(a, b, c)
+ B = sqrt(qdim(a) .* qdim(b)) .* ...
+ (-1).^(2 * (spin(a) + spin(b))) .* ...
+ Wigner6j(spin(a), spin(b), spin(c), spin(b), spin(a), zeros(size(a)));
+ end
+
+ function a = conj(a)
+ end
+
+ function [charges, vertices] = cumprod(a)
+ [charges, vertices] = cumprod@AbstractCharge(a);
+ end
+
+ function nu = frobeniusschur(a)
+ nu = ones(size(a));
+ nu(~mod(a, 2)) = -1;
+ end
+
+ function F = Fsymbol(a, b, c, d, e, f)
+
+ sa = spin(a);
+ sb = spin(b);
+ sc = spin(c);
+ sd = spin(d);
+ se = spin(e);
+ sf = spin(f);
+
+ F = sqrt(qdim(e)) .* sqrt(qdim(f)) .* ...
+ (-1).^(sa + sb + sc + sd) .* ...
+ Wigner6j(sa, sb, se, sc, sd, sf);
+ end
+
+ function F = Fmatrix(a, b, c, d, e, f)
+ if nargin < 5
+ e = intersect(a * b, conj(c * conj(d)));
+ end
+ if nargin < 6
+ f = intersect(b * c, conj(conj(d) * a));
+ end
+ nf = length(f);
+ ne = length(e);
+
+ a = repmat(a, nf, ne);
+ b = repmat(b, nf, ne);
+ c = repmat(c, nf, ne);
+ d = repmat(d, nf, ne);
+ e = repmat(reshape(e, 1, ne), nf, 1);
+ f = repmat(reshape(f, nf, 1), 1, ne);
+
+ F = Fsymbol(a, b, c, d, e, f);
+ end
+
+ function style = fusionstyle(~)
+ style = FusionStyle.Simple;
+ end
+
+ function C = fusiontensor(a, b, c)
+ ma = repmat(reshape(spin(a):-1:-spin(a), qdim(a), 1, 1), [1 qdim(b) qdim(c)]);
+ mb = repmat(reshape(spin(b):-1:-spin(b), 1, qdim(b), 1), [qdim(a) 1 qdim(c)]);
+ mc = repmat(reshape(spin(c):-1:-spin(c), 1, 1, qdim(c)), [qdim(a) qdim(b) 1]);
+
+ ja = repmat(spin(a), [qdim(a) qdim(b) qdim(c)]);
+ jb = repmat(spin(b), size(ja));
+ jc = repmat(spin(c), size(ja));
+
+ C = Wigner3j(ja, jb, jc, ma, mb, mc, 4, true);
+ end
+
+ function c = intersect(a, b)
+ c = a(mod(find(a(:) == b(:).') - 1, length(a)) + 1);
+ end
+
+ function c = mtimes(a, b)
+ c = SU2((double(max(a - b, b - a)):2:double(a + b - 2)) + 1);
+ end
+
+ function N = Nsymbol(a, b, c)
+ N = a <= b + c & b <= a + c & c <= a + b & mod(a + b + c, 2) == 1;
+ end
+
+ function e = one(~)
+ e = SU2(ones(1, 1, 'uint8'));
+ end
+
+ function varargout = prod(varargin)
+ [varargout{1:nargout}] = prod@AbstractCharge(varargin{:});
+ if nargout > 0
+ varargout{1} = SU2(varargout{1});
+ end
+ end
+
+ function d = qdim(a)
+ d = double(a);
+ end
+
+ function R = Rsymbol(a, b, c, ~)
+ R = (-Nsymbol(a, b, c)).^mod(spin(a) + spin(b) - spin(c), 2);
+ end
+
+ function s = string(a)
+ s = strings(size(a));
+ halfints = logical(mod(a-1, 2));
+ s(halfints) = arrayfun(@(x) sprintf("%d/2", x-1), a(halfints));
+ s(~halfints) = arrayfun(@(x) sprintf("%d", (x-1)/2), a(~halfints));
+ end
+
+ function s = spin(a)
+ s = (double(a) - 1) ./ 2;
+ end
+
+ function charge = SU2(varargin)
+ if nargin == 0
+ labels = [];
+ else
+ labels = horzcat(varargin{:});
+ end
+ charge@uint8(labels);
+ end
+ end
+end
diff --git a/src/tensors/charges/U1.m b/src/tensors/charges/U1.m
new file mode 100644
index 0000000..b8b77dc
--- /dev/null
+++ b/src/tensors/charges/U1.m
@@ -0,0 +1,94 @@
+classdef U1 < AbstractCharge & int16
+ % U1 - Irreducible representations of U(1).
+ % This class represents the representations of U(1), labeled using integers.
+ %
+ % See also AbstractCharge
+
+ methods
+ function charge = U1(varargin)
+ if nargin == 0
+ labels = [];
+ else
+ labels = horzcat(varargin{:});
+ end
+ charge@int16(labels);
+ end
+
+ function A = Asymbol(a, b, c)
+ A = double(Nsymbol(a, b, c));
+ end
+
+ function style = braidingstyle(~)
+ style = BraidingStyle.Abelian;
+ end
+
+ function B = Bsymbol(a, b, c)
+ B = double(Nsymbol(a, b, c));
+ end
+
+ function a = conj(a)
+ a = U1(-a);
+ end
+
+ function [charges, vertices] = cumprod(a)
+ charges = U1(cumsum(a));
+ vertices = [];
+ end
+
+ function nu = frobeniusschur(a)
+ nu = ones(size(a));
+ end
+
+ function F = Fsymbol(a, b, c, d, e, f)
+ F = double(Nsymbol(a, b, e) .* Nsymbol(b, c, f) .* ...
+ Nsymbol(e, c, d) .* Nsymbol(a, f, d));
+ end
+
+
+ function style = fusionstyle(~)
+ style = FusionStyle.Unique;
+ end
+
+ function C = fusiontensor(a, b, c)
+ C = double(Nsymbol(a, b, c));
+ end
+
+ function c = mtimes(a, b)
+ c = U1(a + b);
+ end
+
+ function N = Nsymbol(a, b, c)
+ N = a + b == c;
+ end
+
+ function e = one(~)
+ e = U1(zeros(1, 1, 'int16'));
+ end
+
+ function [d, N] = prod(a, dim)
+ if nargin < 2 || isempty(dim)
+ dim = find(size(a) ~= 1, 1);
+ end
+ d = U1(sum(a, dim));
+ if nargout > 1
+ N = ones(size(d));
+ end
+ end
+
+ function R = Rsymbol(a, b, c, ~)
+ R = double(Nsymbol(a, b, c));
+ end
+
+ function s = string(a)
+ s = arrayfun(@(x) sprintf("%+d", x), a);
+ end
+
+ function y = typecast(x, datatype)
+ y = typecast(int16(x), datatype);
+ end
+
+ function s = GetMD5_helper(data)
+ s = int16(data);
+ end
+ end
+end
\ No newline at end of file
diff --git a/src/tensors/charges/Z1.m b/src/tensors/charges/Z1.m
new file mode 100644
index 0000000..ecdd847
--- /dev/null
+++ b/src/tensors/charges/Z1.m
@@ -0,0 +1,88 @@
+classdef Z1 < AbstractCharge
+ % Trivial charges.
+
+ methods
+ function A = Asymbol(~, ~, ~)
+ A = 1;
+ end
+
+ function B = Bsymbol(~, ~, ~)
+ B = 1;
+ end
+
+ function c = char(a)
+ c = '0';
+ end
+
+ function a = conj(a)
+ end
+
+ function bool = eq(~, ~)
+ bool = true;
+ end
+
+ function nu = frobeniusschur(~)
+ nu = 1;
+ end
+
+ function F = Fsymbol(~, ~, ~, ~, ~, ~)
+ F = 1;
+ end
+
+ function C = fusiontensor(~, ~, ~)
+ C = 1;
+ end
+
+ function a = intersect(a, ~)
+ end
+
+ function bool = issortedrows(~)
+ bool = true;
+ end
+
+ function a = mtimes(a, ~), end
+
+ function bools = ne(A, B)
+ if isscalar(A)
+ bools = true(size(B));
+ elseif isscalar(B)
+ bools = true(size(A));
+ else
+ assert(all(size(A) == size(B)), 'tensors:charges:SizeError');
+ bools = true(size(A));
+ end
+ end
+
+ function N = Nsymbol(~, ~, ~)
+ N = 1;
+ end
+
+ function R = Rsymbol(~, ~, ~, ~)
+ R = 1;
+ end
+
+ function varargout = sort(a, varargin)
+ if nargout == 1
+ varargout = {a};
+ elseif nargout == 2
+ varargout = {a, 1:numel(a)};
+ end
+ end
+
+ function s = string(a)
+ s = repmat("0", size(a));
+ end
+
+ function style = braidingstyle(~)
+ style = BraidingStyle.Bosonic;
+ end
+
+ function style = fusionstyle(~)
+ style = FusionStyle.Unique;
+ end
+
+ function e = one(~)
+ e = Z1;
+ end
+ end
+end
diff --git a/src/tensors/charges/Z2.m b/src/tensors/charges/Z2.m
new file mode 100644
index 0000000..b14fbdd
--- /dev/null
+++ b/src/tensors/charges/Z2.m
@@ -0,0 +1,96 @@
+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
+ %
+ % See also AbstractCharge
+
+ methods
+
+ function A = Asymbol(a, b, c)
+ A = double(Nsymbol(a, b, c));
+ end
+
+ function B = Bsymbol(a, b, c)
+ B = double(Nsymbol(a, b, c));
+ end
+
+ function a = conj(a)
+ end
+
+ function [charges, vertices] = cumprod(a)
+ charges = Z2(mod(cumsum(a), 2));
+ vertices = [];
+ end
+
+ function nu = frobeniusschur(a)
+ nu = ones(size(a));
+ end
+
+ function F = Fsymbol(a, b, c, d, e, f)
+ F = double(Nsymbol(a, b, e) .* Nsymbol(b, c, f) .* ...
+ Nsymbol(e, c, d) .* Nsymbol(a, f, d));
+ end
+
+ function C = fusiontensor(a, b, c)
+ C = double(Nsymbol(a, b, c));
+ end
+
+ function s = GetMD5_helper(data)
+ s = logical(data);
+ end
+
+ function c = mtimes(a, b)
+ c = Z2(xor(a, b));
+ end
+
+ function N = Nsymbol(a, b, c)
+ N = xor(a, b) == c;
+ end
+
+ function e = one(~)
+ e = Z2(false);
+ end
+
+ function [d, N] = prod(a, dim)
+ if nargin < 2 || isempty(dim)
+ dim = find(size(a) ~= 1, 1);
+ end
+ d = Z2(mod(sum(a, dim), 2));
+ if nargout > 1
+ N = ones(size(d));
+ end
+ end
+
+ function R = Rsymbol(a, b, c, ~)
+ R = double(Nsymbol(a, b, c));
+ end
+
+ function s = string(a)
+ s = strings(size(a));
+ s(~a) = "+";
+ s(a) = "-";
+ end
+
+ function charge = Z2(varargin)
+ if nargin == 0
+ labels = [];
+ else
+ labels = horzcat(varargin{:});
+ end
+ charge@logical(labels);
+ end
+
+ function style = braidingstyle(~)
+ style = BraidingStyle.Abelian;
+ end
+
+ function style = fusionstyle(~)
+ style = FusionStyle.Unique;
+ end
+ end
+end
diff --git a/src/tensors/kernels/AbelianBlock.m b/src/tensors/kernels/AbelianBlock.m
new file mode 100644
index 0000000..509a6e4
--- /dev/null
+++ b/src/tensors/kernels/AbelianBlock.m
@@ -0,0 +1,241 @@
+classdef AbelianBlock < MatrixBlock
+ %ABELIANBLOCK Summary of this class goes here
+ % Detailed explanation goes here
+
+
+ methods
+ function Y = axpby(a, X, b, Y, p, map)
+ %% Special cases
+ % only overload general case, other cases are unchanged.
+ if a == 0 || ...
+ nargin == 4 || ...
+ (isempty(p) && isempty(map)) || ...
+ (all(p == 1:length(p)) && all(X(1).rank == Y(1).rank))
+
+ Y = axpby@MatrixBlock(a, X, b, Y);
+ return;
+ end
+
+
+ %% General case: addition with permutation
+ % tensor indexing to matrix indexing
+ rrx = rankrange(X(1).rank);
+ rry = rankrange(Y(1).rank);
+ p_eff(rry) = rrx(p);
+
+ % extract small tensor blocks
+ doA = a ~= 1;
+ vars = cell(size(map, 1), 1);
+ offset = 0;
+ for i = 1:length(X)
+ rowsz = X(i).rowsizes;
+ colsz = X(i).colsizes;
+
+ var_in = X(i).var;
+ 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)];
+
+ 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(...
+ var_in(rowsz(j)+1:rowsz(j+1), colsz(k)+1:colsz(k+1)), ...
+ oldtdims(ind, :)), ...
+ p_eff), ...
+ newmdims(ind, :));
+ end
+ end
+ end
+
+ % apply map
+ [row, col] = find(map);
+ vars(col) = vars(row);
+
+ % 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;
+ 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
+
+% 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);
+ end
+ end
+end
diff --git a/src/tensors/kernels/AbstractBlock.m b/src/tensors/kernels/AbstractBlock.m
new file mode 100644
index 0000000..bd1355e
--- /dev/null
+++ b/src/tensors/kernels/AbstractBlock.m
@@ -0,0 +1,345 @@
+classdef (Abstract) AbstractBlock
+ % Abstract structure for storing tensor data.
+ % This represents the blocks in the block-diagonal decomposition of a general tensor.
+
+ %#ok<*INUSD>
+ %#ok<*INUSL>
+ %#ok<*STOUT>
+
+ %% Constructor
+ methods (Static)
+ function X = new(codomain, domain)
+ % Dispatch block type based on codomain and domain.
+ %
+ % Arguments
+ % ---------
+ % fun : function_handle
+ % initialising function for the tensor data, with signature
+ % `data = fun(dims)` where dims is a row vector of dimensions.
+ %
+ % codomain : AbstractSpace
+ % vector of vector spaces that form the codomain.
+ %
+ % domain : AbstractSpace
+ % vector of vector spaces that form the domain.
+ %
+ % Returns
+ % -------
+ % X : AbstractBlock
+ % tensor data.
+
+ if isa(codomain, 'CartesianSpace') || isa(codomain, 'ComplexSpace') || ...
+ isa(domain, 'CartesianSpace') || isa(domain, 'ComplexSpace')
+ X = TrivialBlock(codomain, domain);
+ return
+ end
+
+ if braidingstyle(codomain, domain) == BraidingStyle.Abelian && ...
+ fusionstyle(codomain, domain) == FusionStyle.Unique
+ X = AbelianBlock(codomain, domain);
+ return
+ end
+
+ X = MatrixBlock(codomain, domain);
+ end
+ end
+
+
+ %% Required methods
+ methods
+ function Y = axpby(a, X, b, Y, p, map)
+ % (Abstract) Compute ```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 optimisations when a == 1, b == 0 | b == 1. Additionally, this method
+ % should not be called directly as it should not perform any error checks.
+ %
+ % Arguments
+ % ---------
+ % a : double
+ % scalar to multiply with X.
+ %
+ % X : AbstractBlock
+ % list of source blocks.
+ %
+ % b : double
+ % scalar to multiply with Y.
+ %
+ % Y : AbstractBlock
+ % list of destination blocks.
+ %
+ % p : int
+ % permutation vector for X.
+ %
+ % map : (sparse) double
+ % coefficient matrix for permuting X.
+ %
+ % Returns
+ % -------
+ % Y : AbstractBlock
+ % Result of computing Y = permute(X, p) .* a + Y .* b.
+
+ error('This method should be overloaded.');
+ end
+
+ function [mblocks, mcharges] = matrixblocks(b)
+ % (Abstract) Extract a list of coupled matrix blocks.
+ %
+ % Arguments
+ % ---------
+ % b : AbstractBlock
+ % list of input data.
+ %
+ % Returns
+ % -------
+ % mblocks : cell
+ % list of non-zero coupled matrix blocks, sorted according to its charge.
+ %
+ % mcharges : AbstractCharge
+ % list of coupled charges.
+
+ error('This method should be overloaded.');
+ end
+
+ function C = mul(C, A, B, a, b)
+ % (Abstract) Compute ```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
+ % location to store the result
+ %
+ % A : AbstractBlock
+ % first matrix factor
+ %
+ % B : AbstractBlock
+ % second matrix factor
+ %
+ % a : double = 1
+ % first scalar factor
+ %
+ % b : double = 1
+ % second scalar factor
+ %
+ % Returns
+ % -------
+ % C : AbstractBlock
+ % Result of computing C = (A .* a) * (B .* b)
+
+ error('This method should be overloaded.');
+ end
+
+ function tblocks = tensorblocks(b)
+ % (Abstract) Extract a list of uncoupled tensor blocks.
+ %
+ % Arguments
+ % ---------
+ % b : AbstractBlock
+ % list of input data.
+ %
+ % Returns
+ % -------
+ % tblocks : 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.
+
+ error('This method should be overloaded.');
+ end
+ end
+
+
+ %% Optional methods
+ methods
+ function Y = axpy(a, X, Y, p, map)
+ % Compute ```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
+ % efficiency is desired.
+ %
+ % Arguments
+ % ---------
+ % a : double
+ % scalar to multiply with X.
+ %
+ % X : AbstractBlock
+ % list of source blocks.
+ %
+ % Y : AbstractBlock
+ % list of destination blocks.
+ %
+ % p : int
+ % permutation vector for X.
+ %
+ % map : (sparse) double
+ % coefficient matrix for permuting X.
+ %
+ % Returns
+ % -------
+ % Y : AbstractBlock
+ % Result of computing 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.
+ %
+ % Usage
+ % -----
+ % Y = minus(X, Y)
+ % Y = X - Y
+ %
+ % Arguments
+ % ---------
+ % X : AbstractBlock
+ % first list of input data.
+ %
+ % Y : AbstractBlock
+ % second list of input data.
+ %
+ % Returns
+ % -------
+ % Y : AbstractBlock
+ % list of output matrices.
+
+ Y = axpby(1, X, -1, Y);
+ end
+
+ function Y = plus(X, Y)
+ % Addition of X and Y.
+ %
+ % Usage
+ % -----
+ % Y = plus(X, Y)
+ % Y = X + Y
+ %
+ % Arguments
+ % ---------
+ % X : AbstractBlock
+ % first list of input data.
+ %
+ % Y : AbstractBlock
+ % second list of input data.
+ %
+ % Returns
+ % -------
+ % Y : MatrixBlock
+ % list of output data.
+
+ Y = axpby(1, X, 1, Y);
+ end
+
+ function Y = times(Y, a)
+ % Scalar multiplication of Y and a.
+ %
+ % Usage
+ % -----
+ % Y = times(Y, a)
+ % Y = Y .* a
+ %
+ % Arguments
+ % ---------
+ % Y : AbstractBlock
+ % list of input data.
+ %
+ % a : double
+ % scalar factor.
+ %
+ % Returns
+ % -------
+ % Y : AbstractBlock
+ % list of output data.
+
+ Y = axpby(0, [], a, Y);
+ end
+
+ function Y = rdivide(Y, a)
+ % Scalar division of Y and a.
+ %
+ % Usage
+ % -----
+ % Y = rdivide(Y, a)
+ % Y = Y ./ a
+ %
+ % Arguments
+ % ---------
+ % Y : AbstractBlock
+ % list of input data.
+ %
+ % a : double
+ % scalar factor.
+ %
+ % Returns
+ % -------
+ % Y : AbstractBlock
+ % list of output data.
+
+ Y = axpby(0, [], 1/a, Y);
+ end
+
+ function A = uplus(A)
+ % Unary plus. Equivalent to making a copy.
+ %
+ % Usage
+ % -----
+ % A = uplus(A)
+ % A = +A
+ %
+ % Arguments
+ % ---------
+ % A : AbstractBlock
+ % list of input data.
+ %
+ % Returns
+ % -------
+ % A : AbstractBlock
+ % list of output data.
+
+ end
+
+ function Y = uminus(Y)
+ % Unary minus. Computes the additive inverse.
+ %
+ % Usage
+ % -----
+ % A = uminus(A)
+ % A = -A
+ %
+ % Arguments
+ % ---------
+ % A : MatrixBlock
+ % list of input matrices.
+ %
+ % Returns
+ % -------
+ % A : MatrixBlock
+ % list of output matrices.
+
+ Y = Y .* (-1);
+ end
+
+ function t = underlyingType(X)
+ % The scalar type of the tensor.
+ %
+ % Arguments
+ % ---------
+ % X : :class:`AbstractBlock`
+ % input block.
+ %
+ % Returns
+ % -------
+ % type : 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'
+
+ error('tensors:AbstractMethod', 'This method should be overloaded.');
+ end
+ end
+end
\ No newline at end of file
diff --git a/src/tensors/kernels/MatrixBlock.m b/src/tensors/kernels/MatrixBlock.m
new file mode 100644
index 0000000..8b9b671
--- /dev/null
+++ b/src/tensors/kernels/MatrixBlock.m
@@ -0,0 +1,613 @@
+classdef MatrixBlock < AbstractBlock
+ %MATRIXBLOCK Summary of this class goes here
+ % Detailed explanation goes here
+
+ %#ok<*INUSD>
+ properties
+ charge
+ var
+ rowsizes
+ colsizes
+ tdims
+ rank
+ end
+
+ methods
+ function b = MatrixBlock(codomain, domain)
+ if nargin == 0, return; end
+
+ rank = [length(codomain) length(domain)];
+
+ trees = fusiontrees(codomain, domain);
+ [c, ~, ic] = unique(trees.coupled);
+ uncoupled = trees.uncoupled;
+
+ spaces = [codomain flip(domain)];
+ ch = charges(spaces).';
+ d = degeneracies(spaces).';
+
+ [lia, locb] = ismember(uncoupled, ch, 'rows');
+ tdims = d(locb(lia), :);
+% tdims = computedegeneracies(codomain, domain, uncoupled);
+ mdims = [prod(tdims(:, 1:rank(1)), 2) prod(tdims(:, (1:rank(2)) + rank(1)), 2)];
+ splits = split(trees);
+ fuses = fuse(trees);
+
+ for i = length(c):-1:1
+ ids = ic == i;
+
+ [~, ia] = unique(splits(ids));
+ rowdims = mdims(ids, 1);
+ rowsizes = [0 cumsum(rowdims(ia)).'];
+
+ [~, ia] = unique(fuses(ids));
+ 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;
+ end
+ end
+
+ function Y = axpby(a, X, b, Y, p, map)
+ % Compute ```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 optimisations when a == 1, b == 0 | b == 1.
+ %
+ % Arguments
+ % ---------
+ % a : double
+ % scalar to multiply with X.
+ %
+ % X : MatrixBlock
+ % list of source blocks.
+ %
+ % b : double
+ % scalar to multiply with Y.
+ %
+ % Y : MatrixBlock
+ % list of destination blocks.
+ %
+ % p : int
+ % permutation vector for X.
+ %
+ % map : (sparse) double
+ % coefficient matrix for permuting X.
+ %
+ % Returns
+ % -------
+ % Y : MatrixBlock
+ % Result of computing Y = permute(X, p) .* a + Y .* b.
+
+ %% Special case 1: scalar multiplication
+ if a == 0
+ Y = Y .* b;
+ return
+ end
+
+
+ %% Special case 2: addition without permutation
+ if nargin == 4 || (isempty(p) && isempty(map)) || ...
+ (all(p == 1:length(p)) && all(X(1).rank == Y(1).rank))
+ % reduce to scalar multiplication
+ if b == 0 % a ~= 0 -> case 1
+ Y = X .* a;
+ return
+ end
+
+ % reduce to simple addition or substraction
+ if a == 1 && b == 1, Y = X + Y; return; end
+ if a == 1 && b == -1, Y = X - Y; return; end
+ if a == -1 && b == 1, Y = Y - X; return; end
+
+ % 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;
+ end
+ return
+ end
+
+ if b == 1 % a ~= 1
+ for i = 1:length(Y)
+ Y(i).var = Y(i).var + X(i).var .* 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
+ return
+ end
+
+
+ %% General case: addition with permutation
+ % tensor indexing to matrix indexing
+ rrx = rankrange(X(1).rank);
+ rry = rankrange(Y(1).rank);
+ p_eff(rry) = rrx(p);
+
+ % 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;
+
+ var_in = X(i).var;
+ 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)];
+
+ for k = 1:length(colsz) - 1
+ for j = 1:length(rowsz) - 1
+ ind = j + (k-1) * (length(rowsz) - 1);
+ offset = offset + 1;
+
+ vars_in{offset} = reshape(permute(reshape(...
+ var_in(rowsz(j)+1:rowsz(j+1), colsz(k)+1:colsz(k+1)), ...
+ oldtdims(ind, :)), ...
+ p_eff), ...
+ newmdims(ind, :));
+ end
+ end
+ end
+
+ % apply map
+ vars_out = cell(size(map, 2), 1);
+ [rows, cols, vals] = find(map);
+ for i = 1:length(vals)
+ if isempty(vars_out{cols(i)})
+ vars_out{cols(i)} = vals(i) * vars_in{rows(i)};
+ else
+ vars_out{cols(i)} = vars_out{cols(i)} + vals(i) * vars_in{rows(i)};
+ end
+ 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;
+ 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{:});
+ 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{:});
+ 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_out{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_out{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_out{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_out{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 C = mul(C, A, B, a, b)
+ % Compute C = (A .* a) * (B .* b).
+ %
+ % Arguments
+ % ---------
+ % C : MatrixBlock
+ % location to store the result
+ %
+ % A : MatrixBlock
+ % first matrix factor
+ %
+ % B : MatrixBlock
+ % second matrix factor
+ %
+ % a : double = 1
+ % first scalar factor
+ %
+ % b : double = 1
+ % second scalar factor
+ %
+ % Returns
+ % -------
+ % C : MatrixBlock
+ % Result of computing C = (A .* a) * (B .* b)
+
+ [indA, locA] = ismember([C.charge], [A.charge]);
+ [indB, locB] = ismember([C.charge], [B.charge]);
+
+ if nargin == 3 || (a == 1 && b == 1)
+ for i = find(indA & indB)
+ C(i).var = A(locA(i)).var * B(locB(i)).var;
+ end
+ return
+ end
+
+ if a == 1 % b ~= 1
+ for i = find(indA & indB)
+ C(i).var = (A(locA(i)).var .* a) * B(locB(i)).var;
+ end
+ return
+ end
+
+ if b == 1 % a ~= 1
+ for i = find(indA & indB)
+ C(i).var = A(locA(i)).var * (B(locB(i)).var .* b);
+ end
+ return
+ end
+
+ % a ~= 1 && b ~= 1
+ for i = find(indA & indB)
+ C(i).var = (A(locA(i)).var .* a) * (B(locB(i)).var .* b);
+ end
+ end
+
+ function tblocks = tensorblocks(b)
+ % Extract the non-empty small tensor blocks.
+ %
+ % Arguments
+ % ---------
+ % b : MatrixBlock
+ % Big blocks to convert.
+ %
+ % Returns
+ % -------
+ % tblocks : cell
+ % 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);
+ 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;
+ 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)), ...
+ p);
+ end
+ end
+ end
+ end
+
+ function [mblocks, mcharges] = matrixblocks(b)
+ % Extract a list of coupled matrix blocks.
+ %
+ % Arguments
+ % ---------
+ % b : MatrixBlock
+ % list of input data.
+ %
+ % Returns
+ % -------
+ % mblocks : cell
+ % list of non-zero coupled matrix blocks, sorted according to its charge.
+ %
+ % mcharges : AbstractCharge
+ % list of coupled charges.
+
+ mblocks = {b.var};
+ if nargout > 1
+ 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};
+ end
+ return
+ end
+
+ [lia, locb] = ismember(charges, [b.charge]);
+ assert(all(lia));
+ for i = 1:length(vars)
+ b(locb(i)).var = vars{i};
+ end
+ 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);
+ end
+ else
+ [lia, locb] = ismember(charges, [b.charge]);
+ for i = locb(lia)
+ b(i).var = fun(size(b(i).var), b(i).charge);
+ end
+ end
+ end
+
+ function Y = minus(X, Y)
+ % Subtraction of X and Y.
+ %
+ % Usage
+ % -----
+ % Y = minus(X, Y)
+ % Y = X - Y
+ %
+ % Arguments
+ % ---------
+ % X : MatrixBlock
+ % first list of input matrices.
+ %
+ % Y : MatrixBlock
+ % second list of input matrices.
+ %
+ % Returns
+ % -------
+ % Y : MatrixBlock
+ % list of output matrices.
+
+ for i = 1:length(Y)
+ Y(i).var = X(i).var - Y(i).var;
+ end
+ end
+
+ function Y = plus(X, Y)
+ % Addition of X and Y.
+ %
+ % Usage
+ % -----
+ % Y = plus(X, Y)
+ % Y = X + Y
+ %
+ % Arguments
+ % ---------
+ % X : MatrixBlock
+ % first list of input matrices.
+ %
+ % Y : MatrixBlock
+ % second list of input matrices.
+ %
+ % Returns
+ % -------
+ % Y : MatrixBlock
+ % list of output matrices.
+
+ for i = 1:length(Y)
+ Y(i).var = Y(i).var + X(i).var;
+ end
+ end
+
+ function Y = times(Y, a)
+ % Scalar multiplication of Y and a.
+ %
+ % Usage
+ % -----
+ % Y = times(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
+
+ for i = 1:length(Y)
+ Y(i).var = Y(i).var .* a;
+ end
+ 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
+
+ for i = 1:length(Y)
+ Y(i).var = Y(i).var ./ a;
+ end
+ end
+
+ function A = uplus(A)
+ % Unary plus. Equivalent to making a copy.
+ %
+ % Usage
+ % -----
+ % A = uplus(A)
+ % A = +A
+ %
+ % Arguments
+ % ---------
+ % A : MatrixBlock
+ % list of input matrices.
+ %
+ % Returns
+ % -------
+ % A : MatrixBlock
+ % list of output matrices.
+
+ end
+
+ function Y = uminus(Y)
+ % Unary minus. Computes the additive inverse.
+ %
+ % Usage
+ % -----
+ % A = uminus(A)
+ % A = -A
+ %
+ % Arguments
+ % ---------
+ % A : MatrixBlock
+ % list of input matrices.
+ %
+ % Returns
+ % -------
+ % A : MatrixBlock
+ % list of output matrices.
+
+ for i = 1:length(Y)
+ Y(i).var = -Y(i).var;
+ end
+ end
+
+ function t = underlyingType(X)
+ t = underlyingType(X(1).var);
+ end
+
+ function X = ctranspose(X)
+ % Adjoint of a tensor.
+ %
+ % Usage
+ % -----
+ % X = ctranspose(X)
+ % X = X'
+ %
+ % Arguments
+ % ---------
+ % X : :class:`MatrixBlock`
+ % list of input matrices.
+ %
+ % Returns
+ % -------
+ % 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);
+ end
+ end
+
+ function v = vectorize(X, type)
+ 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);
+ 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)];
+ 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;
+ end
+ end
+ end
+ end
+end
diff --git a/src/tensors/kernels/TrivialBlock.m b/src/tensors/kernels/TrivialBlock.m
new file mode 100644
index 0000000..5959f7c
--- /dev/null
+++ b/src/tensors/kernels/TrivialBlock.m
@@ -0,0 +1,221 @@
+classdef TrivialBlock < AbstractBlock
+ % TrivialBlock - Data structure for tensors without symmetry.
+
+ properties
+ var
+ tdims
+ rank
+ end
+
+ methods
+ function b = TrivialBlock(codomain, domain)
+ if nargin == 0, return; end
+
+ b.rank = [length(codomain) length(domain)];
+ b.tdims = degeneracies([codomain, domain(length(domain):-1:1)]);
+ b.var = uninit(b.tdims);
+ end
+
+ function Y = axpby(a, X, b, Y, p, ~)
+
+ %% Special case 1: scalar multiplication
+ if a == 0
+ if b == 1
+ return;
+ end
+
+ if b == -1
+ Y = -Y;
+ return
+ end
+
+ Y = Y .* b;
+ return
+ end
+
+
+ %% Special case 2: addition without permutation
+ if nargin == 4 || isempty(p) || all(p == 1:length(p)) && ...
+ all(Y.rank == X.rank)
+ % reduce to scalar multiplication
+ if b == 0 % a ~= 0 -> case 1
+ Y = X .* a;
+ return
+ end
+
+ % reduce to simple addition or substraction
+ if a == 1 && b == 1, Y = X + Y; return; end
+ if a == 1 && b == -1, Y = X - Y; return; end
+ if a == -1 && b == 1, Y = Y - X; return; end
+
+ % general addition + scalar multiplication cases
+ if a == 1 % b ~= 1
+ Y.var = Y.var .* b + X.var;
+ return
+ end
+
+ if b == 1 % a ~= 1
+ Y.var = Y.var + X.var .* a;
+ return
+ end
+
+ % a ~= [0 1], b ~= [0 1]
+ Y.var = Y.var .* b + X.var .* a;
+ return
+ end
+
+
+ %% General case: addition with permutation
+ if a == 1
+ if b == 1
+ Y.var = Y.var + permute(X.var, p);
+ return
+ end
+
+ if b == 0
+ Y.var = permute(X.var, p);
+ return
+ end
+
+ Y.var = Y.var .* b + permute(X.var, p);
+ return
+ end
+
+ if b == 1
+ Y.var = Y.var + permute(X.var, p) .* a;
+ return
+ end
+ if nargin == 0, return; end
+
+ Y.var = Y.var .* b + permute(X.var, p) .* a;
+ end
+
+ function C = mul(C, A, B, a, b)
+ if nargin > 3 && ~isempty(a)
+ A = A .* a;
+ end
+
+ if nargin > 4 && ~isempty(b)
+ B = B .* b;
+ end
+
+ C.var = tensorprod(A.var, B.var, ...
+ A.rank(1) + (1:A.rank(2)), B.rank(1):-1:1, ...
+ 'NumDimensionsA', sum(A.rank));
+ end
+
+ function A = ctranspose(A)
+ A.rank = flip(A.rank);
+ A.tdims = flip(A.tdims);
+ A.var = conj(permute(A.var, length(A.tdims):-1:1));
+ end
+
+ function A = times(A, a)
+ if a == 1, return; end
+ if a == -1, A = -A; return; end
+ A.var = A.var .* a;
+ 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
+
+ Y.var = Y.var ./ a;
+ end
+
+ function tblocks = tensorblocks(b)
+ tblocks = {b.var};
+ end
+
+ function [mblocks, mcharges] = matrixblocks(b)
+ mblocks = {reshape(permute(b.var, rankrange(b.rank)), ...
+ prod(b.tdims(1:b.rank(1)), 2), ...
+ prod(b.tdims((1:b.rank(2)) + b.rank(1)), 2))};
+
+ if nargout > 1
+ mcharges = Z1;
+ end
+ end
+
+ function Y = minus(X, Y)
+ Y.var = X.var - Y.var;
+ end
+
+ function Y = plus(X, Y)
+ Y.var = X.var + Y.var;
+ end
+
+ function A = uplus(A)
+ end
+
+ function Y = uminus(Y)
+ Y.var = -Y.var;
+ end
+
+ function Y = fill_matrix_data(Y, dat, ~)
+ rry = rankrange(Y.rank);
+ Y.var = permute(reshape(dat{1}, Y.tdims(rry)), rry);
+ end
+
+ function Y = fill_matrix_fun(Y, fun, ~)
+ rry = rankrange(Y.rank);
+ mdims = [prod(Y.tdims(1:Y.rank(1)), 2), ...
+ prod(Y.tdims(Y.rank(1) + (1:Y.rank(2))), 2)];
+ Y.var = permute(reshape(fun(mdims), Y.tdims(rry)), rry);
+ end
+
+ function Y = fill_tensor_data(Y, dat, ~)
+ Y.var = dat{1};
+ end
+
+ function Y = fill_tensor_fun(Y, fun, ~)
+ Y.var = fun(Y.tdims);
+ end
+
+ function v = vectorize(X, type)
+ switch type
+ case 'complex'
+ v = X.var(:);
+
+ case 'real'
+ v = [real(X.var(:)); imag(X.var(:))];
+ end
+ 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));
+ end
+ end
+
+ function type = underlyingType(X)
+ type = underlyingType(X.var);
+ end
+ end
+end
diff --git a/src/tensors/spaces/AbstractSpace.m b/src/tensors/spaces/AbstractSpace.m
new file mode 100644
index 0000000..1bc7cab
--- /dev/null
+++ b/src/tensors/spaces/AbstractSpace.m
@@ -0,0 +1,307 @@
+classdef (Abstract) AbstractSpace
+ % Abstract structure of a tensor index.
+
+ properties (Access = protected)
+ dimensions % Specification of the internal dimensions
+ isdual (1,1) logical = false % Flag to indicate if the space is a dual space
+ end
+
+ %% Constructors
+ methods
+ function spaces = AbstractSpace(dimensions, isdual)
+ % Construct an array of vector spaces.
+ %
+ % Repeating Arguments
+ % -------------------
+ % dimensions : int or struct
+ % a variable which represents the internal dimension of the space.
+ %
+ % isdual : logical
+ % a variable which indicates if a space is dual.
+ %
+ % Returns
+ % -------
+ % spaces : :class:`AbstractSpace`
+ % array of spaces.
+
+ arguments (Repeating)
+ dimensions
+ isdual
+ end
+
+ if nargin == 0, return; end
+
+ for i = length(dimensions):-1:1
+ spaces(i).dimensions = dimensions{i};
+ spaces(i).isdual = isdual{i};
+ end
+ end
+ end
+
+ methods (Static)
+ function spaces = new(dimensions, isdual)
+ % 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
+ % a variable which represents the internal dimension of the space.
+ %
+ % isdual : logical
+ % a variable which indicates if a space is dual.
+ %
+ % Returns
+ % -------
+ % spaces : :class:`AbstractSpace`
+ % array of spaces.
+
+ error('tensors:AbstractMethod', 'This method should be overloaded.');
+ end
+ end
+
+
+ %% Structure
+ methods
+ 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`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % c : (:, :) :class:`AbstractCharge`
+ % list of charge combinations, where each row is a combination.
+
+ error('tensors:AbstractMethod', 'This method should be overloaded.');
+ end
+
+ function d = degeneracies(spaces)
+ % Compute all degeneracy combinations of a product space.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`AbstractSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % d : (:, :) int
+ % list of degeneracy combinations, where each row is an entry.
+
+ error('tensors:AbstractMethod', 'This method should be overloaded.');
+ end
+
+ function d = dims(spaces)
+ % Compute the dimension of the spaces.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`AbstractSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % d : (1, :) numeric
+ % total dimension of each of the input spaces.
+
+ error('tensors:AbstractMethod', 'This method should be overloaded.');
+ 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 `[]`.
+ %
+ % Arguments
+ % ---------
+ % codomain, domain : :class:`AbstractSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % trees : :class:`FusionTree`
+ % list of fusiontrees that are allowed.
+
+ error('tensors:AbstractMethod', 'This method should be overloaded.');
+ end
+
+ function style = braidingstyle(codomain, domain)
+ % Determine the braiding style of the internal structure of a space.
+ %
+ % Arguments
+ % ---------
+ % codomain, domain : (1, :) :class:`AbstractSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % style : :class:`BraidingStyle`
+ % braiding style of the internal structure.
+
+ error('tensors:AbstractMethod', 'This method should be overloaded.');
+ end
+
+ function style = fusionstyle(codomain, domain)
+ % Determine the fusion style of the internal structure of a space.
+ %
+ % Arguments
+ % ---------
+ % codomain, domain : (1, :) :class:`AbstractSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % style : :class:`FusionStyle`
+ % fusion style of the internal structure.
+
+ error('tensors:AbstractMethod', 'This method should be overloaded.');
+ end
+ end
+
+
+ %% Manipulations
+ methods
+ function spaces = conj(spaces)
+ % Compute the element-wise dual space.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`AbstractSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % spaces : (1, :) :class:`AbstractSpace`
+ % dual spaces.
+
+ for i = 1:length(spaces)
+ spaces(i).isdual = ~spaces(i).isdual;
+ end
+ end
+
+ function spaces = ctranspose(spaces)
+ % Compute the dual space of the product space, found by taking the reverse order
+ % of the dual spaces.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`AbstractSpace`
+ % input product space.
+ %
+ % Returns
+ % -------
+ % spaces : (1, :) :class:`AbstractSpace`
+ % dual product space.
+
+ spaces = conj(spaces(length(spaces):-1:1));
+ end
+
+ function space = mtimes(space1, space2)
+ % Fuse two spaces to a single space.
+ %
+ % Arguments
+ % ---------
+ % space1, space2 : (1,1) :class:`AbstractSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % space : (1,1) :class:`AbstractSpace`
+ % fused 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.
+ %
+ % Returns
+ % -------
+ % 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.
+ space = spaces(1);
+ for i = 2:length(spaces)
+ space = space * spaces(i);
+ end
+ end
+
+
+ %% Utility
+ function bools = eq(spaces1, spaces2)
+ % Verify if spaces are element-wise equal.
+ %
+ % Arguments
+ % ---------
+ % spaces1, spaces2 : (1, :) :class:`AbstractSpace`
+ % input spaces, either of equal size or scalar.
+ %
+ % Returns
+ % -------
+ % bools : (1, :) logical
+ % flags that indicate if the element spaces are equal.
+
+ error('tensors:AbstractMethod', 'This method should be overloaded.');
+ 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
+ % are considered equal to any empty object.
+ %
+ % Usage
+ % -----
+ % bool = isequal(spaces{:})
+ %
+ % Repeating Arguments
+ % -------------------
+ % spaces : (1,:) :class:`AbstractSpace`
+ % input spaces to compare.
+ %
+ % Returns
+ % -------
+ % bool : (1,1) logical
+ % true if all inputs are equal.
+
+ arguments (Repeating)
+ spaces
+ end
+
+ if nargin > 2
+ bool = isequal(spaces{1:2}) && isequal(spaces{2:end});
+ return
+ end
+
+ bool = (isempty(spaces{1}) && isempty(spaces{2})) || ...
+ (isequal(size(spaces{1}), size(spaces{2})) && ...
+ all(spaces{1} == spaces{2}));
+ 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 :function:`GetMD5`.
+ %
+ % Arguments
+ % ---------
+ % spaces : :class:`AbstractSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % hashable : cell
+ % data which can be accepted by :function:`GetMD5`.
+
+ hashable = {spaces.dimensions, spaces.isdual};
+ end
+ end
+end
diff --git a/src/tensors/spaces/Arrow.m b/src/tensors/spaces/Arrow.m
new file mode 100644
index 0000000..83b9af1
--- /dev/null
+++ b/src/tensors/spaces/Arrow.m
@@ -0,0 +1,10 @@
+classdef Arrow < logical
+% Arrow - Direction of tensor leg.
+% Enumeration class with directions.
+
+enumeration
+ in (false)
+ out (true)
+end
+
+end
diff --git a/src/tensors/spaces/CartesianSpace.m b/src/tensors/spaces/CartesianSpace.m
new file mode 100644
index 0000000..e3aaa31
--- /dev/null
+++ b/src/tensors/spaces/CartesianSpace.m
@@ -0,0 +1,288 @@
+classdef CartesianSpace < AbstractSpace
+ % Tensor index without any internal structure, which is self dual.
+
+ %% Constructors
+ methods
+ function spaces = CartesianSpace(dimensions, ~)
+ % Construct an array of vector spaces.
+ %
+ % Repeating Arguments
+ % -------------------
+ % dimensions : (1, 1) int
+ % dimension of the vector space.
+ %
+ % ~ : any
+ % ignored argument for syntax congruency with other spacetypes.
+ %
+ % Returns
+ % -------
+ % spaces : :class:`CartesianSpace`
+ % array of cartesian spaces.
+
+ arguments (Repeating)
+ dimensions (1,1) {mustBePositive}
+ ~
+ end
+
+ if nargin == 0
+ args = {};
+ else
+ isdual = num2cell(false(size(dimensions)));
+ args = [dimensions; isdual];
+ end
+
+ spaces = spaces@AbstractSpace(args{:});
+ end
+ end
+
+ methods (Static)
+ function spaces = new(varargin)
+ % Construct a vector space. This is a utility method to be able to access the
+ % constructor of a subclass.
+ %
+ % Usage
+ % -----
+ % spaces = CartesianSpace.new(dimensions, ~, ...)
+ %
+ % spaces = CartesianSpace.new(dims)
+ %
+ % Repeating Arguments
+ % -------------------
+ % dimensions : 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.
+
+ if isstruct(varargin{1})
+ assert(mod(nargin, 2) == 0)
+ args = cell(2, nargin / 2);
+ for i = 1:nargin / 2
+ args{1, i} = varargin{2 * i - 1}.degeneracies;
+ end
+ spaces = CartesianSpace(args{:});
+ return
+ end
+
+ if nargin == 1 && isnumeric(varargin{1})
+ args = cell(2, length(varargin{1}));
+ args(1, :) = num2cell(varargin{1});
+ spaces = CartesianSpace(args{:});
+ return
+ end
+
+ error('Undefined syntax.');
+ end
+ end
+
+
+ %% Structure
+ methods
+ function c = charges(~)
+ % Compute all charge combinations of a space. No internal structure is present,
+ % this yields an empty result.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`CartesianSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % c : []
+ % empty result.
+
+ c = [];
+ end
+
+ function d = degeneracies(spaces)
+ % Compute all degeneracy combinations of a product space.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`CartesianSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % d : (1, :) int
+ % list of degeneracy combinations, with 1 element.
+
+ d = [spaces.dimensions];
+ end
+
+ function d = dims(spaces)
+ % Compute the dimension of the spaces.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`CartesianSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % d : (1, :) 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`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % style : :class:`BraidingStyle`
+ % Trivial braiding style
+
+ style = BraidingStyle.Abelian;
+ end
+
+ function style = fusionstyle(~, ~)
+ % Determine the fusion style of the internal structure of a space.
+ %
+ % Arguments
+ % ---------
+ % codomain, domain : (1, :) :class:`CartesianSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % style : :class:`FusionStyle`
+ % fusion style of the internal structure.
+
+ style = FusionStyle.Unique;
+ end
+ end
+
+
+ %% Manipulations
+ methods
+ function spaces = conj(spaces)
+ % Compute the element-wise dual space, which is equal to itself.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`CartesianSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % spaces : (1, :) :class:`CartesianSpace`
+ % dual spaces.
+ end
+
+ function space = mtimes(space, space2)
+ % Fuse two spaces to a single space.
+ %
+ % Arguments
+ % ---------
+ % space1, space2 : (1, 1) :class:`CartesianSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % space : (1, 1) :class:`CartesianSpace`
+ % fused space.
+
+ space.dimensions = space.dimensions * space2.dimensions;
+ end
+
+ function space = prod(spaces)
+ % Fuse a product space to a single space.
+ %
+ % Arguments
+ % ---------
+ % 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 = CartesianSpace(prod(dims(spaces)), []);
+ end
+ end
+
+
+ %% Utility
+ methods
+ function bools = eq(spaces1, spaces2)
+ % Verify if spaces are element-wise equal.
+ %
+ % Arguments
+ % ---------
+ % spaces1, spaces2 : (1, :) :class:`CartesianSpace`
+ % input spaces, either of equal size or scalar.
+ %
+ % Returns
+ % -------
+ % bools : (1, :) logical
+ % flags that indicate if the element spaces are equal.
+
+ bools = [spaces1.dimensions] == [spaces2.dimensions];
+ 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 :function:`GetMD5`.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`CartesianSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % hashable : (1, :) int
+ % data which can be accepted by :function:`GetMD5`.
+
+ hashable = [spaces.dimensions];
+ end
+
+ function disp(spaces)
+ % Custom display of spaces.
+
+ if isscalar(spaces)
+ fprintf('CartesianSpace of dimension %d:\n', spaces.dimensions);
+ 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');
+ end
+ end
+ end
+end
diff --git a/src/tensors/spaces/ComplexSpace.m b/src/tensors/spaces/ComplexSpace.m
new file mode 100644
index 0000000..8111481
--- /dev/null
+++ b/src/tensors/spaces/ComplexSpace.m
@@ -0,0 +1,251 @@
+classdef ComplexSpace < AbstractSpace
+ % Tensor index without any internal structure, which is not self dual.
+
+ %% Constructors
+ methods
+ function spaces = ComplexSpace(dimensions, isdual)
+ % Construct an array of vector spaces.
+ %
+ % Repeating Arguments
+ % -------------------
+ % dimensions : (1, 1) int
+ % dimension of the vector space.
+ %
+ % isdual : (1, 1) logical
+ % flag to denote dual spaces.
+ %
+ % Returns
+ % -------
+ % spaces : :class:`ComplexSpace`
+ % array of complex spaces.
+
+ arguments (Repeating)
+ dimensions (1, 1) {mustBePositive}
+ isdual (1, 1) logical
+ end
+
+ if nargin == 0
+ args = {};
+ else
+ args = [dimensions; isdual];
+ end
+
+ spaces = spaces@AbstractSpace(args{:});
+ end
+ end
+
+ methods (Static)
+ function spaces = new(dimensions, isdual)
+ % 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
+ % a variable which represents the internal dimension of the space.
+ %
+ % isdual : logical
+ % a variable which indicates if a space is dual.
+ %
+ % Returns
+ % -------
+ % spaces : :class:`ComplexSpace`
+ % array of spaces.
+
+ arguments (Repeating)
+ dimensions (1, 1)
+ isdual (1, 1) logical
+ end
+
+ if nargin == 0
+ spaces = ComplexSpace;
+ else
+ for i = 1:length(dimensions)
+ if isstruct(dimensions{i})
+ dimensions{i} = dimensions{i}.degeneracies;
+ end
+ end
+ args = [dimensions; isdual];
+ spaces = ComplexSpace(args{:});
+ end
+ end
+ end
+
+
+ %% Structure
+ methods
+ function c = charges(~)
+ % Compute all charge combinations of a space. No internal structure is present,
+ % this yields an empty result.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`ComplexSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % c : []
+ % empty result.
+
+ c = [];
+ end
+
+ function d = degeneracies(spaces)
+ % Compute all degeneracy combinations of a product space.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`ComplexSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % d : (1, :) int
+ % list of degeneracy combinations, with 1 element.
+
+ d = [spaces.dimensions];
+ end
+
+ function d = dims(spaces)
+ % Compute the dimension of the spaces.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`ComplexSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % d : (1, :) 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`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % style : :class:`BraidingStyle`
+ % Trivial braiding style
+
+ style = BraidingStyle.Abelian;
+ end
+
+ function style = fusionstyle(~, ~)
+ % Determine the fusion style of the internal structure of a space.
+ %
+ % Arguments
+ % ---------
+ % codomain, domain : (1, :) :class:`ComplexSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % style : :class:`FusionStyle`
+ % fusion style of the internal structure.
+
+ style = FusionStyle.Unique;
+ end
+ end
+
+
+ %% Manipulations
+ methods
+ function space = mtimes(space, space2)
+ % Fuse two spaces to a single space.
+ %
+ % Arguments
+ % ---------
+ % space1, space2 : (1, 1) :class:`ComplexSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % space : (1, 1) :class:`ComplexSpace`
+ % fused space.
+
+ space.dimensions = space.dimensions * space2.dimensions;
+ space.isdual = false;
+ end
+
+ function space = prod(spaces)
+ % Fuse a product space to a single space.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`ComplexSpace`
+ % Array of input spaces.
+ %
+ % Returns
+ % -------
+ % space : (1, 1) :class:`ComplexSpace`
+ % Fused space which is isomorphic to the input product space.
+
+ space = ComplexSpace(prod(dims(spaces)), false);
+ end
+ end
+
+
+ %% Utility
+ methods
+ function bools = eq(spaces1, spaces2)
+ % Verify if spaces are element-wise equal.
+ %
+ % Arguments
+ % ---------
+ % spaces1, spaces2 : (1, :) :class:`ComplexSpace`
+ % input spaces, either of equal size or scalar.
+ %
+ % Returns
+ % -------
+ % bools : (1, :) logical
+ % flags that indicate if the element spaces are equal.
+
+ bools = [spaces1.dimensions] == [spaces2.dimensions] & ...
+ [spaces1.isdual] == [spaces2.isdual];
+ 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 :function:`GetMD5`.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`ComplexSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % hashable : (1, :) int
+ % data which can be accepted by :function:`GetMD5`.
+
+ hashable = [spaces.dimensions spaces.isdual];
+ end
+ end
+
+end
\ No newline at end of file
diff --git a/src/tensors/spaces/GradedSpace.m b/src/tensors/spaces/GradedSpace.m
new file mode 100644
index 0000000..a2f4b39
--- /dev/null
+++ b/src/tensors/spaces/GradedSpace.m
@@ -0,0 +1,537 @@
+classdef GradedSpace < AbstractSpace
+ % Tensor index structure with internal structure, which is not self dual.
+
+ %% Constructors
+ methods
+ function spaces = GradedSpace(dimensions, isdual)
+ % Construct an array of vector spaces.
+ %
+ % Repeating Arguments
+ % -------------------
+ % dimensions : (1, 1) struct
+ % internal structure of the vector space, with fields 'charges' and
+ % 'degeneracies'.
+ %
+ % isdual : (1, 1) logical
+ % flag to denote dual spaces.
+ %
+ % Returns
+ % -------
+ % spaces : :class:`GradedSpace`
+ % array of cartesian spaces.
+
+ arguments (Repeating)
+ dimensions (1, 1) struct
+ isdual (1, 1) logical
+ end
+
+ if nargin == 0
+ args = {};
+ else
+ for i = 1:length(dimensions)
+ assert(isfield(dimensions{i}, 'charges'));
+ assert(isfield(dimensions{i}, 'degeneracies'));
+ assert(length(dimensions{i}.charges) == ...
+ length(dimensions{i}.degeneracies), ...
+ 'tensors:ArgumentError', ...
+ 'Charges and degeneracies should have equal size.');
+ assert(length(unique(dimensions{i}.charges)) == ...
+ length(dimensions{i}.charges), ...
+ 'tensors:ArgumentError', ...
+ 'Charges should be unique.');
+
+ [dimensions{i}.charges, I] = sort(dimensions{i}.charges);
+ dimensions{i}.degeneracies = dimensions{i}.degeneracies(I);
+ end
+
+ args = [dimensions; isdual];
+ end
+
+ spaces = spaces@AbstractSpace(args{:});
+ end
+ end
+
+ methods (Static)
+ function spaces = new(varargin)
+ % Construct a vector space. This is a utility method to be able to access the
+ % constructor of a subclass.
+ %
+ % Usage
+ % -----
+ % spaces = GradedSpace.new(charges, degeneracies, isdual, ...)
+ %
+ % spaces = GradedSpace.new(dimensions, isdual, ...)
+ %
+ % Repeating Arguments
+ % -------------------
+ % dimensions : struct
+ % a variable which represents the internal dimension of the space.
+ %
+ % charges : :class:`AbstractCharge`
+ % charges for the internal structure of the space.
+ %
+ % degeneracies : int
+ % degeneracies for the internal structure of the space.
+ %
+ % isdual : logical
+ % a variable which indicates if a space is dual.
+ %
+ % Returns
+ % -------
+ % spaces : :class:`GradedSpace`
+ % array of spaces.
+
+ if isstruct(varargin{1}) % default
+ assert(mod(nargin, 2) == 0);
+ spaces = GradedSpace(varargin{:});
+ return
+ end
+
+ if isa(varargin{1}, 'AbstractCharge')
+ assert(mod(nargin, 3) == 0);
+ 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{:});
+ return
+ end
+
+ error('Undefined syntax.');
+ end
+ end
+
+
+ %% Structure
+ methods
+ function c = charges(spaces)
+ % Compute all charge combinations of a product space.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`GradedSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % c : (:, :) :class:`AbstractCharge`
+ % list of charge combinations, where each row is an entry.
+
+ if spaces(1).isdual
+ c = conj(spaces(1).dimensions.charges);
+ else
+ c = spaces(1).dimensions.charges;
+ end
+
+ for i = 2:length(spaces)
+ if spaces(i).isdual
+ c = combvec(c, conj(spaces(i).dimensions.charges));
+ else
+ c = combvec(c, spaces(i).dimensions.charges);
+ end
+ end
+ end
+
+ function d = degeneracies(spaces)
+ % Compute all degeneracy combinations of a product space.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`GradedSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % d : (:, :) int
+ % list of degeneracy combinations, where each row is an entry.
+
+ d = spaces(1).dimensions.degeneracies;
+ for i = 2:length(spaces)
+ d = combvec(d, spaces(i).dimensions.degeneracies);
+ end
+ end
+
+ function d = dims(spaces)
+ % Compute the dimension of the spaces.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`GradedSpace`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % d : (1, :) int
+ % total dimension of each of the input spaces.
+
+ d = zeros(size(spaces));
+ for i = 1:length(spaces)
+ d(i) = sum(qdim(spaces(i).dimensions.charges) .* ...
+ spaces(i).dimensions.degeneracies);
+ 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`
+ % input spaces.
+ %
+ % Returns
+ % -------
+ % space : (1, 1) :class:`GradedSpace`
+ % fused space.
+
+ space = prod([space1, space2]);
+ end
+
+ function space = prod(spaces)
+ % Fuse a product space to a single space.
+ %
+ % Arguments
+ % ---------
+ % spaces : (1, :) :class:`GradedSpace`
+ % Array of input spaces.
+ %
+ % Returns
+ % -------
+ % space : (1, 1) :class:`GradedSpace`
+ % Fused space which is isomorphic to the input product space.
+
+ if fusionstyle(spaces) == FusionStyle.Unique
+ c = prod(charges(spaces), 1);
+ d = prod(degeneracies(spaces), 1);
+ else
+ c = charges(spaces).';
+ d = prod(degeneracies(spaces), 1);
+ c_cell = cell(size(c, 1), 1);
+ d_cell = cell(size(c_cell));
+ for i = 1:length(c_cell)
+ [c_cell{i}, N] = prod(c(i, :));
+ d_cell{i} = d(i) .* N;
+ end
+ c = horzcat(c_cell{:});
+ d = horzcat(d_cell{:});
+ end
+
+ newdimensions = struct;
+ [newdimensions.charges, ~, ic] = unique(c);
+ newdimensions.degeneracies = zeros(size(newdimensions.charges));
+ for i = 1:length(newdimensions.degeneracies)
+ idx = ic == i;
+ newdimensions.degeneracies(i) = sum(d(idx));
+ end
+
+ space = GradedSpace(newdimensions, false);
+ end
+
+
+ %% Utility
+ function bools = eq(spaces1, spaces2)
+ % Verify if spaces are element-wise equal.
+ %
+ % Arguments
+ % ---------
+ % spaces1, spaces2 : (1, :) :class:`GradedSpace`
+ % input spaces, either of equal size or scalar.
+ %
+ % Returns
+ % -------
+ % bools : (1, :) logical
+ % flags that indicate if the element spaces are equal.
+
+ if isempty(spaces1) && isempty(spaces2)
+ bools = [];
+ return
+ end
+
+ bools = [spaces1.isdual] == [spaces2.isdual];
+
+ if isscalar(spaces2)
+ for i = 1:length(spaces1)
+ bools(i) = bools(i) && ...
+ isequal(spaces1(i).dimensions, spaces2.dimensions);
+ end
+ elseif isscalar(spaces1)
+ for i = 1:length(spaces2)
+ bools(i) = bools(i) && ...
+ isequal(spaces2(i).dimensions, spaces1.dimensions);
+ end
+ else
+ assert(isequal(size(spaces1), size(spaces2)));
+ for i = 1:length(spaces1)
+ bools(i) = bools(i) && ...
+ isequal(spaces1(i).dimensions, spaces2(i).dimensions);
+ end
+ end
+ end
+
+%
+%
+%
+% function engine = update_hash(spaces, engine)
+% engine = update_hash({spaces.charges, spaces.bonds, spaces.isdual}, engine);
+% end
+%
+% function s = GetMD5_helper(data)
+% s = {data.charges, data.bonds, data.isdual};
+% end
+%
+% function bool = hassymmetry(codomain, domain)
+% if isempty(codomain)
+% bool = ~isa(domain(1).charges, 'Z1');
+% return
+% end
+% bool = ~isa(codomain(1).charges, 'Z1');
+% end
+%
+% function [engine, digest] = hash(spaces, engine)
+% if nargin < 2 || isempty(engine)
+% engine = init_hash();
+% end
+%
+% engine.update(uint8('CartesianSpace'));
+% d = dim(spaces);
+% if ~isempty(d)
+% engine.update(typecast(d, 'uint8'));
+% end
+%
+% if nargout > 1
+% digest = engine.digest;
+% end
+% end
+%
+% function d = computedims(codomain, domain, charges)
+% assert(length(codomain) + length(domain) == size(charges, 2));
+%
+% d = zeros(size(charges));
+% for i = 1:size(d, 2)
+% if i <= length(codomain)
+% d(:, i) = dims(codomain(i), charges(:, i));
+% else
+% d(:, i) = dims(domain(end + 1 - (i - length(codomain))), charges(:, i));
+% end
+% end
+% if size(d, 2) == 1
+% if isempty(codomain)
+% d = [ones(size(d)) d];
+% else
+% d = [d ones(size(d))];
+% end
+% end
+% end
+%
+% function d = computedegeneracies(codomain, domain, charges)
+% assert(length(codomain) + length(domain) == size(charges, 2));
+%
+% d = zeros(size(charges));
+% for i = 1:size(d, 2)
+% if i <= length(codomain)
+% d(:, i) = degeneracy(codomain(i), charges(:, i));
+% else
+% d(:, i) = degeneracy(domain(end + 1 - (i - length(codomain))), charges(:, i));
+% end
+% end
+% if size(d, 2) == 1
+% if isempty(codomain)
+% d = [ones(size(d)) d];
+% else
+% d = [d ones(size(d))];
+% end
+% end
+% end
+%
+% function d = dim(spaces, charges)
+% d = zeros(1, length(spaces));
+% if nargin == 1
+% for i = 1:length(spaces)
+% d(i) = sum(dims(spaces(i)));
+% end
+% return
+% end
+% for i = 1:length(spaces)
+% d(i) = sum(dims(spaces(i), charges(:, i)));
+% end
+% end
+%
+% function d = degeneracy(space, charges)
+% if nargin == 1 || isempty(charges)
+% d = space.bonds;
+% return
+% end
+% d = zeros(1, length(charges));
+% [lia, locb] = ismember(charges, space.int_charges);
+% d(lia) = space.bonds(locb(lia));
+% end
+% %
+% % function d = dims(space, charges)
+% % if nargin == 1 || isempty(charges)
+% % d = qdim(space.charges) .* space.bonds;
+% % return
+% % end
+% % d = zeros(1, length(charges));
+% % [lia, locb] = ismember(charges, space.int_charges);
+% % d(lia) = qdim(space.charges(locb(lia))) .* space.bonds(locb(lia));
+% % end
+% %
+%
+%
+%
+% function bools = eq(A, B)
+% if isempty(A) && isempty(B)
+% bools = [];
+% return
+% end
+% if isscalar(A)
+% bools = false(size(B));
+% for i = 1:numel(B)
+% bools(i) = A.isdual == B(i).isdual && ...
+% all(A.bonds == B(i).bonds) && ...
+% all(A.charges == B(i).charges);
+% end
+% return
+% end
+% if isscalar(B)
+% bools = B == A;
+% return
+% end
+%
+% assert(all(size(A) == size(B)));
+% bools = false(size(A));
+% for i = 1:numel(A)
+% bools(i) = A(i).isdual == B(i).isdual && ...
+% all(A(i).bonds == B(i).bonds) && ...
+% all(A(i).charges == B(i).charges);
+% end
+% end
+%
+
+
+ function style = braidingstyle(codomain, domain)
+ if nargin == 1 || ~isempty(codomain)
+ style = braidingstyle(codomain(1).charges);
+ else
+ style = braidingstyle(domain(1).charges);
+ end
+ end
+
+ function style = fusionstyle(codomain, domain)
+ if nargin == 1 || ~isempty(codomain)
+ style = fusionstyle(codomain(1).charges);
+ else
+ style = fusionstyle(domain(1).charges);
+ 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
+
+ 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);
+
+ end
+ 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)));
+ 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');
+ end
+ end
+ end
+
+
+
+end
diff --git a/src/utility/Options.m b/src/utility/Options.m
new file mode 100644
index 0000000..2a65273
--- /dev/null
+++ b/src/utility/Options.m
@@ -0,0 +1,18 @@
+classdef Options
+ % Various package settings.
+
+
+ methods (Static)
+ function bool = CacheEnabled(bool)
+ persistent isenabled
+ if nargin > 0
+ isenabled = bool;
+ return
+ end
+
+ if isempty(isenabled), isenabled = true; end
+ bool = isenabled;
+ end
+ end
+end
+
diff --git a/src/utility/indices/contractinds.m b/src/utility/indices/contractinds.m
new file mode 100644
index 0000000..627586f
--- /dev/null
+++ b/src/utility/indices/contractinds.m
@@ -0,0 +1,13 @@
+function [dimA, dimB] = contractinds(ia, ib)
+% contractinds - Find the contracted dimensions.
+% [dimA, dimB] = contractinds(ia, ib)
+% locates the repeated indices.
+
+ind = find(ia(:) == ib).' - 1;
+dimA = mod(ind, length(ia)) + 1;
+dimB = floor(ind / length(ia)) + 1;
+
+% slower than the automatic:
+% [dimA, dimB] = ind2sub([length(ia) length(ib)], find(ia(:) == ib));
+
+end
diff --git a/src/utility/indices/mod1.m b/src/utility/indices/mod1.m
new file mode 100644
index 0000000..3c0207a
--- /dev/null
+++ b/src/utility/indices/mod1.m
@@ -0,0 +1,20 @@
+function m = mod1(x, y)
+% Modulus after division, counting from 1:y.
+%
+% Arguments
+% ---------
+% x : int
+% numerator.
+%
+% y : int
+% divisor.
+%
+% Returns
+% -------
+% m : int
+% remainder after division, where a value of 0 is replaced with y.
+
+m = mod(x-1, y) + 1;
+
+end
+
diff --git a/src/utility/indices/rankrange.m b/src/utility/indices/rankrange.m
new file mode 100644
index 0000000..8751470
--- /dev/null
+++ b/src/utility/indices/rankrange.m
@@ -0,0 +1,7 @@
+function r = rankrange(rank)
+%RANKRANGE Summary of this function goes here
+% Detailed explanation goes here
+
+r = [1:rank(1) rank(1) + (rank(2):-1:1)];
+
+end
diff --git a/src/utility/indices/treeindsequence.m b/src/utility/indices/treeindsequence.m
new file mode 100644
index 0000000..28fd144
--- /dev/null
+++ b/src/utility/indices/treeindsequence.m
@@ -0,0 +1,25 @@
+function n = treeindsequence(n)
+% Compute the number of edge labels needed in a splitting or fusing tree.
+%
+% Usage
+% -----
+% ```t = treeindsequence(n)```
+%
+% Arguments
+% ---------
+% n : int
+% number of external edges
+%
+% Returns
+% -------
+% t : int
+% total number of edges
+%
+% Examples
+% --------
+% ```treeindsequence(0:4)```
+% ans = [0 1 2 4 6]
+
+n = max(n, 2 * n - 2);
+
+end
diff --git a/src/utility/indices/unique1.m b/src/utility/indices/unique1.m
new file mode 100644
index 0000000..e2621af
--- /dev/null
+++ b/src/utility/indices/unique1.m
@@ -0,0 +1,8 @@
+function inds = unique1(inds)
+% unique1 - Find the elements that appear exactly once.
+% inds = unique1(inds)
+% deletes all elements that appear more than once.
+
+inds = inds(sum(inds(:) == inds) == 1);
+
+end
diff --git a/src/utility/indices/unique2.m b/src/utility/indices/unique2.m
new file mode 100644
index 0000000..e56e310
--- /dev/null
+++ b/src/utility/indices/unique2.m
@@ -0,0 +1,10 @@
+function inds = unique2(inds)
+% unique2 - Find the elements that appear more than once.
+% inds = unique2(inds)
+% deletes all elements that appear only once.
+
+[~, ia1] = unique(inds, 'first');
+[~, ia2] = unique(inds, 'last');
+inds(ia1 == ia2) = [];
+
+end
diff --git a/src/utility/linalg/contract.m b/src/utility/linalg/contract.m
new file mode 100644
index 0000000..9219202
--- /dev/null
+++ b/src/utility/linalg/contract.m
@@ -0,0 +1,144 @@
+function C = contract(tensors, indices, kwargs)
+% Compute a tensor network contraction.
+%
+% Usage
+% -----
+% :code:`C = contract(t1, idx1, t2, idx2, ...)`
+%
+% :code:`C = contract(..., 'Conj', conjlist)`
+%
+% :code:`C = contract(..., 'Rank', r)`
+%
+% Repeating Arguments
+% -------------------
+% 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.
+%
+% Keyword Arguments
+% -----------------
+% Conj : (1, :) logical
+% optional list to flag that tensors should be conjugated.
+%
+% Rank : (1, 2) int
+% optionally specify the rank of the resulting tensor.
+%
+% Returns
+% -------
+% C : :class:`Tensor` or numeric
+% result of the tensor network contraction.
+
+% TODO contraction order checker, order specifier.
+
+arguments (Repeating)
+ tensors
+ indices (1, :) {mustBeInteger}
+end
+
+arguments
+ kwargs.Conj (1, :) logical = false(size(tensors))
+ kwargs.Rank = []
+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.');
+ end
+end
+
+% Special case for single input tensor
+if nargin == 2
+ [~, order] = sort(indices{1}, 'descend');
+ C = tensors{1};
+ if isnumeric(C)
+ 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
+
+% 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});
+[B, ib, cb] = contracttree(tensors, indices, kwargs.Conj, tree{2});
+
+% contract last pair
+[dimA, dimB] = contractinds(ia, ib);
+C = tensorprod(A, B, dimA, dimB, ca, cb, 'NumDimensionsA', length(ia));
+ia(dimA) = []; ib(dimB) = [];
+ic = [ia ib];
+
+% permute last tensor
+if ~isempty(ic)
+ [~, 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
+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/distance.m b/src/utility/linalg/distance.m
new file mode 100644
index 0000000..7be4257
--- /dev/null
+++ b/src/utility/linalg/distance.m
@@ -0,0 +1,6 @@
+function d = distance(A, B)
+% Compute the Euclidian distance between two arrays.
+
+d = norm(A(:) - B(:));
+
+end
diff --git a/src/utility/linalg/isapprox.m b/src/utility/linalg/isapprox.m
new file mode 100644
index 0000000..91ea4c2
--- /dev/null
+++ b/src/utility/linalg/isapprox.m
@@ -0,0 +1,13 @@
+function bool = isapprox(A, B, tol)
+
+arguments
+ A
+ B
+ tol.RelTol = max(sqrt(eps(underlyingType(A))), ...
+ sqrt(eps(underlyingType(B))))
+ 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(:))));
+end
diff --git a/src/utility/linalg/isisometry.m b/src/utility/linalg/isisometry.m
new file mode 100644
index 0000000..a4fc99e
--- /dev/null
+++ b/src/utility/linalg/isisometry.m
@@ -0,0 +1,53 @@
+function bool = isisometry(A, side, tol)
+% Verify whether a matrix is an isometry.
+%
+% Arguments
+% ---------
+% A : numeric
+% input matrix.
+%
+% side : 'left', 'right' or 'both'
+% check if A' * A == I, A * A' == I, or both by default.
+%
+% Keyword Arguments
+% -----------------
+% AbsTol : double
+% absolute tolerance
+%
+% RelTol : double
+% relative tolerance
+%
+% Returns
+% -------
+% bool : logical
+% true if A is an isometry.
+
+arguments
+ A
+ side {mustBeMember(side, {'left', 'right', 'both'})} = 'both'
+ tol.RelTol = sqrt(eps(underlyingType(A)))
+ tol.AbsTol = 0
+end
+
+[m, n] = size(A);
+
+switch side
+ case 'left'
+ if m < n, bool = false; return; end
+
+ bool = isapprox(A' * A, eye(n), ...
+ 'RelTol', tol.RelTol, 'AbsTol', tol.AbsTol);
+
+ case 'right'
+ if n < m, bool = false; return; end
+
+ bool = isapprox(A * A', eye(m), 'RelTol', tol.RelTol, 'AbsTol', tol.AbsTol);
+
+ case 'both'
+ if m ~= n, bool = false; return; end
+ I = eye(m);
+ bool = isapprox(A * A', I, 'RelTol', tol.RelTol, 'AbsTol', tol.AbsTol) && ...
+ isapprox(A' * A, I, 'RelTol', tol.RelTol, 'AbsTol', tol.AbsTol);
+end
+
+end
diff --git a/src/utility/linalg/isposdef.m b/src/utility/linalg/isposdef.m
new file mode 100644
index 0000000..1b06aef
--- /dev/null
+++ b/src/utility/linalg/isposdef.m
@@ -0,0 +1,7 @@
+function bool = isposdef(A)
+% Verify whether a matrix is positive definite.
+
+[~, flag] = chol(A);
+bool = flag == 0;
+
+end
diff --git a/src/utility/linalg/leftnull.m b/src/utility/linalg/leftnull.m
new file mode 100644
index 0000000..c547045
--- /dev/null
+++ b/src/utility/linalg/leftnull.m
@@ -0,0 +1,55 @@
+function N = leftnull(A, alg, atol)
+% Compute the left nullspace of a matrix, such that N' * A = 0.
+%
+% Arguments
+% ---------
+% A : numeric
+% input matrix.
+%
+% alg : '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))).
+%
+% Returns
+% -------
+% N : numeric
+% orthonormal basis for the orthogonal complement of the support of the row space of A.
+
+arguments
+ A
+ alg = 'svd'
+ atol = []
+end
+
+[m, n] = size(A);
+
+switch alg
+ case 'svd'
+ [U, S] = svd(A);
+
+ if n == 1
+ s = S(1);
+ else
+ s = diag(S);
+ end
+
+ if isempty(atol)
+ atol = max(m, n) * eps(max(s));
+ end
+
+ r = sum(s > atol);
+ N = U(:, r+1:m);
+
+ case 'qr'
+ [Q, R] = qr(A);
+
+ r = size(R, 2);
+ N = Q(:, r+1:m);
+
+ otherwise
+ error('Invalid algorithm');
+end
+
+end
diff --git a/src/utility/linalg/leftorth.m b/src/utility/linalg/leftorth.m
new file mode 100644
index 0000000..2a621db
--- /dev/null
+++ b/src/utility/linalg/leftorth.m
@@ -0,0 +1,56 @@
+function [Q, R] = leftorth(A, alg)
+
+
+
+% 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);
+ D = diag(R);
+ D(abs(D) < 1e-12) = 1;
+ D = sign(D);
+ Q = Q .* D';
+ R = D .* R;
+
+ case 'ql'
+ [Q, R] = qr(flip(A, 2), 0);
+ Q = flip(Q, 2);
+ R = flip(flip(R, 1), 2);
+
+ case 'qlpos'
+ [Q, R] = qr(flip(A, 2), 0);
+ D = diag(R);
+ D(abs(D) < 1e-12) = 1;
+ D = sign(D);
+ Q = Q .* D';
+ R = D .* R;
+ Q = flip(Q, 2);
+ R = flip(flip(R, 1), 2);
+
+ case 'polar'
+ [U, S, V] = svd(A, 0);
+ [m, n] = size(A);
+ if m < n % m > n handled by economy svd
+ S = S(:, 1:m);
+ V = V(:, 1:m);
+ end
+ Q = U * V';
+ R = V * S * V';
+ R = (R + R') / 2; % force hermitian
+
+ case 'svd'
+ % TODO add tol?
+ [Q, S, V] = svd(A, 0);
+ R = S * V';
+
+ otherwise
+ error('Invalid alg (%s)', alg);
+
+end
+
+end
diff --git a/src/utility/linalg/rightnull.m b/src/utility/linalg/rightnull.m
new file mode 100644
index 0000000..f891242
--- /dev/null
+++ b/src/utility/linalg/rightnull.m
@@ -0,0 +1,55 @@
+function N = rightnull(A, alg, atol)
+% Compute the right nullspace of a matrix, such that A * N' = 0.
+%
+% Arguments
+% ---------
+% A : numeric
+% input matrix.
+%
+% alg : '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))).
+%
+% Returns
+% -------
+% N : numeric
+% orthonormal basis for the orthogonal complement of the support of the column space of A.
+
+arguments
+ A
+ alg = 'svd'
+ atol = []
+end
+
+[m, n] = size(A);
+
+switch alg
+ case 'svd'
+ [~, S, V] = svd(A, 0);
+
+ if m == 1
+ s = S(1);
+ else
+ s = diag(S);
+ end
+
+ if isempty(atol)
+ atol = max(m, n) * eps(max(s));
+ end
+
+ r = sum(s > atol);
+ N = V(:, r+1:n)';
+
+ case 'lq'
+ [Q, R] = qr(A.');
+
+ r = size(R, 2);
+ N = Q(:, r+1:n).';
+
+ otherwise
+ error('Invalid algorithm');
+end
+
+end
diff --git a/src/utility/linalg/rightorth.m b/src/utility/linalg/rightorth.m
new file mode 100644
index 0000000..ca2ac50
--- /dev/null
+++ b/src/utility/linalg/rightorth.m
@@ -0,0 +1,31 @@
+function [R, Q] = rightorth(A, alg)
+
+
+
+
+% TODO have a look at https://github.com/iwoodsawyer/factor
+
+switch alg
+ case 'svd'
+ [U, S, Q] = svd(A, 0);
+ R = U * S;
+ Q = Q';
+ return;
+
+ case 'polar'
+ newalg = 'polar';
+ case 'lq'
+ newalg = 'qr';
+ case 'lqpos'
+ newalg = 'qrpos';
+ case 'rq'
+ newalg = 'ql';
+ case 'rqpos'
+ newalg = 'qlpos';
+end
+
+[Q, R] = leftorth(A.', newalg);
+Q = Q.';
+R = R.';
+
+end
diff --git a/src/utility/linalg/tensorprod.m b/src/utility/linalg/tensorprod.m
new file mode 100644
index 0000000..679f77d
--- /dev/null
+++ b/src/utility/linalg/tensorprod.m
@@ -0,0 +1,65 @@
+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.
+%
+% C = tensorprod(A, B)
+% returns the outer product of tensors A and B. This is equivalent to the previous
+% syntax with dimA = dimB = [].
+%
+% C = tensorprod(_, NumDimensionsA=ndimsA)
+% optionally specifies the number of dimensions in tensor A in addition to combat the
+% removal of trailing singleton dimensions.
+
+arguments
+ A
+ B
+ dimA = []
+ dimB = []
+ cA = false
+ cB = false
+ 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
+
+szA = size(A, 1:options.NumDimensionsA);
+
+szB = size(B);
+szB = [szB ones(1, max(0, max(dimB) - length(szB)))];
+
+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
+
+C = reshape( ...
+ reshape(permute(A, [uncA dimA]), prod(szA(uncA)), prod(szA(dimA))) * ...
+ reshape(permute(B, [dimB uncB]), prod(szB(dimB)), prod(szB(uncB))), ...
+ szC);
+
+end
diff --git a/src/utility/linalg/tensortrace.m b/src/utility/linalg/tensortrace.m
new file mode 100644
index 0000000..103a340
--- /dev/null
+++ b/src/utility/linalg/tensortrace.m
@@ -0,0 +1,11 @@
+function [C, ic] = tensortrace(A, ia, ic)
+% tensortrace - Compute the (partial) trace of a tensor.
+% [C, ic] = tensortrace(A, ia)
+% traces over the indices that appear twice in ia.
+%
+% [C, ic] = tensortrace(A, ia, ic)
+% optionally specifies the output indices' order.
+
+error('Tensors:TBA', 'Traces are not implemented.');
+
+end
diff --git a/src/utility/permutations/invperm.m b/src/utility/permutations/invperm.m
new file mode 100644
index 0000000..a18dc13
--- /dev/null
+++ b/src/utility/permutations/invperm.m
@@ -0,0 +1,8 @@
+function invp = invperm(p)
+% invp - Compute the inverse permutation.
+% invp = invperm(p)
+% computes the permutation that satisfies invp(p) = 1:length(p).
+
+invp(p) = 1:length(p);
+
+end
diff --git a/src/utility/permutations/isperm.m b/src/utility/permutations/isperm.m
new file mode 100644
index 0000000..4a1a635
--- /dev/null
+++ b/src/utility/permutations/isperm.m
@@ -0,0 +1,8 @@
+function bool = isperm(p)
+% isperm - Check if a vector is a permutation.
+% bool = isperm(p)
+
+bool = all(sort(p) == 1:length(p));
+
+end
+
diff --git a/src/utility/permutations/perm2swap.m b/src/utility/permutations/perm2swap.m
new file mode 100644
index 0000000..4cd3075
--- /dev/null
+++ b/src/utility/permutations/perm2swap.m
@@ -0,0 +1,25 @@
+function s = perm2swap(p)
+% Convert a permutation into a sequence of swaps on neighbouring indices.
+%
+% Arguments
+% ---------
+% p : 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`.
+
+N = length(p);
+s = [];
+for k = 1:N - 1
+ s = [s flip(k:p(k) - 1)]; %#ok
+ p2 = p(k + 1:N);
+ p2(p2 < p(k)) = p2(p2 < p(k)) + 1;
+ p(k + 1:N) = p2;
+ p(k) = k;
+end
+
+end
diff --git a/src/utility/randc.m b/src/utility/randc.m
new file mode 100644
index 0000000..cb0bbdd
--- /dev/null
+++ b/src/utility/randc.m
@@ -0,0 +1,35 @@
+function R = randc(varargin)
+% Generate an array containing pseudorandom complex values, both real and imaginary part
+% drawn from the standard uniform distribution on the open interval (0, 1).
+%
+% Usage
+% -----
+% :code:`R = randc([m n])`
+%
+% :code:`R = randc(m, n, p, ...)`
+%
+% :code:`R = randc(..., classname)`
+%
+% :code:`R = randc(..., 'like', Y)`
+%
+% Arguments
+% ---------
+% m, n, p, ... : int
+% integers defining the size of the output array.
+%
+% classname : :class:`char`
+% datatype of the array, default :class:`double`.
+%
+% Y : numeric
+% create an array of the same class as Y.
+%
+% Returns
+% -------
+% R : numeric
+% complex pseudorandom values, real and imaginary part distributed from the uniform
+% distribution.
+
+R = complex(rand(varargin{:}), rand(varargin{:}));
+
+end
+
diff --git a/src/utility/randnc.m b/src/utility/randnc.m
new file mode 100644
index 0000000..5d884bc
--- /dev/null
+++ b/src/utility/randnc.m
@@ -0,0 +1,34 @@
+function R = randnc(varargin)
+% Generate an array containing pseudorandom complex values, both real and imaginary part
+% drawn from the standard normal distribution.
+%
+% Usage
+% -----
+% :code:`R = randnc([m n])`
+%
+% :code:`R = randnc(m, n, p, ...)`
+%
+% :code:`R = randnc(..., classname)`
+%
+% :code:`R = randnc(..., 'like', Y)`
+%
+% Arguments
+% ---------
+% m, n, p, ... : int
+% integers defining the size of the output array.
+%
+% classname : :class:`char`
+% datatype of the array, default :class:`double`.
+%
+% Y : numeric
+% create an array of the same class as Y.
+%
+% Returns
+% -------
+% R : numeric
+% complex pseudorandom values, real and imaginary part distributed from the normal
+% distribution.
+
+R = complex(randn(varargin{:}), randn(varargin{:}));
+
+end
diff --git a/src/utility/simulsort.m b/src/utility/simulsort.m
new file mode 100644
index 0000000..107354e
--- /dev/null
+++ b/src/utility/simulsort.m
@@ -0,0 +1,121 @@
+function [I, varargout] = simulsort(arrays, kwargs)
+% Simultaneous sorting of several input arrays.
+%
+% Sorts the elements such that equal elements of array{i} appear sorted by
+% array{i+1}.
+%
+% This is achieved by sorting from end to front, making use of the fact that SORT is stable
+% and thus will not mess up the order of later elements when earlier elements are equal.
+%
+% Usage
+% -----
+% :code:`[I, array1, array2, ...] = simulsort(array1, array2, ..., kwargs)`
+%
+% Arguments
+% ---------
+% array1, array2, ...
+% arrays of equal size that need to be sorted. These can be of any type that supports
+% SORT.
+%
+% Keyword Arguments
+% -----------------
+% Dimension : 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'
+% specify the sorting direction, defaults to 'ascend'.
+%
+% Returns
+% -------
+% I : int
+% permutation vector that brings the input arrays into sorted order.
+%
+% array1, array2, ...
+% sorted arrays.
+
+arguments (Repeating)
+ arrays
+end
+
+arguments
+ kwargs.Dimension = find(size(arrays{1}) ~= 1, 1)
+ kwargs.Direction {mustBeMember(kwargs.Direction, {'ascend', 'descend'})} = 'ascend'
+end
+
+
+%% Input validation
+sz = size(arrays{1});
+assert(length(sz) == 2, 'Not implemented for N-D arrays yet.');
+for i = 2:length(arrays)
+ assert(all(sz == size(arrays{i})), 'Input arrays should have equal size.');
+end
+
+
+%% Special cases
+if all(sz == 1)
+ varargout = arrays;
+ I = 1;
+ return
+end
+if any(sz == 0)
+ varargout = arrays;
+ I = [];
+ return
+end
+
+
+%% Sort last array
+[varargout{length(arrays)}, I] = sort(arrays{end}, kwargs.Dimension, kwargs.Direction);
+if kwargs.Dimension == 1
+ for k = length(arrays)-1:-1:1
+ for i = 1:size(I, 2)
+ varargout{k}(:, i) = arrays{k}(I(:, i), i);
+ end
+ end
+else
+ for k = length(arrays)-1:-1:1
+ for i = 1:size(I, 2)
+ varargout{k}(i, :) = arrays{k}(i, I(i, :));
+ end
+ end
+end
+
+
+%% sort other arrays
+for n = length(arrays)-1:-1:1
+ [varargout{n}, I_] = sort(varargout{n}, kwargs.Dimension, kwargs.Direction);
+
+ if kwargs.Dimension == 1
+ % permute arrays
+ for k = 1:length(arrays)
+ if k == n, continue; end
+ for i = 1:size(I, 2)
+ varargout{k}(:, i) = varargout{k}(I_(:, i), i);
+ end
+ end
+
+ % permute index
+ for i = 1:size(I, 2)
+ I(:, i) = I(I_(:, i), i);
+ end
+
+ else
+ % permute arrays
+ for k = 1:length(arrays)
+ if k == n, continue; end
+ for i = 1:size(I, 1)
+ varargout{k}(i, :) = varargout{k}(i, I_(i, :));
+ end
+ end
+
+ % permute index
+ for i = 1:size(I, 1)
+ I(i, :) = I(i, I_(:, i));
+ end
+ end
+end
+
+end
diff --git a/src/utility/simulsortrows.m b/src/utility/simulsortrows.m
new file mode 100644
index 0000000..dd72157
--- /dev/null
+++ b/src/utility/simulsortrows.m
@@ -0,0 +1,70 @@
+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}.
+%
+% 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)
+%
+% 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
+% vector of indices that specifies the columns used for sorting.
+%
+% Direction : '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
+% permutation vector that brings the input arrays into rowsorted order.
+%
+% array1, array2, ...
+% rowsorted arrays
+
+arguments (Repeating)
+ arrays
+end
+
+arguments
+ kwargs.Col = 1:size(arrays{1}, 2)
+ kwargs.Direction = 'ascend'
+end
+
+%% Input validation
+sz = size(arrays{1});
+assert(length(sz) == 2, 'Input should be matrices.');
+for i = 2:length(arrays)
+ assert(all(sz == size(arrays{i})), 'Input arrays should have equal sizes.');
+end
+
+
+%% Sort last array
+[varargout{length(arrays)}, I] = sortrows(arrays{end}, kwargs.Col, kwargs.Direction);
+for k = length(arrays)-1:-1:1
+ varargout{k} = arrays{k}(I, :);
+end
+
+
+%% Sort other arrays
+for n = length(arrays)-1:-1:1
+ [varargout{n}, I_] = sortrows(varargout{n}, kwargs.Col, kwargs.Direction);
+ for k = 1:length(arrays)
+ if k == n, continue; end
+ varargout{k} = varargout{k}(I_, :);
+ end
+ I = I(I_);
+end
+
+end
diff --git a/src/utility/swapvars.m b/src/utility/swapvars.m
new file mode 100644
index 0000000..f1c6352
--- /dev/null
+++ b/src/utility/swapvars.m
@@ -0,0 +1,7 @@
+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.
+
+end
+
diff --git a/src/utility/uninit/license.txt b/src/utility/uninit/license.txt
new file mode 100644
index 0000000..32b41c6
--- /dev/null
+++ b/src/utility/uninit/license.txt
@@ -0,0 +1,24 @@
+Copyright (c) 2011, James Tursa
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the distribution
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/utility/uninit/uninit.c b/src/utility/uninit/uninit.c
new file mode 100644
index 0000000..9b68613
--- /dev/null
+++ b/src/utility/uninit/uninit.c
@@ -0,0 +1,368 @@
+/*************************************************************************************
+ *
+ * MATLAB (R) is a trademark of The Mathworks (R) Corporation
+ *
+ * Function: uninit
+ * Filename: uninit.c
+ * Programmer: James Tursa
+ * Version: 1.00
+ * Date: May 03, 2011
+ * Copyright: (c) 2011 by James Tursa, All Rights Reserved
+ *
+ * This code uses the BSD License:
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the distribution
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * Building:
+ *
+ * UNINIT is typically self building. That is, the first time you call UNINIT,
+ * the uninit.m file recognizes that the mex routine needs to be compiled and
+ * then the compilation will happen automatically. UNINIT uses the undocumented
+ * MATLAB API function mxCreateUninitNumericMatrix. It has been tested in PC
+ * versions R2006b through R2011a, but may not work in future versions of MATLAB.
+ *
+ * Syntax (nearly identical to the ZEROS function)
+ *
+ * B = uninit
+ * B = uninit(n)
+ * B = uninit(m,n)
+ * B = uninit([m n])
+ * B = uninit(m,n,p,...)
+ * B = uninit([m n p ...])
+ * B = uninit(size(A))
+ * B = uninit(m, n,...,classname)
+ * B = uninit([m,n,...],classname)
+ * B = uninit(m, n,...,complexity)
+ * B = uninit([m,n,...],complexity)
+ * B = uninit(m, n,...,classname,complexity)
+ * B = uninit([m,n,...],classname,complexity)
+ *
+ * Description
+ *
+ * B = uninit
+ * Returns a 1-by-1 scalar uninitialized value.
+ *
+ * B = uninit(n)
+ * Returns an n-by-n matrix of uninitialized values. An error message
+ * appears if n is not a scalar.
+ *
+ * B = uninit(m,n) or B = uninit([m n])
+ * Returns an m-by-n matrix of uninitialized values.
+ *
+ * B = uninit(m,n,p,...) or B = uninit([m n p ...])
+ * Returns an m-by-n-by-p-by-... array of uninitialized values. The
+ * size inputs m, n, p, ... should be nonnegative integers. Negative
+ * integers are treated as 0.
+ *
+ * B = uninit(size(A))
+ * Returns an array the same size as A consisting of all uninitialized
+ * values.
+ *
+ * If any of the numeric size inputs are empty, they are taken to be 0.
+ *
+ * The optional classname argument can be used with any of the above.
+ * classname is a string specifying the data type of the output.
+ * classname can have the following values:
+ * 'double', 'single', 'int8', 'uint8', 'int16', 'uint16',
+ * 'int32', 'uint32', 'int64', 'uint64', 'logical', or 'char'.
+ * (Note: 'logical' and 'char' are not allowed in the ZEROS function)
+ * The default classname is 'double'.
+ *
+ * The optional complexity argument can be used with any of the above.
+ * complexity can be 'real' or 'complex', except that 'logical' and 'char'
+ * outputs cannot be complex. (this option not allowed in the ZEROS function)
+ * The default complexity is 'real'.
+ *
+ * UNINIT is very similar to the ZEROS function, except that UNINIT returns
+ * an uninitialized array instead of a zero-filled array. Thus, UNINIT is
+ * faster than the ZEROS function for large size arrays. Since the return
+ * variable is uninitialized, the user must take care to assign values to
+ * the elements before using them. UNINIT is useful for preallocation of an
+ * array where you know the elements will be assigned values before using them.
+ *
+ * Example
+ *
+ * x = uninit(2,3,'int8');
+ *
+ * Change Log:
+ * 2011/May/03 --> 1.00, Initial Release
+ *
+ ****************************************************************************/
+
+/* Includes ----------------------------------------------------------- */
+
+#include
+#include
+#include
+#include "mex.h"
+
+/* Macros ------------------------------------------------------------- */
+
+#ifndef MWSIZE_MAX
+#define mwSize int
+#endif
+
+/* Undocumented Function mxCreateUninitNumericMatrix ------------------ */
+
+mxArray *mxCreateUninitNumericMatrix(mwSize m, mwSize n, mxClassID classid,
+ mxComplexity ComplexFlag);
+
+/* myCreateUninitNumericArray ----------------------------------------- */
+/* */
+/* Creates uninitialized array using above function. Essentially, this */
+/* is a replacement for mxCreateUninitNumericArray, which is only */
+/* available for R2008b and later. For two dimensions just call the */
+/* mxCreateUninitNumericMatrix function directly. For more than two */
+/* dimensions, first multiply all the dimensions to get a single value, */
+/* pass that to mxCreateUninitNumericMatrix, then set the dimensions of */
+/* the result to the actual desired dimensions. */
+
+mxArray *myCreateUninitNumericArray(mwSize ndim, const mwSize *dims,
+ mxClassID classid, mxComplexity ComplexFlag)
+{
+ mxArray *mx;
+ mwSize i, m, n;
+
+ if( ndim <= 0 ) {
+ mx = mxCreateUninitNumericMatrix(0,0,classid,ComplexFlag);
+ } else if( ndim == 1 ) {
+ mx = mxCreateUninitNumericMatrix(dims[0],1,classid,ComplexFlag);
+ } else if( ndim == 2 ) {
+ mx = mxCreateUninitNumericMatrix(dims[0],dims[1],classid,ComplexFlag);
+ } else {
+ m = 1;
+ for( i=0; i 0.0 || dims[0] > 0 && d < 0.0 ) { /* Check for overflow */
+ mexErrMsgTxt("Maximum variable size allowed by the program is exceeded.");
+ }
+ if( dims[0] != d && warning1 ) {
+ warning1 = 0;
+ mexWarnMsgTxt("Size vector should be a row vector with integer elements.");
+ }
+ if( dims[0] < 0 ) dims[0] = 0;
+ dims[1] = dims[0];
+ } else {
+ dims = mxMalloc(ndim*sizeof(*dims));
+ dp = mxGetPr(mx);
+ for( i=0; i 0.0 || dims[i] > 0 && dp[i] < 0.0 ) { /* Check for overflow */
+ mexErrMsgTxt("Maximum variable size allowed by the program is exceeded.");
+ }
+ if( dims[i] != dp[i] && warning1 ) {
+ warning1 = 0;
+ mexWarnMsgTxt("Size vector should be a row vector with integer elements.");
+ }
+ if( dims[i] < 0 ) dims[i] = 0;
+ }
+ }
+ if( mx != prhs[k] ) {
+ mxDestroyArray(mx);
+ }
+
+/* If the number of numeric arguments is > 1, then each numeric argument */
+/* will contribute one number to the resulting dimensions. Empty or negative */
+/* arguments are taken to be 0. A warning is given for non-integral or non- */
+/* scalar arguments. */
+
+ } else {
+ ndim = n;
+ dims = mxMalloc(ndim*sizeof(*dims));
+ k = 0;
+ for( i=0; i 1 && warning2 ) {
+ warning2 = 0;
+ mexWarnMsgTxt("Input arguments must be scalar.");
+ }
+ d = mxGetScalar(prhs[i]);
+ dims[k] = (mwSize) d;
+ if( dims[k] < 0 && d > 0.0 || dims[k] > 0 && d < 0.0 ) { /* Check for overflow */
+ mexErrMsgTxt("Maximum variable size allowed by the program is exceeded.");
+ }
+ if( dims[k] != d && warning1 ) {
+ warning1 = 0;
+ mexWarnMsgTxt("Input arguments must be integer elements.");
+ }
+ if( dims[k] < 0 ) dims[k] = 0;
+ k++;
+ }
+ }
+ }
+ }
+
+/* Now process the char arguments to get the desired class and complexity. */
+/* The default is double real. Allow logical and char classes also, since */
+/* the mxCreateUninitNumericMatrix function seems to allow them OK. This, */
+/* and the complexity input, are extensions not available in zeros function. */
+
+ for( i=0; i 1 ) {
+ mexErrMsgTxt("Too many class inputs.");
+ }
+ if( gotcomplexity == 0 ) {
+ ComplexFlag = mxREAL;
+ } else if( gotcomplexity > 1 ) {
+ mexErrMsgTxt("Too many complexity inputs.");
+ }
+ if( ComplexFlag == mxCOMPLEX && (classid == mxCHAR_CLASS || classid == mxLOGICAL_CLASS) ) {
+ mexErrMsgTxt("Char and Logical class variables cannot be complex.");
+ }
+
+/* Create the uninitialized output array */
+
+ plhs[0] = myCreateUninitNumericArray(ndim, dims, classid, ComplexFlag);
+ mxFree(dims);
+
+}
diff --git a/src/utility/uninit/uninit.m b/src/utility/uninit/uninit.m
new file mode 100644
index 0000000..db1bd46
--- /dev/null
+++ b/src/utility/uninit/uninit.m
@@ -0,0 +1,152 @@
+% UNINIT returns an unitialized array of specified size and class.
+%*************************************************************************************
+%
+% MATLAB (R) is a trademark of The Mathworks (R) Corporation
+%
+% Function: uninit
+% Filename: uninit.c
+% Programmer: James Tursa
+% Version: 1.00
+% Date: May 03, 2011
+% Copyright: (c) 2011 by James Tursa, All Rights Reserved
+%
+% Change Log:
+% 2011/May/03 --> 1.00, Initial Release
+%
+% This code uses the BSD License:
+%
+% Redistribution and use in source and binary forms, with or without
+% modification, are permitted provided that the following conditions are
+% met:
+%
+% * Redistributions of source code must retain the above copyright
+% notice, this list of conditions and the following disclaimer.
+% * Redistributions in binary form must reproduce the above copyright
+% notice, this list of conditions and the following disclaimer in
+% the documentation and/or other materials provided with the distribution
+%
+% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+% POSSIBILITY OF SUCH DAMAGE.
+%
+% Building:
+%
+% UNINIT is typically self building. That is, the first time you call UNINIT,
+% the uninit.m file recognizes that the mex routine needs to be compiled and
+% then the compilation will happen automatically. UNINIT uses the undocumented
+% MATLAB API function mxCreateUninitNumericMatrix. It has been tested in PC
+% versions R2008b through R2011a, but may not work in future versions of MATLAB.
+%
+% Syntax (nearly identical to the ZEROS function)
+%
+% B = uninit
+% B = uninit(n)
+% B = uninit(m,n)
+% B = uninit([m n])
+% B = uninit(m,n,p,...)
+% B = uninit([m n p ...])
+% B = uninit(size(A))
+% B = uninit(m, n,...,classname)
+% B = uninit([m,n,...],classname)
+% B = uninit(m, n,...,complexity)
+% B = uninit([m,n,...],complexity)
+% B = uninit(m, n,...,classname,complexity)
+% B = uninit([m,n,...],classname,complexity)
+%
+% Description
+%
+% B = uninit
+% Returns a 1-by-1 scalar uninitialized value.
+%
+% B = uninit(n)
+% Returns an n-by-n matrix of uninitialized values. An error message
+% appears if n is not a scalar.
+%
+% B = uninit(m,n) or B = uninit([m n])
+% Returns an m-by-n matrix of uninitialized values.
+%
+% B = uninit(m,n,p,...) or B = uninit([m n p ...])
+% Returns an m-by-n-by-p-by-... array of uninitialized values. The
+% size inputs m, n, p, ... should be nonnegative integers. Negative
+% integers are treated as 0.
+%
+% B = uninit(size(A))
+% Returns an array the same size as A consisting of all uninitialized
+% values.
+%
+% If any of the numeric size inputs are empty, they are taken to be 0.
+%
+% The optional classname argument can be used with any of the above.
+% classname is a string specifying the data type of the output.
+% classname can have the following values:
+% 'double', 'single', 'int8', 'uint8', 'int16', 'uint16',
+% 'int32', 'uint32', 'int64', 'uint64', 'logical', or 'char'.
+% (Note: 'logical' and 'char' are not allowed in the ZEROS function)
+% The default classname is 'double'.
+%
+% The optional complexity argument can be used with any of the above.
+% complexity can be 'real' or 'complex', except that 'logical' and 'char'
+% outputs cannot be complex. (this option not allowed in the ZEROS function)
+% The default complexity is 'real'.
+%
+% UNINIT is very similar to the ZEROS function, except that UNINIT returns
+% an uninitialized array instead of a zero-filled array. Thus, UNINIT is
+% faster than the ZEROS function for large size arrays. Since the return
+% variable is uninitialized, the user must take care to assign values to
+% the elements before using them. UNINIT is useful for preallocation of an
+% array where you know the elements will be assigned values before using them.
+%
+% Example
+%
+% x = uninit(2,3,'int8');
+%
+%***************************************************************************/
+
+function varargout = uninit(varargin)
+fname = 'uninit';
+disp(' ');
+disp(['Detected that the mex routine for ' fname ' is not yet built.']);
+disp('Attempting to do so now ...');
+disp(' ');
+try
+ mname = mfilename('fullpath');
+catch
+ mname = fname;
+end
+cname = [mname '.c'];
+if( isempty(dir(cname)) )
+ disp(['Cannot find the file ' fname '.c in the same directory as the']);
+ disp(['file ' fname '.m. Please ensure that they are in the same']);
+ disp('directory and try again. The following file was not found:');
+ disp(' ');
+ disp(cname);
+ disp(' ');
+ error(['Unable to compile ' fname '.c']);
+else
+ disp(['Found file ' fname '.c in ' cname]);
+ disp(' ');
+ disp('Now attempting to compile ...');
+ disp('(If prompted, please press the Enter key and then select any C/C++');
+ disp('compiler that is available, such as lcc.)');
+ disp(' ');
+ disp(['mex(''' cname ''',''-output'',''',mname,''')']);
+ disp(' ');
+ try
+ mex(cname,'-output',mname);
+ disp([ fname ' mex build completed ... you may now use ' fname '.']);
+ disp(' ');
+ catch
+ disp(' ');
+ error(['Unable to compile ' fname ' ... Contact author.']);
+ end
+ [varargout{1:nargout}] = uninit(varargin{:});
+end
+end
diff --git a/src/utility/uninit/uninit.mexa64 b/src/utility/uninit/uninit.mexa64
new file mode 100755
index 0000000..c047c2d
Binary files /dev/null and b/src/utility/uninit/uninit.mexa64 differ
diff --git a/src/utility/validations/mustBeEqualLength.m b/src/utility/validations/mustBeEqualLength.m
new file mode 100644
index 0000000..6359ddd
--- /dev/null
+++ b/src/utility/validations/mustBeEqualLength.m
@@ -0,0 +1,13 @@
+function mustBeEqualLength(a, b)
+% mustBeEqualLength - Validate that the inputs are of equal length.
+% mustBeEqualLength(a, b) throws an error if length(a) ~= length(b)
+%
+% Class support:
+% All classes that define these methods:
+% length
+
+if length(a) ~= length(b)
+ throwAsCaller(createValidatorException('TensorTrack:validators:mustBeEqualLength'));
+end
+
+end
diff --git a/src/utility/validations/mustBeNconstyle.m b/src/utility/validations/mustBeNconstyle.m
new file mode 100644
index 0000000..e69de29
diff --git a/src/utility/validations/mustBeSorted.m b/src/utility/validations/mustBeSorted.m
new file mode 100644
index 0000000..492014e
--- /dev/null
+++ b/src/utility/validations/mustBeSorted.m
@@ -0,0 +1,13 @@
+function mustBeSorted(A)
+% mustBeSorted - Validate that value is sorted.
+% mustBeSorted(A) throws an error if A is not sorted.
+%
+% Class support:
+% All classes that define these methods:
+% issorted
+
+if ~issorted(A)
+ throwAsCaller(createValidatorException('TensorTrack:validators:mustBeSorted'));
+end
+
+end
diff --git a/src/utility/validations/mustBeUnique.m b/src/utility/validations/mustBeUnique.m
new file mode 100644
index 0000000..3a11e51
--- /dev/null
+++ b/src/utility/validations/mustBeUnique.m
@@ -0,0 +1,14 @@
+function mustBeUnique(A)
+% mustBeUnique - Validate that there are no repeated values.
+% mustBeUnique(A) throws an error if A contains repeated values.
+%
+% Class support:
+% All classes that define these methods:
+% unique
+% length
+
+if length(A) ~= length(unique(A))
+ throwAsCaller(createValidatorException('validators:mustBeUnique'));
+end
+
+end
diff --git a/src/utility/wigner/Wigner3j.m b/src/utility/wigner/Wigner3j.m
new file mode 100644
index 0000000..b2373fd
--- /dev/null
+++ b/src/utility/wigner/Wigner3j.m
@@ -0,0 +1,887 @@
+function wig = Wigner3j(j1,j2,j3,m1,m2,m3,ifs,ifcb)
+% Computes the Wigner $3j$ symbols
+%
+% .. math::
+%
+% W = \begin{pmatrix}
+% j_1 & j_2 & j_3 \\
+% m_1 & m_2 & m_3
+% \end{pmatrix}
+%
+% or the Clebsch-Gordan coefficient
+%
+% .. math::
+%
+% W = \left\langle j_1,m_1,j_2,m_2 \middle| j_3,m_3 \right\rangle
+%
+% using the Racah formula.
+%
+% Usage
+% -----
+% :code:`W = Wigner3j(j1,j2,j3,m1,m2,m3,ifs,ifcb)`
+% :code:`Wigner3j(j1,j2,j3,m1,m2,m3)`
+%
+% Arguments
+% ---------
+% j1, j2, j3
+% set of three angular momenta, must be arrays of the same size
+% m1, m2, m3
+% set of three angular momentum projections, must be arrays of the same size as the
+% angular momenta
+%
+% Optional Arguments
+% ------------------
+% ifs
+% the switch of the computational methods;
+% although the central part of all the computations is based on the Racah formula, some
+% details differ:
+%
+% - 0: the symbolic (presumably accurate) computation with the double-precision output
+% - -1: the same symbolic computation as ifs=0 but with the symbolic output (accurate
+% square root/rational-type equation, simplified)
+% - -2: the same as :code:`ifs=-1` but without a final simplification of the symbolic
+% expression, which can be time-consuming sometimes
+% - 1: (default, recommended), 2, 3 - numeric double-precision algorithms (see Notes below)
+% - 4: the symbolic computation with the double precision output, but cached.
+%
+% all other input values of ifs are set to the closest from the above list.
+%
+% ifcb
+% if exists and is true, switches to computing the Clebsch-Gordan coefficients instead of
+% the $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$.
+%
+%
+% Notes
+% -----
+% most of the estimates below is based on extensive numerical tests within a range of the
+% quantum nubers <=1000
+%
+% a. the numeric algorithms (:code:`ifs>0`) are usually much faster than the symbolic
+% algorithm (:code:`ifs<=0`)
+% b. the accuracy of the symbolic algorithm remains the uttermost possible
+% c. the accuracy of the numeric algorithms can worsen for big quantum numbers
+% 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
+% 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
+% g. the algorithm :code:`ifs=1` ensures a reasonably good accuracy; the worst registered
+% case was :code:`Wigner3j(56,59,51,-2,14,-12,1)=-0.00019235064` (exact
+% :code:`Wigner3j(56,59,51,-2,14,-12,0)=-0.00019235487`)
+% with the relative inaccuracy of 2.2e-5
+% (occuring before switching to the symbolic regime)
+% h. the algorithm :code:`ifs=2` (comparing to :code:`ifs=1`) proceeds in a wider range of
+% quantum numbers numerically before switching to the symbolic regime (i.e., can work
+% faster with big quantum numbers) but with somewhat lower precision; in our experimental
+% probing we registered the cases with the worst relative inaccuracy of 0.13 for
+% :code:`Wigner3j(80.5,85,68.5,-18.5,-15,33.5,2)=-2.781e-06` (exact
+% :code:`Wigner3j(80.5,85,68.5,-18.5,-15,33.5,0)=-2.4637e-06)` and abolute inaccuracy of
+% 0.0001 for :code:`Wigner3j(82.5,67,90.5,4.5,1,-5.5,2)=0.0045102` (exact
+% 0.0046133=0.0046133); in the range of the numeric calculations of the algorithm
+% :code:`ifs=1`, the algorithm :code:`ifs=2` provides the same characteristic accuracy as
+% :code:`ifs=1`;
+% i. the algorithm :code:`ifs=3` (comparing to :code:`ifs=2`) proceeds in a wider range of
+% quantum numbers numerically before switching to the symbolic regime (i.e., can work
+% faster with big quantum numbers) but for some combinations of big quantum numbers,
+% completely wrong results are observed: e.g.,
+% :code:`Wigner3j(465.5,488.5,498,13.5,-90.5,77,3)`;
+% :code:`Wigner3j(95,96,99,-17,1,16,3)`; thereby, we do not recommend using the algorithm
+% :code:`ifs=3` at all, and only keep it here in view of the completeness.
+%
+% References
+% ----------
+% 1. https://en.wikipedia.org/wiki/3-j_symbol
+% 2. https://en.wikipedia.org/wiki/Clebsch-Gordan_coefficients
+% 3. `Zare, R. N., & Harter, W. G. (1988). Angular momentum: understanding spatial aspects
+% in chemistry and physics. New York, 120.
+% `_
+
+
+
+%%
+% general check, preprocessing
+NJ = min([numel(j1) numel(j2) numel(j3) numel(m1) numel(m2) numel(m3)]);
+Nj1S=size(j1);
+j1 = double(reshape(j1(1:NJ),NJ,1));
+j2 = double(reshape(j2(1:NJ),NJ,1));
+j3 = double(reshape(j3(1:NJ),NJ,1));
+m1 = double(reshape(m1(1:NJ),NJ,1));
+m2 = double(reshape(m2(1:NJ),NJ,1));
+m3 = double(reshape(m3(1:NJ),NJ,1));
+
+if nargin<7 || isempty(ifs) || ~isnumeric(ifs) && ~islogical(ifs) || ifs(1)>0 && ifs(1)<=1.5 % choose the algorithm
+ ifs = 1; % default
+elseif ifs(1)>0
+ ifs = min(round(ifs(1)),4);
+else
+ ifs=max(round(ifs(1)),-2);
+end
+
+ifcb = nargin>7 && ~isempty(ifcb) && ifcb(1); % if the Clebsch-Gordan coefficient instead of the 3j-symbols are needed?
+if ifcb
+ m3 = -m3;
+ M3=m3;
+ J1=j1;
+ J2=j2;
+ J3=j3;
+end
+
+%% Cached results
+persistent cache Jmax visited
+if ifs == 4 && isempty(cache)
+ Jmax = 30;
+ filename = sprintf('Wigner3j_Jmax=%d.mat', Jmax);
+ if exist(filename, 'file')
+ load(filename, 'cache');
+ else
+ fprintf('No cache with name (%s) on the path.\n', filename);
+ userfile = uiputfile('*.mat', 'Select a cache file', filename);
+ if isequal(userfile, 0)
+ fprintf('Generating cache for Wigner3j, this may take some time:\n');
+ tic;
+ cache = zeros(1000,0);
+ visited = false;
+ for ja = 0:0.5:Jmax
+ for jb = 0:0.5:Jmax
+ for jc = abs(ja-jb):min(Jmax, ja+jb)
+ for ma = -ja:ja
+ for mb = -jb:jb
+ mc = -ma-mb;
+ if abs(mc) > jc, continue; end
+ [key, sign] = WignerCache(ja, jb, jc, ma, mb, mc);
+ if key > length(cache) || ~visited(key)
+ cache(key) = (sign)^(ja+jb+jc) * ...
+ Wigner3j(ja, jb, jc, ma, mb, mc, 0, false);
+ visited(key) = true;
+ end
+ end
+ end
+ end
+ end
+ end
+ cache = sparse(cache(:));
+ toc;
+ userfile = uiputfile('*.mat', 'Save cached values', filename);
+ if ~isequal(userfile, 0)
+ save(userfile, 'cache');
+ end
+ else
+ load(userfile, 'cache');
+ end
+ end
+end
+
+if ifs == 4
+ assert(all(j1 <= Jmax) && all(j2 <= Jmax) && all(j3 <= Jmax), ...
+ 'tensors:ValueError', 'quantum numbers out of range for cache.');
+ [keys, signs] = arrayfun(@WignerCache, j1, j2, j3, m1, m2, m3);
+ wig = (signs) .^ (j1+j2+j3) .* full(cache(keys));
+ if ifcb
+ wig = wig .* sqrt(2*J3+1) .* (-1).^(J1-J2-M3);
+ end
+ if NJ>1 && prod(Nj1S)==NJ
+ wig = reshape(wig,Nj1S);
+ end
+ return
+end
+
+
+
+NK = if3jc(j1,j2,j3) & (m1+m2+m3==0) & ( j1 - m1 == floor ( j1 - m1 ) ) & ( j2 - m2 == floor ( j2 - m2 ) ) & ( j3 - m3 == floor ( j3 - m3 ) ) & (abs(m1) <= j1) & (abs(m2) <= j2) & (abs(m3) <= j3);
+if all(~NK) % all the results are zero based on the selection rules
+ if ifs>=0
+ wig=zeros(NJ,1);
+ else
+ wig=sym(zeros(NJ,1));
+ end
+ return;
+end
+
+if ifs>0
+ wig=zeros(NJ,1);
+else
+ wig=sym(zeros(NJ,1));
+end
+
+sp = ones(NJ,1); % to keep the signs produced by the permutations
+
+for k=1:NJ % permute the elements of every set of quantum numbers into the standard (ascending) order
+ jj = [j1(k) ; j2(k) ; j3(k)];
+ mm = [m1(k) ; m2(k) ; m3(k)];
+ [ind,a,b] = ArranA(jj,abs(mm));
+ j1(k) = a(1);
+ j2(k) = a(2);
+ j3(k) = a(3);
+ m1(k) = b(1);
+ if mm(ind(1))<0 || ( ~mm(ind(1)) && ( mm(ind(2))<0 || (~mm(ind(2)) && mm(ind(3))<0) ) )
+ m2(k) = -mm(ind(2));
+ m3(k) = -mm(ind(3));
+ if mod(j1(k)+j2(k)+j3(k),2) && ~sper(ind)
+ sp(k) = -1;
+ end
+ else
+ m2(k) = mm(ind(2));
+ m3(k) = mm(ind(3));
+ if mod(j1(k)+j2(k)+j3(k),2) && sper(ind)
+ sp(k) = -1;
+ end
+ end
+end
+
+if NJ>1 % arrange the sets into the standard (ascending) order
+ [ind0,j1,j2,j3,m1,m2,m3] = ...
+ ArranA ( j1,j2,j3,m1,m2,m3);
+ NK = NK(ind0);
+ sp = sp(ind0);
+end
+
+kk = find ( mod(j1+j2+j3,2) & (m1==0 & m2==0 & m3==0 | j1==j2 & m1==m2 | j2==j3 & m2==m3) ); % find some obviously zero (due to symmetry properties) coefficients
+if ~isempty(kk)
+ NK(kk)=0; % exclude the obviously zero coefficients
+end
+
+kk = find ( j1(1:NJ-1)==j1(2:NJ) & j2(1:NJ-1)==j2(2:NJ) & j3(1:NJ-1)==j3(2:NJ) & m1(1:NJ-1)==m1(2:NJ) & m2(1:NJ-1)==m2(2:NJ) & m3(1:NJ-1)==m3(2:NJ) ); % find duplicates
+if ~isempty(kk)
+ NK(kk)=0; % exclude duplicates in order to avoid repetitive computations
+end
+
+NKs=[]; % for pointing to erronious numeric results in order to switch them to the symbolic computations
+%%
+% special cases - faster and more accurate computation than the general case below
+
+if any(NK & j1<=2) % the range of the special cases currently included
+
+ % case j = 0
+ kp = find(NK & j1==0);
+ if ~isempty(kp)
+ if ifs>0
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp)) ./ sqrt(2*j2(kp)+1);
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp)) ./ sqrt(sym(2*j2(kp)+1));
+ end
+ NK(kp)=0;
+ end
+ % case j = 1/2
+ kp = find(NK & j1==1/2);
+ if ~isempty(kp)
+ if ifs>0
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)-1/2) .* sqrt( (j2(kp)-m3(kp)+0.5) ./ (2*j2(kp)+2) ./ (2*j2(kp)+1) );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)-1/2) .* sqrt( sym((j2(kp)-m3(kp)+1/2) ./ (2*j2(kp)+2) ./ (2*j2(kp)+1)) );
+ end
+ NK(kp)=0;
+ end
+ % case j = 1
+ kp = find(NK & j1==1 & j2==j3 & m1==0);
+ if ~isempty(kp)
+ if ifs>0
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp)) .* (2*m2(kp)) ./ sqrt( (2*j2(kp)+2) .* (2*j2(kp)+1) .* (2*j2(kp)) );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp)) .* sym(2*m2(kp)) ./ sqrt( sym( (2*j2(kp)+2) .* (2*j2(kp)+1) .* (2*j2(kp)) ) );
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==1 & m1==0); % & j2~=j3
+ if ~isempty(kp)
+ if ifs>0
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp)-1) .* sqrt( 2*(j2(kp)+m2(kp)+1) .* (j2(kp)-m2(kp)+1) ./ (2*j2(kp)+3) ./ (2*j2(kp)+2) ./ (2*j2(kp)+1) );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp)-1) .* sqrt( sym ( 2*(j2(kp)+m2(kp)+1) .* (j2(kp)-m2(kp)+1) ./ (2*j2(kp)+3) ./ (2*j2(kp)+2) ./ (2*j2(kp)+1) ) );
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==1 & j2==j3 & m1==1);
+ if ~isempty(kp)
+ if ifs>0
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp)) .* sqrt( 2*(j2(kp)-m2(kp)) .* (j2(kp)+m2(kp)+1) ./ (2*j2(kp)+2) ./ (2*j2(kp)+1) ./ (2*j2(kp)) );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp)) .* sqrt( sym( 2*(j2(kp)-m2(kp)) .* (j2(kp)+m2(kp)+1) ./ (2*j2(kp)+2) ./ (2*j2(kp)+1) ./ (2*j2(kp)) ) );
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==1 & m1==1); % & j2~=j3
+ if ~isempty(kp)
+ if ifs>0
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)-1) .* sqrt( (j2(kp)-m3(kp)) .* (j2(kp)-m3(kp)+1) ./ (2*j2(kp)+3) ./ (2*j2(kp)+2) ./ (2*j2(kp)+1) );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)-1) .* sqrt( sym ( (j2(kp)-m3(kp)) .* (j2(kp)-m3(kp)+1) ./ (2*j2(kp)+3) ./ (2*j2(kp)+2) ./ (2*j2(kp)+1) ) );
+ end
+ NK(kp)=0;
+ end
+ % case j = 3/2
+ kp = find(NK & j1==3/2 & j3-j2==1/2 & m1==1/2);
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+3;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)+1/2)...
+ .* (j2(kp)+3*m3(kp)+3/2) .* sqrt(j2(kp)-m3(kp)+1/2)...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+3);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)+1/2)...
+ .* sym(j2(kp)+3*m3(kp)+3/2) .* sqrt(sym(j2(kp)-m3(kp)+1/2))...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==3/2 & m1==1/2); % & j3-j2==3/2
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+4;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)+1/2)...
+ .* sqrt( 3 * (j2(kp)-m3(kp)+1/2) .* (j2(kp)-m3(kp)+3/2) .* (j2(kp)+m3(kp)+3/2) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+4);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)+1/2)...
+ .* sqrt( sym( 3 * (j2(kp)-m3(kp)+1/2) .* (j2(kp)-m3(kp)+3/2) .* (j2(kp)+m3(kp)+3/2) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==3/2 & j3-j2==1/2 & m1==3/2);
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+3;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)+1/2)...
+ .* sqrt( 3 * (j2(kp)-m3(kp)-1/2) .* (j2(kp)-m3(kp)+1/2) .* (j2(kp)+m3(kp)+3/2) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+3);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)+1/2)...
+ .* sqrt( sym( 3 * (j2(kp)-m3(kp)-1/2) .* (j2(kp)-m3(kp)+1/2) .* (j2(kp)+m3(kp)+3/2) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==3/2 & m1==3/2); % & j3-j2==3/2
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+4;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)+1/2)...
+ .* sqrt( (j2(kp)-m3(kp)-1/2) .* (j2(kp)-m3(kp)+1/2) .* (j2(kp)-m3(kp)+3/2) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+4);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp)+1/2)...
+ .* sqrt( sym( (j2(kp)-m3(kp)-1/2) .* (j2(kp)-m3(kp)+1/2) .* (j2(kp)-m3(kp)+3/2) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ % case j = 2, m = 0
+ kp = find(NK & j1==2 & j3==j2 & m1==0);
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+3;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp))...
+ .* 2.*(3.*m2(kp).^2-j2(kp).*(j2(kp)+1))...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+3);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp))...
+ .* sym( 2*(3*m2(kp).^2-j2(kp).*(j2(kp)+1)) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==1 & m1==0);
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+4;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp))...
+ .* 2.*m3(kp) .* sqrt( 6 .* (j2(kp)+m2(kp)+1) .* (j2(kp)-m2(kp)+1) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2.*j2(kp)+4);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp))...
+ .* sym(2.*m3(kp)) .* sqrt( sym( 6 .* (j2(kp)+m2(kp)+1) .* (j2(kp)-m2(kp)+1) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & m1==0); % & j3-j2==2
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+5;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp))...
+ .* sqrt( 6 .* (j2(kp)+m3(kp)+2) .* (j2(kp)-m3(kp)+2) .* (j2(kp)+m3(kp)+1) .* (j2(kp)-m3(kp)+1) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+5);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp))...
+ .* sqrt( sym( 6 * (j2(kp)+m3(kp)+2) .* (j2(kp)-m3(kp)+2) .* (j2(kp)+m3(kp)+1) .* (j2(kp)-m3(kp)+1) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ % case j = 2, m = 1
+ kp = find(NK & j1==2 & j3==j2 & m1==1);
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+3;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp))...
+ .* (2.*m2(kp)+1) .* sqrt( 6 .* (j2(kp)+m2(kp)+1) .* (j2(kp)-m2(kp)) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+3);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp))...
+ .* sym(2*m2(kp)+1) .* sqrt( sym( 6 * (j2(kp)+m2(kp)+1) .* (j2(kp)-m2(kp)) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==1 & m1==1);
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+4;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp))...
+ .* 2.*(j2(kp)+2.*m3(kp)+2) .* sqrt( (j2(kp)-m3(kp)+1) .* (j2(kp)-m3(kp)) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+4);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp))...
+ .* sym(2*(j2(kp)+2*m3(kp)+2)) .* sqrt( sym( (j2(kp)-m3(kp)+1) .* (j2(kp)-m3(kp)) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & m1==1); % & j3-j2==2
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+5;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp))...
+ .* sqrt( 4 * (j2(kp)+m3(kp)+2) .* (j2(kp)-m3(kp)+2) .* (j2(kp)-m3(kp)+1) .* (j2(kp)-m3(kp)) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+5);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp))...
+ .* sqrt( sym( 4 * (j2(kp)+m3(kp)+2) .* (j2(kp)-m3(kp)+2) .* (j2(kp)-m3(kp)+1) .* (j2(kp)-m3(kp)) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ % case j = 2, m = 2
+ kp = find(NK & j1==2 & j3==j2 & m1==2);
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+3;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp))...
+ .* sqrt( 6 * (j2(kp)-m2(kp)-1) .* (j2(kp)-m2(kp)) .* (j2(kp)+m2(kp)+1) .* (j2(kp)+m2(kp)+2) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+3);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)-m2(kp))...
+ .* sqrt( sym( 6 * (j2(kp)-m2(kp)-1) .* (j2(kp)-m2(kp)) .* (j2(kp)+m2(kp)+1) .* (j2(kp)+m2(kp)+2) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==1 & m1==2);
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+4;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp))...
+ .* sqrt( 4 * (j2(kp)-m3(kp)-1) .* (j2(kp)-m3(kp)) .* (j2(kp)-m3(kp)+1) .* (j2(kp)+m3(kp)+2) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+4);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp))...
+ .* sqrt( sym( 4 * (j2(kp)-m3(kp)-1) .* (j2(kp)-m3(kp)) .* (j2(kp)-m3(kp)+1) .* (j2(kp)+m3(kp)+2) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & m1==2); % & j3-j2==2
+ if ~isempty(kp)
+ if ifs>0
+ A = 2*j2(kp)+5;
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp))...
+ .* sqrt( (j2(kp)-m3(kp)-1) .* (j2(kp)-m3(kp)) .* (j2(kp)-m3(kp)+1) .* (j2(kp)-m3(kp)+2) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ A = sym(2*j2(kp)+5);
+ wig(kp) = sp(kp) .* (-1).^(j2(kp)+m3(kp))...
+ .* sqrt( sym( (j2(kp)-m3(kp)-1) .* (j2(kp)-m3(kp)) .* (j2(kp)-m3(kp)+1) .* (j2(kp)-m3(kp)+2) ) )...
+ ./ sqrt( A .* (A-1) .* (A-2) .* (A-3) .* (A-4) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+
+end
+% If other exact equation are known for some other partial cases, they can be included in the same manner
+
+%%
+% main computation --- general case
+
+NK = find(NK);
+
+if ~isempty(NK)
+
+ if ifs>0 % numeric computations
+ C=zeros(length(NK),1);
+ if ifs==3
+ for k=length(NK):-1:1
+ C(k) = sum(log(max(2,(-j1(NK(k))+j2(NK(k))+j3(NK(k))+1)):(j1(NK(k))+j2(NK(k))+j3(NK(k))+1)));
+ if isinf(C(k)) || isnan(C(k))
+ NKs = sort([NKs;NK(k)]);
+ NK(k) = [];
+ C(k)=[];
+ else
+ C(k)= (...
+ 2*sum( log( 2:( j1(NK(k))-abs(m1(NK(k))) ) ) )...
+ + 2*sum( log( 2:( j2(NK(k))-abs(m2(NK(k))) ) ) )...
+ + 2*sum( log( 2:( j3(NK(k))-abs(m3(NK(k))) ) ) )...
+ + sum( log( (j1(NK(k))-abs(m1(NK(k)))+1):(j1(NK(k))+abs(m1(NK(k)))) ) )...
+ + sum( log( (j2(NK(k))-abs(m2(NK(k)))+1):(j2(NK(k))+abs(m2(NK(k)))) ) )...
+ + sum( log( (j3(NK(k))-abs(m3(NK(k)))+1):(j3(NK(k))+abs(m3(NK(k)))) ) )...
+ - C(k) + (...
+ sum( log( 2:(j1(NK(k))+j2(NK(k))-j3(NK(k))) ) ) + sum( log( 2:(j1(NK(k))-j2(NK(k))+j3(NK(k))) ) )...
+ ) )/2;
+ if isinf(C(k)) || isnan(C(k))
+ NKs = sort([NKs;NK(k)]);
+ NK(k) = [];
+ C(k)=[];
+ else
+ end
+ end
+ end
+ k0 = find(isinf(C) | isnan(C));
+ if ~isempty(k0)
+ NKs = sort([NKs;NK(k0)]);
+ NK(k0) = [];
+ C(k0)=[];
+ end
+ else
+ if ifs==2
+ for k=1:length(NK)
+ C(k) = prod((-j1(NK(k))+j2(NK(k))+j3(NK(k))+1):(j1(NK(k))+j2(NK(k))+j3(NK(k))+1));
+ end
+ k0 = find(isinf(C) | isnan(C));
+ if ~isempty(k0)
+ NKs = sort([NKs;NK(k0)]);
+ NK(k0) = [];
+ C(k0)=[];
+ end
+ else % ifs==1 --- the default recommended case
+ C = factorial(j1(NK)+j2(NK)+j3(NK)+1);
+ k0 = find(isinf(C) | isnan(C));
+ if ~isempty(k0)
+ NKs = sort([NKs;NK(k0)]);
+ NK(k0) = [];
+ C(k0)=[];
+ end
+ if ~isempty(NK)
+ C = C ./ factorial(-j1(NK)+j2(NK)+j3(NK));
+ end
+ end
+ if ~isempty(NK)
+ C = (-1).^(j1(NK)-j2(NK)-m3(NK)) .* sp(NK) ...
+ .* sqrt( factorial(j1(NK)-j2(NK)+j3(NK)) ./ C )...
+ .* sqrt( factorial(j1(NK)+m1(NK)) ) .* sqrt( factorial(j1(NK)-m1(NK)) )...
+ .* sqrt( factorial(j2(NK)+m2(NK)) ) .* sqrt( factorial(j2(NK)-m2(NK)) )...
+ .* sqrt( factorial(j3(NK)+m3(NK)) ) .* sqrt( factorial(j3(NK)-m3(NK)) )...
+ .* sqrt( factorial(j1(NK)+j2(NK)-j3(NK)) )...
+ ;
+ k0=find(isinf(C) | isnan(C));
+ if ~isempty(k0)
+ C(k0)=[];
+ NKs = sort([NKs;NK(k0)]);
+ NK(k0)=[];
+ end
+ end
+ end
+ else % symbolic computations
+ C = (-1).^(j1(NK)-j2(NK)-m3(NK)) .* sp(NK) .* sqrt ( ...
+ factorial(sym(-j1(NK)+j2(NK)+j3(NK)))...
+ ./ factorial(sym(j1(NK)+j2(NK)+j3(NK)+1))...
+ .* factorial(sym(j1(NK)-j2(NK)+j3(NK)))...
+ .* factorial(sym(j1(NK)+j2(NK)-j3(NK)))...
+ .* factorial(sym(j3(NK)+m3(NK)))...
+ .* factorial(sym(j3(NK)-m3(NK)))...
+ .* factorial(sym(j2(NK)+m2(NK)))...
+ .* factorial(sym(j2(NK)-m2(NK)))...
+ .* factorial(sym(j1(NK)+m1(NK)))...
+ .* factorial(sym(j1(NK)-m1(NK)))...
+ );
+ end
+
+ if ~isempty(NK) % could be emptied due to Infs in the double-precision mode
+ t1 = j2(NK) - m1(NK) - j3(NK);
+ t2 = j1(NK) + m2(NK) - j3(NK);
+ t3 = j1(NK) + j2(NK) - j3(NK);
+ t4 = j1(NK) - m1(NK);
+ t5 = j2(NK) + m2(NK);
+
+ tmin = max( [zeros(size(t1)), t1, t2], [], 2 );
+ tmax = min( [t3, t4, t5], [], 2 );
+
+ for k=numel(NK):-1:1 % t below can be of different lengths for the different k! Cannot apply the element-wise operations in this sense!
+
+ kN = NK(k);
+
+ t = tmin(k):tmax(k);
+
+ if ifs>0
+ if ifs==3
+ wig(kN) = sp(kN) * (-1).^(j1(kN)-j2(kN)-m3(kN)+t) * exp(C(k) - (...
+ gammaln(t-t1(k)+1) + gammaln(t-t2(k)+1) + gammaln(t+1)...
+ + gammaln(t3(k)-t+1) + gammaln(t4(k)-t+1) + gammaln(t5(k)-t+1)...
+ ))';
+ else
+ wig(kN) = (-1).^t * ( C(k) ...
+ ./ factorial(t)...
+ ./ factorial(t-t1(k))...
+ ./ factorial(t-t2(k))...
+ ./ factorial(t3(k)-t)...
+ ./ factorial(t4(k)-t)...
+ ./ factorial(t5(k)-t)...
+ )';
+ end
+ else
+ wig(kN) = (-1).^t * ( C(k) ...
+ ./ factorial(sym(t))...
+ ./ factorial(sym(t-t1(k)))...
+ ./ factorial(sym(t-t2(k)))...
+ ./ factorial(sym(t3(k)-t))...
+ ./ factorial(sym(t4(k)-t))...
+ ./ factorial(sym(t5(k)-t))...
+ )';
+ end
+ end
+ end
+end
+
+%%
+% post-processing
+
+if~ifcb
+ if ~ifs
+ wig = double(wig);
+ elseif ifs==-1
+ wig = simplify(wig);
+ end
+end
+
+if ~isempty(NKs) % incorrect numerical results - swich to the symbolic computations
+ NKs=sort(NKs);
+ wig(NKs) = sp(NKs) .* Wigner3j(j1(NKs),j2(NKs),j3(NKs),m1(NKs),m2(NKs),m3(NKs),0);
+end
+
+if ~isempty(kk) % duplicates
+ k0 = diff(kk);
+ if all(k0~=1)
+ wig(kk) = sp(kk+1).*sp(kk).*wig(kk+1);
+ else
+ k0 = find(k0>1);
+ k00=1;
+ for k=1:numel(k0)
+ wig(kk(k00:k0(k))) = sp(kk(k0(k))+1).*sp(kk(k00:k0(k))).*wig(kk(k0(k))+1);
+ k00 = k0(k)+1;
+ end
+ wig(kk(k00:end)) = sp(kk(end)+1).*sp(kk(k00:end)).*wig(kk(end)+1);
+ end
+end
+
+if exist('ind0','var') % rearrange to the initial order
+ wig(ind0)=wig;
+end
+
+if ifcb % recompute to the Clebsch-Gordan coefficients
+ if ifs>0
+ wig = wig .* sqrt(2*J3+1) .* (-1).^(J1-J2-M3);
+ elseif ifs==0
+ wig = double( wig .* sqrt(sym(2*J3+1)) .* (-1).^(J1-J2-M3) );
+ elseif ifs==-1
+ wig = simplify(wig .* sqrt(sym(2*J3+1)) .* (-1).^(J1-J2-M3));
+ else
+ wig = wig .* sqrt(sym(2*J3+1)) .* (-1).^(J1-J2-M3);
+ end
+end
+
+if NJ>1 && prod(Nj1S)==NJ
+ wig=reshape(wig,Nj1S);
+end
+
+
+return;
+end
+
+function if3j = if3jc(J1,J2,J3)
+% checks necessary conditions on the angular momenta J1, J2, J3, which must be:
+% (1) non-negative;
+% (2) either integer or half-integer;
+% (3) their sum must be integer;
+% (4) triangular rule.
+% returns true if all the conditions are fulfilled; otherwise returns false.
+
+if3j = J1(:)>=0 & J2(:)>=0 & J3(:)>=0 & mod(J1(:)-J2(:)-J3(:),1)==0 & mod(2*J1(:),1)==0 & mod(2*J2(:),1)==0 & mod(2*J3(:),1)==0 & J3(:) <= J1(:)+J2(:) & J3(:) >= abs(J1(:)-J2(:));
+
+return;
+end
+
+function s=sper(ind)
+% permutation parity
+if size(ind,1)==1
+ ind=ind';
+end
+s = zeros(1,size(ind,2));
+
+for k=1:size(ind,1)-1
+ s = s + sum(ind(k,:) > ind(k+1:end,:),1);
+end
+s = mod(s,2);
+return;
+end
+
+function [ind0,varargout] = ArranA (varargin)
+% simultaneous sorting of several input column vectors
+% so that varargout{1} becomes an acsending-order version of varargin{1},
+% and varargout{2} is resorted so that elements of varargin{2} corresponding
+% to equal elements of i1 becomes an ascending-order subvectors as well, etc.;
+% ind0 is a permutation vector.
+%
+% USAGE: [ind0,i1,i2,...] = ArranA (j1,j2,...)
+% with j1, j2, ... being numerical arrays (matrices) containing the vectors to be arranged column-wise.
+%
+% Programmer: V. B. Sovkov
+% St. Petersburg State University
+% Shanxi University
+%%
+
+if ~nargin
+ ind0=[];
+ varargout={};
+ return;
+end
+[varargout{nargin},ind0] = sort(varargin{nargin},1);
+for m=1:size(ind0,2)
+ for k=nargin-1:-1:1
+ varargout{k}(:,m) = varargin{k}(ind0(:,m),m);
+ end
+end
+for n=nargin-1:-1:1
+ [varargout{n},ind] = sort(varargout{n},1);
+ for m=1:size(ind0,2)
+ for k=nargin:-1:1
+ if k~=n
+ varargout{k}(:,m) = varargout{k}(ind(:,m),m);
+ end
+ end
+ ind0(:,m)=ind0(ind(:,m),m);
+ end
+end
+
+
+end
diff --git a/src/utility/wigner/Wigner3j_Jmax=10.mat b/src/utility/wigner/Wigner3j_Jmax=10.mat
new file mode 100644
index 0000000..69fc5d8
Binary files /dev/null and b/src/utility/wigner/Wigner3j_Jmax=10.mat differ
diff --git a/src/utility/wigner/Wigner3j_Jmax=15.mat b/src/utility/wigner/Wigner3j_Jmax=15.mat
new file mode 100644
index 0000000..4ebdf17
Binary files /dev/null and b/src/utility/wigner/Wigner3j_Jmax=15.mat differ
diff --git a/src/utility/wigner/Wigner3j_Jmax=20.mat b/src/utility/wigner/Wigner3j_Jmax=20.mat
new file mode 100644
index 0000000..ba3c123
Binary files /dev/null and b/src/utility/wigner/Wigner3j_Jmax=20.mat differ
diff --git a/src/utility/wigner/Wigner3j_Jmax=30.mat b/src/utility/wigner/Wigner3j_Jmax=30.mat
new file mode 100644
index 0000000..3288d95
Binary files /dev/null and b/src/utility/wigner/Wigner3j_Jmax=30.mat differ
diff --git a/src/utility/wigner/Wigner6j.m b/src/utility/wigner/Wigner6j.m
new file mode 100644
index 0000000..6d526cc
--- /dev/null
+++ b/src/utility/wigner/Wigner6j.m
@@ -0,0 +1,1159 @@
+function wig = Wigner6j(j1,j2,j3,j4,j5,j6,ifs,ifcb)
+% Computes the Wigner $6j$ symbols
+%
+% .. math::
+%
+% W = \begin{Bmatrix}
+% j_1 & j_2 & j_3 \\
+% j_4 & j_5 & j_6
+% \end{Bmatrix}
+%
+% or the recoupling matrix element
+%
+% .. math::
+%
+% W = \left\langle (j_1,j_2)j_3,j_4,j_5 \middle| j_1,(j_2,j_4)j_6,j_5 \right\rangle
+%
+% using the Racah formula.
+%
+% VERSION of January, 2020
+%
+% Programmer: V. B. Sovkov, St. Petersburg State University, Shanxi University
+%
+% Usage
+% -----
+% :code:`W = Wigner6j(j1,j2,j3,j4,j5,j6,ifs,ifcb)`
+% :code:`Wigner6j(j1,j2,j3,j4,j5,j6)`
+%
+% Arguments
+% ---------
+% j1, j2, j3, j4, j5, j6
+% set of six angular momenta, must be arrays of the same size
+%
+% Optional Arguments
+% ------------------
+% ifs
+% the switch of the computational methods;
+% although the central part of all the computations is based on the Racah formula, some details differ:
+%
+% - 0 - the symbolic (presumably accurate) computation with the double-precision output
+% - -1 - the same symbolic computation as ifs=0 but with the symbolic output (accurate
+% square root/rational-type equation, simplified)
+% - -2 - the same as :code:`ifs=-1` but without a final simplification of the symbolic
+% expression, which can be time-consuming sometimes
+% - 1, 2 (default, recommended), 3 - numeric double-precision algorithms (see Notes below)
+%
+% all other input values of ifs are set to the closest from the above list.
+%
+% ifcb
+% if exists and is true, switches to computing the coupling matrix elements instead of the
+% $6j$-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$.
+%
+% Notes
+% -----
+% most of the estimates below is based on extensive numerical tests within a range of the
+% quantum nubers <=1000
+%
+% a. the numeric algorithms (:code:`ifs>0`) are usually much faster than the symbolic
+% algorithm (:code:`ifs<=0`)
+% b. the accuracy of the symbolic algorithm remains the uttermost possible
+% c. the accuracy of the numeric algorithms can worsen for big quantum numbers
+% 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
+% 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
+% h. the algorithms :code:`ifs=1` and :code:`ifs=2` ensure a reasonably good accuracy; the
+% worst registered ca for both algorithms (occuring before switching to the symbolic
+% regime) was :code:`Wigner6j(41.5,52,43.5,36,38.5,46)=-0.00029` with the biggest
+% relative inaccuracy of 3.6e-10
+% i. the algorithm :code:`ifs=2` (comparing to :code:`ifs=1`) proceeds in a wider range of
+% quantum numbers numerically before switching to the symbolic regime (i.e., can work
+% faster with big quantum numbers); in our experimental probing it proved to ensure a
+% good enough accuracy and was chosen as a default one
+% j. the algorithm :code:`ifs=3` (comparing to :code:`ifs=2`) proceeds in a wider range of
+% quantum numbers numerically before switching to the symbolic regime (i.e., can work
+% faster with big quantum numbers) but for some combinations of big quantum numbers,
+% completely wrong results are observed: e.g.,
+% :code:`Wigner6j(227,230.5,210.5,249.5,235,232,3)`; thereby, we do not recommend using
+% the algorithm code:`ifs=3` at all, and only keep it here in view of the completeness.
+%
+% References
+% ----------
+% 1. https://en.wikipedia.org/wiki/6-j_symbol
+% 2. `Zare, R. N., & Harter, W. G. (1988). Angular momentum: understanding spatial aspects
+% in chemistry and physics. New York, 120.
+% `_
+
+NJ = max([numel(j1), numel(j2), numel(j3), numel(j4), numel(j5), numel(j6)]);
+
+if numel(j1) ~= NJ && isscalar(j1)
+ j1(1:NJ) = j1;
+end
+if numel(j2) ~= NJ && isscalar(j2)
+ j2(1:NJ) = j2;
+end
+if numel(j3) ~= NJ && isscalar(j3)
+ j3(1:NJ) = j3;
+end
+if numel(j4) ~= NJ && isscalar(j4)
+ j4(1:NJ) = j4;
+end
+if numel(j5) ~= NJ && isscalar(j5)
+ j5(1:NJ) = j5;
+end
+if numel(j6) ~= NJ && isscalar(j6)
+ j6(1:NJ) = j6;
+end
+
+try
+
+
+ NJ = min([numel(j1) numel(j2) numel(j3) numel(j4) numel(j5) numel(j6)]);
+ Nj1S=size(j1);
+ j1 = double(reshape(j1(1:NJ),NJ,1));
+ j2 = double(reshape(j2(1:NJ),NJ,1));
+ j3 = double(reshape(j3(1:NJ),NJ,1));
+ j4 = double(reshape(j4(1:NJ),NJ,1));
+ j5 = double(reshape(j5(1:NJ),NJ,1));
+ j6 = double(reshape(j6(1:NJ),NJ,1));
+
+ if nargin<7 || isempty(ifs) || ~isnumeric(ifs) && ~islogical(ifs) || ifs(1)>0 && ifs(1)<=1.5 % choose the algorithm
+ ifs = 2; % default
+ elseif ifs(1)>0
+ ifs = min(round(ifs(1)),3);
+ else
+ ifs=max(round(ifs(1)),-2);
+ end
+
+ NK = if3jc(j1,j2,j3) & if3jc(j1,j5,j6) & if3jc(j3,j4,j5) & if3jc(j2,j4,j6);
+ if all(~NK)
+ if ifs>=0
+ wig=zeros(NJ,1);
+ else
+ wig=sym(zeros(NJ,1));
+ end
+ if prod(Nj1S)==NJ
+ reshape(wig, Nj1S);
+ end
+ return;
+ end
+
+ if ifs>0
+ wig=zeros(NJ,1);
+ else
+ wig=sym(zeros(NJ,1));
+ end
+
+ ifcb = nargin>7 && ~isempty(ifcb) && ifcb(1); % if the Clebsch-Gordan coefficient instead of the 3j-symbols are needed?
+ if ifcb
+ J1=j1;
+ J2=j2;
+ J3=j3;
+ J4=j4;
+ J5=j5;
+ J6=j6;
+ end
+
+ for k=1:NJ
+ jup=[j1(k),j2(k),j3(k)]';
+ jdw=[j4(k),j5(k),j6(k)]';
+ if min(jdw)jdw(2)
+ jj=jdw(2:3);
+ jdw(2:3)=jup(2:3);
+ jup(2:3)=jj;
+ [~,jup,jdw]=ArranA(jup,jdw);
+ end
+ j1(k)=jup(1);
+ j2(k)=jup(2);
+ j3(k)=jup(3);
+ j4(k)=jdw(1);
+ j5(k)=jdw(2);
+ j6(k)=jdw(3);
+ end
+ if NJ>1
+ [ind0,j1,j2,j3,j4,j5,j6] = ...
+ ArranA ( reshape(j1(1:NJ),NJ,1),reshape(j2(1:NJ),NJ,1),reshape(j3(1:NJ),NJ,1),reshape(j4(1:NJ),NJ,1),reshape(j5(1:NJ),NJ,1),reshape(j6(1:NJ),NJ,1));
+ NK = NK(ind0);
+
+ kk = find ( j1(1:NJ-1)==j1(2:NJ) & j2(1:NJ-1)==j2(2:NJ) & j3(1:NJ-1)==j3(2:NJ) & j4(1:NJ-1)==j4(2:NJ) & j5(1:NJ-1)==j5(2:NJ) & j6(1:NJ-1)==j6(2:NJ) );
+ if ~isempty(kk)
+ NK(kk)=0;
+ end
+ else
+ kk=[];
+ end
+
+ NKs=[]; % for pointing to erronious numeric results in order to switch them to the symbolic computations
+
+%%
+% special cases - faster and more accurate computation than the general case below
+
+ if any(NK & j1<=2) % the range of the special cases currently included
+
+% case j = 0
+ kp = find(NK & j1==0);
+ if ~isempty(kp)
+ if ifs>0
+ wig(kp) = (-1).^(j2(kp)+j4(kp)+j5(kp)) ./ sqrt((2*j2(kp)+1).*(2*j5(kp)+1));
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^(j2(kp)+j4(kp)+j5(kp)) ./ sqrt(sym((2*j2(kp)+1).*(2*j5(kp)+1)));
+ end
+ NK(kp)=0;
+ end
+% case j = 1/2
+ kp = find(NK & j1==1/2 & j50
+ wig(kp) = (-1).^s .* sqrt((s-2*j5(kp))./(2*j5(kp)+1)./(2*j5(kp)+2).*(s-2*j3(kp)+1)./(2*j3(kp))./(2*j3(kp)+1));
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym((s-2*j5(kp))./(2*j5(kp)+1)./(2*j5(kp)+2).*(s-2*j3(kp)+1)./(2*j3(kp))./(2*j3(kp)+1)));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==1/2); % & j5>j6
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ if ifs>0
+ wig(kp) = (-1).^s .* sqrt((s+1)./(2*j5(kp))./(2*j5(kp)+1).*(s-2*j4(kp))./(2*j3(kp))./(2*j3(kp)+1));
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym((s+1)./(2*j5(kp))./(2*j5(kp)+1).*(s-2*j4(kp))./(2*j3(kp))./(2*j3(kp)+1)));
+ end
+ NK(kp)=0;
+ end
+% case j = 1
+ kp = find(NK & j1==1 & j5>j6 & j3>j2); % & j3>j2
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ if ifs>0
+ wig(kp) = (-1).^s .* sqrt(s./(2*j5(kp)-1)./(2*j5(kp)).*(s+1)./(2*j5(kp)+1).*(s-2*j4(kp)-1)./(2*j3(kp)-1).*(s-2*j4(kp))./(2*j3(kp))./(2*j3(kp)+1));
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym(s./(2*j5(kp)-1)./(2*j5(kp)).*(s+1)./(2*j5(kp)+1).*(s-2*j4(kp)-1)./(2*j3(kp)-1).*(s-2*j4(kp))./(2*j3(kp))./(2*j3(kp)+1)));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==1 & j5j2); % & j3>j2
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ if ifs>0
+ wig(kp) = (-1).^s .* sqrt((s-2*j5(kp)-1)./(2*j5(kp)+1)./(2*j5(kp)+2).*(s-2*j5(kp))./(2*j5(kp)+3)./(2*j3(kp)-1).*(s-2*j3(kp)+1)./(2*j3(kp)).*(s-2*j3(kp)+2)./(2*j3(kp)+1));
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym((s-2*j5(kp)-1)./(2*j5(kp)+1)./(2*j5(kp)+2).*(s-2*j5(kp))./(2*j5(kp)+3)./(2*j3(kp)-1).*(s-2*j3(kp)+1)./(2*j3(kp)).*(s-2*j3(kp)+2)./(2*j3(kp)+1)));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==1 & j5==j6 & j3>j2);
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ if ifs>0
+ wig(kp) = (-1).^s .* sqrt(2*(s+1)./(2*j5(kp))./(2*j5(kp)+1).*(s-2*j4(kp))./(2*j5(kp)+2)./(2*j3(kp)-1).*(s-2*j5(kp))./(2*j3(kp)).*(s-2*j3(kp)+1)./(2*j3(kp)+1));
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym(2*(s+1)./(2*j5(kp))./(2*j5(kp)+1).*(s-2*j4(kp))./(2*j5(kp)+2)./(2*j3(kp)-1).*(s-2*j5(kp))./(2*j3(kp)).*(s-2*j3(kp)+1)./(2*j3(kp)+1)));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==1 & j50
+ wig(kp) = (-1).^s .* sqrt(2*(s+1)./(2*j2(kp))./(2*j2(kp)+1).*(s-2*j4(kp))./(2*j2(kp)+2)./(2*j6(kp)-1).*(s-2*j2(kp))./(2*j6(kp)).*(s-2*j6(kp)+1)./(2*j6(kp)+1));
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym(2*(s+1)./(2*j2(kp))./(2*j2(kp)+1).*(s-2*j4(kp))./(2*j2(kp)+2)./(2*j6(kp)-1).*(s-2*j2(kp))./(2*j6(kp)).*(s-2*j6(kp)+1)./(2*j6(kp)+1)));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==1 & j5==j6 & j2==j3); % & j2==j3
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ if ifs>0
+ wig(kp) = 2*(-1).^s .* (j4(kp).*(j4(kp)+1)-j5(kp).*(j5(kp)+1)-j3(kp).*(j3(kp)+1)) ./ sqrt((2*j5(kp)).*(2*j5(kp)+1).*(2*j5(kp)+2).*(2*j3(kp)).*(2*j3(kp)+1).*(2*j3(kp)+2));
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = 2*(-1).^s .* sym(j4(kp).*(j4(kp)+1)-j5(kp).*(j5(kp)+1)-j3(kp).*(j3(kp)+1)) ./ sqrt(sym((2*j5(kp)).*(2*j5(kp)+1).*(2*j5(kp)+2).*(2*j3(kp)).*(2*j3(kp)+1).*(2*j3(kp)+2)));
+ end
+ NK(kp)=0;
+ end
+% case j = 3/2
+ kp = find(NK & j1==3/2 & j3-j2==3/2 & j5-j6==3/2);
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+1;
+ A2=2*j3(kp)+1;
+ if ifs>0
+ wig(kp) = (-1).^s .* sqrt(...
+ (s-1).*s.*(s+1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j4(kp)-2).*(s-2*j4(kp)-1).*(s-2*j4(kp))...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym(...
+ (s-1).*s.*(s+1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j4(kp)-2).*(s-2*j4(kp)-1).*(s-2*j4(kp))...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==3/2 & j3-j2==3/2 & j5-j6==1/2);
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+2;
+ A2=2*j3(kp)+1;
+ if ifs>0
+ wig(kp) = (-1).^s .* sqrt(...
+ 3*s.*(s+1).*(s-2*j4(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j4(kp)).*(s-2*j5(kp)).*(s-2*j3(kp)+1)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym(...
+ 3*s.*(s+1).*(s-2*j4(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j4(kp)).*(s-2*j5(kp)).*(s-2*j3(kp)+1)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ kp = find(NK & j1==3/2 & j3-j2==1/2 & j5-j6==3/2);
+ if ~isempty(kp)
+ s=j4(kp)+j3(kp)+j5(kp);
+ A1=2*j3(kp)+2;
+ A2=2*j5(kp)+1;
+ if ifs>0
+ wig(kp) = (-1).^s .* sqrt(...
+ 3*s.*(s+1).*(s-2*j4(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j4(kp)).*(s-2*j3(kp)).*(s-2*j5(kp)+1)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym(...
+ 3*s.*(s+1).*(s-2*j4(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j4(kp)).*(s-2*j3(kp)).*(s-2*j5(kp)+1)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==3/2 & j3-j2==1/2 & j5-j6==-3/2);
+ if ~isempty(kp)
+ s=j4(kp)+j2(kp)+j6(kp);
+ A1=2*j2(kp)+3;
+ A2=2*j6(kp)+1;
+ if ifs>0
+ wig(kp) = (-1).^s .* sqrt(...
+ 3*(s+1).*(s-2*j4(kp)).*(s-2*j2(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j2(kp)).*(s-2*j6(kp)+1).*(s-2*j6(kp)+2)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym(...
+ 3*(s+1).*(s-2*j4(kp)).*(s-2*j2(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j2(kp)).*(s-2*j6(kp)+1).*(s-2*j6(kp)+2)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+ kp = find(NK & j1==3/2 & j3-j2==3/2 & j5-j6==-1/2);
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+3;
+ A2=2*j3(kp)+1;
+ if ifs>0
+ wig(kp) = (-1).^s .* sqrt(...
+ 3*(s+1).*(s-2*j4(kp)).*(s-2*j5(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j5(kp)).*(s-2*j3(kp)+1).*(s-2*j3(kp)+2)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym(...
+ 3*(s+1).*(s-2*j4(kp)).*(s-2*j5(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j5(kp)).*(s-2*j3(kp)+1).*(s-2*j3(kp)+2)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==3/2 & j3-j2==3/2 & j5-j6==-3/2); % & j5-j6==-3/2
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+4;
+ A2=2*j3(kp)+1;
+ if ifs>0
+ wig(kp) = (-1).^s .* sqrt(...
+ (s-2*j5(kp)-2).*(s-2*j5(kp)-1).*(s-2*j5(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j3(kp)+1).*(s-2*j3(kp)+2).*(s-2*j3(kp)+3)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .* sqrt(sym(...
+ (s-2*j5(kp)-2).*(s-2*j5(kp)-1).*(s-2*j5(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ .* (s-2*j3(kp)+1).*(s-2*j3(kp)+2).*(s-2*j3(kp)+3)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==3/2 & j5-j6==1/2 & j3-j2==1/2); % & j3-j2==1/2
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+2;
+ A2=2*j3(kp)+2;
+ if ifs>0
+ wig(kp) = (-1).^s .*...
+ sqrt(...
+ (s+1).*(s-2*j4(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ ) .* ...
+ (2*(s-2*j5(kp)).*(s-2*j3(kp))-(s+2).*(s-2*j4(kp)-1))...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .*...
+ sqrt(sym(...
+ (s+1).*(s-2*j4(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ )) .* ...
+ sym(2*(s-2*j5(kp)).*(s-2*j3(kp))-(s+2).*(s-2*j4(kp)-1))...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==3/2 & j3-j2==1/2 & j5-j6==-1/2); % & j3-j2==1/2 & j5-j6==-1/2
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+3;
+ A2=2*j3(kp)+2;
+ if ifs>0
+ wig(kp) = (-1).^s .*...
+ sqrt(...
+ (s-2*j5(kp)).*(s-2*j3(kp)+1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ ) .* ...
+ ((s-2*j5(kp)-1).*(s-2*j3(kp))-2*(s+2).*(s-2*j4(kp)))...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .*...
+ sqrt(sym(...
+ (s-2*j5(kp)).*(s-2*j3(kp)+1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)...
+ )) .* ...
+ sym((s-2*j5(kp)-1).*(s-2*j3(kp))-2*(s+2).*(s-2*j4(kp)))...
+ ;
+ end
+ NK(kp)=0;
+ end
+% case j = 2, j3-j2=2
+ kp = find(NK & j1==2 & j3-j2==2 & j5-j6==2);
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+1;
+ A2=2*j3(kp)+1;
+ if ifs>0
+ wig(kp) = (-1).^s .*...
+ sqrt(...
+ (s-2).*(s-1).*s.*(s+1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j4(kp)-3).*(s-2*j4(kp)-2).*(s-2*j4(kp)-1).*(s-2*j4(kp))...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .*...
+ sqrt(sym(...
+ (s-2).*(s-1).*s.*(s+1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j4(kp)-3).*(s-2*j4(kp)-2).*(s-2*j4(kp)-1).*(s-2*j4(kp))...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==2 & j5-j6==1);
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+2;
+ A2=2*j3(kp)+1;
+ if ifs>0
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(...
+ (s-1).*s.*(s+1).*(s-2*j4(kp)-2)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j4(kp)-1).*(s-2*j4(kp)).*(s-2*j5(kp)).*(s-2*j3(kp)+1)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(sym(...
+ (s-1).*s.*(s+1).*(s-2*j4(kp)-2)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j4(kp)-1).*(s-2*j4(kp)).*(s-2*j5(kp)).*(s-2*j3(kp)+1)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==2 & j5==j6);
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+3;
+ A2=2*j3(kp)+1;
+ if ifs>0
+ wig(kp) = (-1).^s .*...
+ sqrt(...
+ 6*s.*(s+1).*(s-2*j4(kp)-1).*(s-2*j4(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j5(kp)-1).*(s-2*j5(kp)).*(s-2*j3(kp)+1).*(s-2*j3(kp)+2)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .*...
+ sqrt(sym(...
+ 6*s.*(s+1).*(s-2*j4(kp)-1).*(s-2*j4(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j5(kp)-1).*(s-2*j5(kp)).*(s-2*j3(kp)+1).*(s-2*j3(kp)+2)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==2 & j5-j6==-1);
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+4;
+ A2=2*j3(kp)+1;
+ if ifs>0
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(...
+ (s+1).*(s-2*j4(kp)).*(s-2*j5(kp)-2).*(s-2*j5(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j5(kp)).*(s-2*j3(kp)+1).*(s-2*j3(kp)+2).*(s-2*j3(kp)+3)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(sym(...
+ (s+1).*(s-2*j4(kp)).*(s-2*j5(kp)-2).*(s-2*j5(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j5(kp)).*(s-2*j3(kp)+1).*(s-2*j3(kp)+2).*(s-2*j3(kp)+3)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==2 & j5-j6==-2); % & j5-j6==-2
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+5;
+ A2=2*j3(kp)+1;
+ if ifs>0
+ wig(kp) = (-1).^s .*...
+ sqrt(...
+ (s-2*j5(kp)-3).*(s-2*j5(kp)-2).*(s-2*j5(kp)-1).*(s-2*j5(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j3(kp)+1).*(s-2*j3(kp)+2).*(s-2*j3(kp)+3).*(s-2*j3(kp)+4)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .*...
+ sqrt(sym(...
+ (s-2*j5(kp)-3).*(s-2*j5(kp)-2).*(s-2*j5(kp)-1).*(s-2*j5(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j3(kp)+1).*(s-2*j3(kp)+2).*(s-2*j3(kp)+3).*(s-2*j3(kp)+4)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ));
+ end
+ NK(kp)=0;
+ end
+% case j = 2, j3-j2=1
+ kp = find(NK & j1==2 & j3-j2==1 & j5-j6==2);
+ if ~isempty(kp)
+ s=j4(kp)+j3(kp)+j5(kp);
+ A1=2*j3(kp)+2;
+ A2=2*j5(kp)+1;
+ if ifs>0
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(...
+ (s-1).*s.*(s+1).*(s-2*j4(kp)-2)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j4(kp)-1).*(s-2*j4(kp)).*(s-2*j3(kp)).*(s-2*j5(kp)+1)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(sym(...
+ (s-1).*s.*(s+1).*(s-2*j4(kp)-2)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j4(kp)-1).*(s-2*j4(kp)).*(s-2*j3(kp)).*(s-2*j5(kp)+1)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==1 & j5-j6==-2);
+ if ~isempty(kp)
+ s=j4(kp)+j2(kp)+j6(kp);
+ A1=2*j2(kp)+4;
+ A2=2*j6(kp)+1;
+ if ifs>0
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(...
+ (s+1).*(s-2*j4(kp)).*(s-2*j2(kp)-2).*(s-2*j2(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j2(kp)).*(s-2*j6(kp)+1).*(s-2*j6(kp)+2).*(s-2*j6(kp)+3)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(sym(...
+ (s+1).*(s-2*j4(kp)).*(s-2*j2(kp)-2).*(s-2*j2(kp)-1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j2(kp)).*(s-2*j6(kp)+1).*(s-2*j6(kp)+2).*(s-2*j6(kp)+3)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==1 & j5-j6==1);
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+2;
+ A2=2*j3(kp)+2;
+ if ifs>0
+ wig(kp) = 4*(-1).^s .*...
+ sqrt(...
+ s.*(s+1).*(s-2*j4(kp)-1).*(s-2*j4(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ) .*...
+ ( (j4(kp)+j5(kp)).*(j4(kp)-j5(kp)+1)-(j3(kp)-1).*(j3(kp)-j5(kp)+1) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = 4*(-1).^s .*...
+ sqrt(sym(...
+ s.*(s+1).*(s-2*j4(kp)-1).*(s-2*j4(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ )) .*...
+ sym( (j4(kp)+j5(kp)).*(j4(kp)-j5(kp)+1)-(j3(kp)-1).*(j3(kp)-j5(kp)+1) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==1 & j5==j6);
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+3;
+ A2=2*j3(kp)+2;
+ if ifs>0
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(...
+ 6*(s+1).*(s-2*j4(kp)).*(s-2*j5(kp)).*(s-2*j3(kp)+1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ) .*...
+ ( (j4(kp)+j5(kp)+1).*(j4(kp)-j5(kp))-j3(kp).^2+1 )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(sym(...
+ 6*(s+1).*(s-2*j4(kp)).*(s-2*j5(kp)).*(s-2*j3(kp)+1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ )) .*...
+ sym( (j4(kp)+j5(kp)+1).*(j4(kp)-j5(kp))-j3(kp).^2+1 )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3-j2==1 & j5-j6==-1); % & j5-j6==-1
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+4;
+ A2=2*j3(kp)+2;
+ if ifs>0
+ wig(kp) = 4*(-1).^s .*...
+ sqrt(...
+ (s-2*j5(kp)-1).*(s-2*j5(kp)).*(s-2*j3(kp)+1).*(s-2*j3(kp)+2)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ) .*...
+ ( (j4(kp)+j5(kp)+2).*(j4(kp)-j5(kp)-1)-(j3(kp)-1).*(j5(kp)+j3(kp)+2) )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = 4*(-1).^s .*...
+ sqrt(sym(...
+ (s-2*j5(kp)-1).*(s-2*j5(kp)).*(s-2*j3(kp)+1).*(s-2*j3(kp)+2)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ )) .*...
+ sym( (j4(kp)+j5(kp)+2).*(j4(kp)-j5(kp)-1)-(j3(kp)-1).*(j5(kp)+j3(kp)+2) )...
+ ;
+ end
+ NK(kp)=0;
+ end
+% case j = 2, j3==j2
+ kp = find(NK & j1==2 & j3==j2 & j5-j6==-2);
+ if ~isempty(kp)
+ s=j4(kp)+j2(kp)+j6(kp);
+ A1=2*j2(kp)+3;
+ A2=2*j6(kp)+1;
+ if ifs>0
+ wig(kp) = (-1).^s .*...
+ sqrt(...
+ 6*s.*(s+1).*(s-2*j4(kp)-1).*(s-2*j4(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j2(kp)-1).*(s-2*j2(kp)).*(s-2*j6(kp)+1).*(s-2*j6(kp)+2)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = (-1).^s .*...
+ sqrt(sym(...
+ 6*s.*(s+1).*(s-2*j4(kp)-1).*(s-2*j4(kp))...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ .* (s-2*j2(kp)-1).*(s-2*j2(kp)).*(s-2*j6(kp)+1).*(s-2*j6(kp)+2)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ));
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3==j2 & j5-j6==-1);
+ if ~isempty(kp)
+ s=j4(kp)+j2(kp)+j6(kp);
+ A1=2*j2(kp)+3;
+ A2=2*j6(kp)+2;
+ if ifs>0
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(...
+ 6*(s+1).*(s-2*j4(kp)).*(s-2*j2(kp)).*(s-2*j6(kp)+1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ ) .*...
+ ( (j4(kp)+j2(kp)+1).*(j4(kp)-j2(kp))-j6(kp).^2+1 )...
+ ;
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = 2*(-1).^s .*...
+ sqrt(sym(...
+ 6*(s+1).*(s-2*j4(kp)).*(s-2*j2(kp)).*(s-2*j6(kp)+1)...
+ ./ A1./(A1-1)./(A1-2)./(A1-3)./(A1-4)...
+ ./ A2./(A2-1)./(A2-2)./(A2-3)./(A2-4)...
+ )) .*...
+ sym( (j4(kp)+j2(kp)+1).*(j4(kp)-j2(kp))-j6(kp).^2+1 )...
+ ;
+ end
+ NK(kp)=0;
+ end
+ kp = find(NK & j1==2 & j3==j2 & j5==j6); % & j3==j2 & j5==j6
+ if ~isempty(kp)
+ s=j3(kp)+j4(kp)+j5(kp);
+ A1=2*j5(kp)+3;
+ A2=2*j3(kp)+3;
+ C =j4(kp).*(j4(kp)+1)-j5(kp).*(j5(kp)+1)-j3(kp).*(j3(kp)+1);
+ if ifs>0
+ wig(kp) = 2*(-1).^s .*...
+ (3*C.*(C+1)-4*j5(kp).*(j5(kp)+1).*j3(kp).*(j3(kp)+1)) ./...
+ sqrt(...
+ A1.*(A1-1).*(A1-2).*(A1-3).*(A1-4)...
+ .* A2.*(A2-1).*(A2-2).*(A2-3).*(A2-4)...
+ );
+ k0=find(isinf(wig(kp)) | isnan(wig(kp)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(kp(k0))];
+ end
+ else
+ wig(kp) = 2*(-1).^s .*...
+ sym(3*C.*(C+1)-4*j5(kp).*(j5(kp)+1).*j3(kp).*(j3(kp)+1)) ./...
+ sqrt(sym(...
+ A1.*(A1-1).*(A1-2).*(A1-3).*(A1-4)...
+ .* A2.*(A2-1).*(A2-2).*(A2-3).*(A2-4)...
+ ));
+ end
+ NK(kp)=0;
+ end
+
+ end
+%%
+
+ NK = find(NK);
+
+ if ~isempty(NK)
+ if ifs==3
+ C = tc(j1(NK),j2(NK),j3(NK),ifs) + tc(j1(NK),j5(NK),j6(NK),ifs) + tc(j4(NK),j2(NK),j6(NK),ifs) + tc(j4(NK),j5(NK),j3(NK),ifs);
+ else
+ C = tc(j1(NK),j2(NK),j3(NK),ifs) .* tc(j1(NK),j5(NK),j6(NK),ifs) .* tc(j4(NK),j2(NK),j6(NK),ifs) .* tc(j4(NK),j5(NK),j3(NK),ifs);
+ end
+ k0 = find(isinf(C) | isnan(C));
+ if ~isempty(k0)
+ NKs = [NKs;NK(k0)];
+ NK(k0) = [];
+ C(k0)=[];
+ end
+
+ if ~isempty(NK)
+
+ t1 = max ( [j1(NK) + j2(NK) + j3(NK), j1(NK) + j5(NK) + j6(NK), j4(NK) + j2(NK) + j6(NK), j4(NK) + j5(NK) + j3(NK)] , [] , 2 );
+ t2 = min ( [j1(NK) + j2(NK) + j4(NK) + j5(NK), j2(NK) + j3(NK) + j5(NK) + j6(NK), j3(NK) + j1(NK) + j6(NK) + j4(NK)] , [] , 2 );
+
+ if ifs>0 % numeric
+ if ifs==3
+ for k=numel(NK):-1:1 % t below can be of different lengths for the different k! Cannot apply the element-wise operations in this sense!
+ kN = NK(k);
+ t = (t1(k):t2(k))';
+ wig(kN) = ((-1).^t)' * exp ( gammaln([...
+ (t+2),(t-j1(kN)-j2(kN)-j3(kN)+1),(t-j1(kN)-j5(kN)-j6(kN)+1),(t-j4(kN)-j2(kN)-j6(kN)+1),(t-j4(kN)-j5(kN)-j3(kN)+1),(j1(kN)+j2(kN)+j4(kN)+j5(kN)-t+1),(j2(kN)+j3(kN)+j5(kN)+j6(kN)-t+1),(j3(kN)+j1(kN)+j6(kN)+j4(kN)-t+1)...
+ ]) * [1;-1;-1;-1;-1;-1;-1;-1] + C(k) );
+ end
+ else % ifs==1 || ifs==2
+ for k=numel(NK):-1:1 % t below can be of different lengths for the different k! Cannot apply the element-wise operations in this sense!
+ kN = NK(k);
+ t = (t1(k):t2(k))';
+ wig(kN) = (-1).^t' * (...
+ factorial(t+1)...
+ ./factorial(t-j1(kN)-j2(kN)-j3(kN))...
+ * C(k)...
+ ./factorial(t-j1(kN)-j5(kN)-j6(kN))...
+ ./factorial(t-j4(kN)-j2(kN)-j6(kN))...
+ ./factorial(t-j4(kN)-j5(kN)-j3(kN))...
+ ./factorial(j1(kN)+j2(kN)+j4(kN)+j5(kN)-t)...
+ ./factorial(j2(kN)+j3(kN)+j5(kN)+j6(kN)-t)...
+ ./factorial(j3(kN)+j1(kN)+j6(kN)+j4(kN)-t)...
+ );
+ end
+ end
+ k0 = find(isinf(wig(NK)) | isnan(wig(NK)));
+ if ~isempty(k0)
+ NKs = [NKs;NK(k0)];
+ end
+ else % symbolic
+ for k=numel(NK):-1:1 % t below can be of different lengths for the different k! Cannot apply the element-wise operations in this sense!
+ kN = NK(k);
+ t = (t1(k):t2(k))';
+ wig(kN) = (-1).^t' * ( ...
+ factorial(sym(t+1))...
+ ./factorial(sym(t-j1(kN)-j2(kN)-j3(kN)))...
+ * C(k)...
+ ./factorial(sym(t-j1(kN)-j5(kN)-j6(kN)))...
+ ./factorial(sym(t-j4(kN)-j2(kN)-j6(kN)))...
+ ./factorial(sym(t-j4(kN)-j5(kN)-j3(kN)))...
+ ./factorial(sym(j1(kN)+j2(kN)+j4(kN)+j5(kN)-t))...
+ ./factorial(sym(j2(kN)+j3(kN)+j5(kN)+j6(kN)-t))...
+ ./factorial(sym(j3(kN)+j1(kN)+j6(kN)+j4(kN)-t))...
+ );
+ end
+ end
+ end
+ end
+%%
+% post-processing
+
+ if~ifcb
+ if ~ifs
+ wig = double(wig);
+ elseif ifs==-1
+ wig = simplify(wig);
+ end
+ end
+
+ if ~isempty(NKs) % incorrect numerical results - swich to the symbolic computations
+ NKs=sort(NKs);
+ wig(NKs) = Wigner6j(j1(NKs),j2(NKs),j3(NKs),j4(NKs),j5(NKs),j6(NKs),0);
+ end
+
+ if ~isempty(kk)
+ k0 = diff(kk);
+ if all(k0~=1)
+ wig(kk) = wig(kk+1);
+ else
+ k0 = find(k0>1);
+ k00=1;
+ for k=1:numel(k0)
+ wig(kk(k00:k0(k))) = wig(kk(k0(k))+1);
+ k00 = k0(k)+1;
+ end
+ wig(kk(k00:end)) = wig(kk(end)+1);
+ end
+ end
+
+ if exist('ind0','var')
+ wig(ind0)=wig;
+ end
+
+ if ifcb % recompute to the coupling matrix element
+ if ifs>0
+ wig = wig .* sqrt((2*J3+1).*(2*J6+1)) .* (-1).^(J1+J2+J4+J5);
+ elseif ifs==0
+ wig = double( wig .* sqrt(sym((2*J3+1).*(2*J6+1))) .* (-1).^(J1+J2+J4+J5) );
+ elseif ifs==-1
+ wig = simplify( wig .* sqrt(sym((2*J3+1).*(2*J6+1))) .* (-1).^(J1+J2+J4+J5) );
+ else
+ wig = wig .* sqrt(sym((2*J3+1).*(2*J6+1))) .* (-1).^(J1+J2+J4+J5);
+ end
+ end
+
+ if NJ>1 && prod(Nj1S)==NJ
+ wig=reshape(wig,Nj1S);
+ end
+
+catch mectc
+%%
+ beep;
+ disp(mectc);
+ disp('Wigner6j ERROR: The input arguments can be incorrect; see comments in the code,');
+ if ifs<=0 || exist('NKs','var') && ~isempty(NKs)
+ disp('or the ``Symbolic'' toolbox is not properly installed')
+ end
+ try
+ if NJ>1 && prod(Nj1S)==NJ
+ wig=NaN(Nj1S);
+ else
+ wig=NaN(size(j1));
+ end
+ catch
+ wig=NaN;
+ end
+ beep;
+end
+
+return;
+end
+
+
+
+function [ind0,varargout] = ArranA (varargin)
+% simultaneous sorting of several input column vectors
+% so that varargout{1} becomes an acsending-order version of varargin{1},
+% and varargout{2} is resorted so that elements of varargin{2} corresponding
+% to equal elements of i1 becomes an ascending-order subvectors as well, etc.;
+% ind0 is a permutation vector.
+%
+% USAGE: [ind0,i1,i2,...] = ArranA (j1,j2,...)
+% with j1, j2, ... being numerical arrays (matrices) containing the vectors to be arranged column-wise.
+%
+% Programmer: V. B. Sovkov
+% St. Petersburg State University
+% Shanxi University
+%%
+try
+ if ~nargin
+ ind0=[];
+ varargout={};
+ return;
+ end
+ [varargout{nargin},ind0] = sort(varargin{nargin},1);
+ for m=1:size(ind0,2)
+ for k=nargin-1:-1:1
+ varargout{k}(:,m) = varargin{k}(ind0(:,m),m);
+ end
+ end
+ for n=nargin-1:-1:1
+ [varargout{n},ind] = sort(varargout{n},1);
+ for m=1:size(ind0,2)
+ for k=nargin:-1:1
+ if k~=n
+ varargout{k}(:,m) = varargout{k}(ind(:,m),m);
+ end
+ end
+ ind0(:,m)=ind0(ind(:,m),m);
+ end
+ end
+catch mectc
+ varargout=[];
+ disp(mectc);
+end
+
+
+
+return
+end
+
+
+
+function if3j = if3jc(J1,J2,J3)
+% checks necessary conditions on the angular momenta J1, J2, J3, which must be:
+% (1) non-negative;
+% (2) either integer or half-integer;
+% (3) their sum must be integer;
+% (4) triangular rule.
+% returns true if all the conditions are fulfilled; otherwise returns false.
+
+if3j = J1(:)>=0 & J2(:)>=0 & J3(:)>=0 & mod(J1(:)-J2(:)-J3(:),1)==0 & mod(2*J1(:),1)==0 & mod(2*J2(:),1)==0 & mod(2*J3(:),1)==0 & J3(:) <= J1(:)+J2(:) & J3(:) >= abs(J1(:)-J2(:));
+
+return;
+end
+
+
+
+
+
+
+
+function tri = tc(j1,j2,j3,ifs)
+% sqrt of the triangle coefficient or its logarithm (when ifs=3)
+if nargin<4 || isempty(ifs) || ~isnumeric(ifs) || ~isreal(ifs) || ifs>0
+ if ifs==3
+ tri = zeros(size(j1));
+ for k=1:numel(tri)
+ tri(k) = gammaln(j1(k)+j2(k)-j3(k)+1)/2 + gammaln(j1(k)-j2(k)+j3(k)+1)/2 - sum( log( (-j1(k)+j2(k)+j3(k)+1):(j1(k)+j2(k)+j3(k)+1) )/2 );
+ end
+ else
+ if ifs==1
+ tri = sqrt( factorial(j1+j2+j3+1) ./ factorial(j1+j2-j3) );
+ elseif ifs==2
+ for k=numel(j1):-1:1
+ tri(k,1) = prod( sqrt( (j1(k)+j2(k)-j3(k)+1):(j1(k)+j2(k)+j3(k)+1) ) );
+ end
+ end
+ k=find(~isinf(tri) & ~isnan(tri));
+ if ~isempty(k)
+ tri(k) = sqrt( factorial(-j1(k)+j2(k)+j3(k)) ) ./ tri(k) .* sqrt( factorial(j1(k)-j2(k)+j3(k)) );
+ end
+ end
+else
+ tri = sqrt( factorial(sym(j1+j2-j3))./ factorial(sym(j1+j2+j3+1)) .* factorial(sym(j1-j2+j3)) .* factorial(sym(-j1+j2+j3)) );
+end
+return;
+end
diff --git a/src/utility/wigner/WignerCache.m b/src/utility/wigner/WignerCache.m
new file mode 100644
index 0000000..ea5f3c4
--- /dev/null
+++ b/src/utility/wigner/WignerCache.m
@@ -0,0 +1,66 @@
+function [key, sign] = WignerCache(j1, j2, j3, m1, m2, m3)
+
+R = [-j1 + j2 + j3, j1 - j2 + j3, j1 + j2 - j3
+ j1 - m1, j2 - m2, j3 - m3
+ j1 + m1, j2 + m2, j3 + m3];
+sign = 1;
+
+% check if magic square
+rowsums = sum(R, 1);
+colsums = sum(R, 2);
+if any(rowsums(1) ~= rowsums) || any(rowsums(1) ~= colsums)
+ key = 1;
+ return
+end
+
+sign = 1;
+
+% bring S to the first row and column:
+[S, i] = min(R, [], 'all', 'linear');
+[row, col] = ind2sub([3 3], i);
+if row ~= 1
+ R([1 row], :) = R([row 1], :);
+ sign = -sign;
+end
+if col ~= 1
+ R(:, [1 col]) = R(:, [col, 1]);
+ sign = -sign;
+end
+
+% bring L to the first row and second column:
+[L1, i] = max(R(1,:), [], 'all', 'linear');
+[L2, j] = max(R(:,1), [], 'all', 'linear');
+
+if L2 > L1
+ % make row 2 and then transpose
+ if j ~= 2
+ R([3 2], :) = R([2 3], :);
+ sign = -sign;
+ end
+ R = R.';
+ L = L2;
+else
+ if i ~= 2
+ R(:, [3 2]) = R(:, [2 3]);
+ sign = -sign;
+ end
+ L = L1;
+end
+
+if R(2,2) > R(3,2) || (R(2,2) == R(3,2) && R(2,3) > R(3,3))
+ R([2 3], :) = R([3 2], :);
+ sign = -sign;
+end
+
+X = R(2,1);
+B = R(2,2);
+T = R(3,3);
+
+key = 1 / 120 * (L * (24 + L * (50 + L * (35 + L * (10 + L))))) + ...
+ 1 / 24 * (X * (6 + X * (11 + X * (6 + X)))) + ...
+ 1 / 6 * (T * (2 + T * (3 + T))) + ...
+ 1 / 2 * (B * (B + 1)) + ...
+ S + 2;
+
+end
+
diff --git a/test/TestCharge.m b/test/TestCharge.m
new file mode 100644
index 0000000..a477230
--- /dev/null
+++ b/test/TestCharge.m
@@ -0,0 +1,184 @@
+classdef TestCharge < matlab.unittest.TestCase
+ % TestCharge - Unit tests for charges.
+
+ properties
+ tol = 1e-14
+ end
+
+ properties (TestParameter)
+ smallset = struct( ...
+ 'Z1', Z1, ...
+ 'Z2', Z2([false, true]), ...
+ 'U1', U1(-2:2), ...
+ 'O2', O2([0 0 1 2], [0 1 2 2]), ...
+ 'SU2', SU2(1:4), ...
+ 'Z2xU1', ProductCharge(Z2(0, 0, 0, 1, 1, 1), U1(-1, 0, 1, -1, 0, 1)))
+ end
+
+ methods (Test)
+ function fusionrules(testCase, smallset)
+ for a = smallset, for b = smallset
+ cs = a * b;
+ N = Nsymbol(repmat(a, 1, length(cs)), repmat(b, 1, length(cs)), cs);
+ verifyEqual(testCase, sum(qdim(cs) .* N), qdim(a) * qdim(b), ...
+ 'Fusion rules must preserve quantum dimensions.');
+ verifyTrue(testCase, all(N > 0), ...
+ 'Nsymbol must be nonnegative for fusion product.');
+ end, end
+ end
+
+ function conj(tc, smallset)
+ for a = smallset
+ abar = conj(a);
+
+ tc.verifyTrue(conj(abar) == a, 'Conj should be an involution.');
+ tc.verifyTrue(Nsymbol(a, abar, one(a)) == 1, ...
+ 'a * abar should contain one.');
+ end
+ end
+
+ function Fmove(testCase, smallset)
+ % Fmove - Compare Fsymbol with fusiontensor.
+ % Fmove(testCase, smallset)
+ % verifies if an F-move on the fusion tensors is compatible with
+ % the Fsymbol.
+ for a = smallset, for b = smallset, for c = smallset
+ for e = a * b
+ F3 = fusiontensor(a, b, e);
+ for f = b * c
+ F2 = conj(fusiontensor(b, c, f));
+ for d = intersect(e * c, a * f)
+ F1 = conj(fusiontensor(a, f, d));
+ F4 = fusiontensor(e, c, d);
+ testCase.assertEqual(contract(F1, [1 6 4 -4], ...
+ F2, [2 3 6 -3], F3, [1 2 5 -1], F4, [5 3 4 -2]), ...
+ Fsymbol(a, b, c, d, e, f) * qdim(d), ...
+ 'AbsTol', testCase.tol, ...
+ 'Fsymbol incompatible with fusiontensor.');
+ end
+ end
+ end
+ end, end, end
+ end
+
+ function Funitary(testCase, smallset)
+ % Funitary - Test if the Fsymbols are unitary.
+ % Funitary(testCase, smallset)
+ % verifies whether the Fsymbol constitutes a unitary operation
+ % when interpreted as a matrix from (emn) to (fkl).
+ for a = smallset, for b = smallset, for c = smallset
+ for d = prod([a, b, c])
+ Fmat = Fmatrix(a, b, c, d);
+ testCase.assertEqual(Fmat' * Fmat, eye(size(Fmat)), ...
+ 'AbsTol', testCase.tol);
+ testCase.assertEqual(Fmat * Fmat', eye(size(Fmat)), ...
+ 'AbsTol', testCase.tol);
+ end
+ end, end, end
+ end
+
+ function Fone(testCase, smallset)
+ % Fone - Test if the Fsymbol is correct for the trivial charge.
+ e = one(smallset);
+ testCase.assertEqual(Fsymbol(e, e, e, e, e, e), 1, ...
+ 'AbsTol', testCase.tol);
+ end
+
+ function pentagon(testCase, smallset)
+ % pentagon - Test if the Fsymbol fulfills the pentagon equation.
+ for a = smallset, for b = smallset, for c = smallset, for d = smallset
+ for f = a * b, for h = c * d
+ for g = f * c, for i = b * h
+ for e = intersect(g * d, a * i)
+ if hasmultiplicity(fusionstyle(smallset))
+ lhs = contract(...
+ Fsymbol(f, c, d, e, g, h), [-1 -2 -3 1], ...
+ Fsymbol(a, b, h, e, f, i), [-4 1 -5 -6]);
+ rhs = zeros(size(lhs));
+ for j = b * c
+ rhs = rhs + contract( ...
+ Fsymbol(a, b, c, g, f, j), [-4 -1 1 2], ...
+ Fsymbol(a, j, d, e, g, i), [2 -2 3 -6], ...
+ Fsymbol(b, c, d, i, j, h), [1 3 -3 -5]);
+ end
+ else
+ lhs = Fsymbol(f, c, d, e, g, h) * ...
+ Fsymbol(a, b, h, e, f, i);
+ rhs = sum(arrayfun(@(j) ...
+ Fsymbol(a, b, c, g, f, j) * ...
+ Fsymbol(a, j, d, e, g, i) * ...
+ Fsymbol(b, c, d, i, j, h), b * c));
+ end
+ testCase.verifyTrue(isapprox(lhs, rhs, ...
+ 'AbsTol', testCase.tol, 'RelTol', testCase.tol));
+ end
+ end, end
+ end, end
+ end, end, end, end
+ end
+
+ function Rmove(testCase, smallset)
+ % Rmove - Compare Rsymbol with fusiontensor.
+ % Rmove(testCase, smallset)
+ % verifies if an R-move on the fusion tensors is compatible with
+ % the Fsymbol.
+ for a = smallset, for b = smallset
+ for c = a * b
+ testCase.verifyEqual(fusiontensor(a, b, c), ...
+ contract(fusiontensor(b, a, c), [-2 -1 -3 1], ...
+ Rsymbol(a, b, c), [-4 1]), 'AbsTol', testCase.tol);
+ end
+ end, end
+ % TODO non-symmetric braiding tests
+ end
+
+ function hexagon(testCase, smallset)
+ % hexagon - Test if the F- and Rsymbol fulfill the hexagon equation.
+ % hexagon(testCase, smallset)
+ for a = smallset, for b = smallset, for c = smallset
+ for e = c * a, for g = c * b
+ for d = intersect(e * b, a * g)
+ if hasmultiplicity(fusionstyle(smallset))
+ lhs = contract(Rsymbol(c, a, e), [-1 1], ...
+ Fsymbol(a, c, b, d, e, g), [1 -2 2 -4], ...
+ Rsymbol(b, c, g), [2 -3]);
+ rhs = zeros(size(lhs));
+ for f = a * b
+ rhs = rhs + contract(...
+ Fsymbol(c, a, b, d, e, f), [-1 -2 1 2], ...
+ Rsymbol(c, f, d), [2 3], ...
+ Fsymbol(a, b, c, d, f, g), [1 3 -3 -4]);
+ end
+ else
+ lhs = Rsymbol(c, a, e) * Fsymbol(a, c, b, d, e, g) ...
+ * Rsymbol(b, c, g);
+ rhs = sum(arrayfun(@(f) Fsymbol(c, a, b, d, e, f) * ...
+ Rsymbol(c, f, d) * Fsymbol(a, b, c, d, f, g), ...
+ a * b));
+ end
+ testCase.verifyEqual(lhs, rhs, 'AbsTol', testCase.tol);
+ end
+ end, end
+ end, end, end
+ end
+
+ function cumprod(testCase, smallset)
+ % cumprod - Test if the cumprod functionality works.
+ % cumprod(testCase, smallset)
+
+ % scalar case:
+ for a = smallset, for b = smallset
+ [d, v] = cumprod([a; b]);
+ for c = smallset
+ [d, v] = cumprod([a; b; c]);
+ end
+ end, end
+
+ % matrix case:
+ [d, v] = cumprod(combvec(smallset, smallset));
+ [d, v] = cumprod(combvec(smallset, smallset, smallset));
+ [d, v] = cumprod(combvec(smallset, smallset, smallset, smallset));
+ end
+ end
+
+end
diff --git a/test/TestFusionTree.m b/test/TestFusionTree.m
new file mode 100644
index 0000000..00b1279
--- /dev/null
+++ b/test/TestFusionTree.m
@@ -0,0 +1,341 @@
+classdef TestFusionTree < matlab.unittest.TestCase
+ % TestFusionTree - Unit tests for fusion trees.
+
+ %#ok<*PROPLC>
+
+ properties (ClassSetupParameter)
+ weight = {'small', 'medium'}%, 'large'}
+ charge = {'Z1', 'Z2', 'U1', 'O2', 'SU2', 'Z2xU1'}
+ end
+
+ methods (TestClassSetup)
+ function generateTrees(tc, charge, weight)
+ rng(123);
+
+ %% Setup charges
+ switch charge
+ case 'Z1'
+ chargeset = Z1;
+ case 'Z2'
+ chargeset = Z2([0 1]);
+ case 'U1'
+ switch weight
+ case 'small'
+ chargeset = U1(-1:1);
+ case 'medium'
+ chargeset = U1(-2:2);
+ case 'large'
+ chargeset = U1(-2:2);
+ end
+ case 'O2'
+ switch weight
+ case 'small'
+ chargeset = O2([0 0 1], [0 1 2]);
+ case 'medium'
+ chargeset = O2([0 0 1 2], [0 1 2 2]);
+ case 'large'
+ chargeset = O2([0 0 1 2 3], [0 1 2 2 2]);
+ end
+ case 'SU2'
+ switch weight
+ case 'small'
+ chargeset = SU2(1:2);
+ case 'medium'
+ chargeset = SU2(1:4);
+ case 'large'
+ chargeset = SU2(1:6);
+ end
+ case 'Z2xU1'
+ switch weight
+ case 'small'
+ chargeset = ProductCharge(Z2([0 0 0 1 1 1]), U1([-1:1 -1:1]));
+ case 'medium'
+ chargeset = ProductCharge(Z2([0 0 0 1 1 1]), U1([-1:1 -1:1]));
+ case 'large'
+ chargeset = ProductCharge(Z2([0 0 0 0 0 1 1 1 1 1]), ...
+ U1([-2:2 -2:2]));
+ end
+ end
+
+ %% Setup weight
+ switch weight
+ case 'small'
+ legs = 0:3;
+ maxTrees = 25;
+ maxLegs = 4;
+ tc.testWeight = 0.5;
+ case 'medium'
+ legs = 0:4;
+ maxTrees = 100;
+ maxLegs = 5;
+ tc.testWeight = 0.25;
+ case 'large'
+ legs = 0:4;
+ maxTrees = Inf;
+ maxLegs = 6;
+ tc.testWeight = 0.5;
+ end
+
+ %% Setup trees
+ tc.trees = cell(length(legs));
+ for rank1 = legs
+ for rank2 = legs
+ if rank1 + rank2 == 0
+ tc.trees{rank1+1, rank2+1} = FusionTree.new([0 0]);
+ continue;
+ end
+ if rank1 + rank2 > maxLegs
+ continue;
+ end
+ args = cell(2, rank1+rank2);
+ args(1, :) = {chargeset};
+ args(2, :) = num2cell(randi([0 1], 1, rank1+rank2));
+ trees = FusionTree.new([rank1 rank2], args{:});
+ nTrees = length(trees);
+ while nTrees < 1 || nTrees > maxTrees
+ if all(cellfun(@length, args(1,:)) == 1)
+ break;
+ end
+ ind = randi([1 rank1+rank2]);
+ l = length(args{1, ind});
+ if l == 1, continue; end
+ args{1, ind}(randi(l)) = [];
+ newtrees = FusionTree.new([rank1 rank2], args{:});
+ nTrees = length(trees);
+ if nTrees == 0
+ break;
+ end
+ trees = newtrees;
+ end
+ assert(nTrees > 0);
+ tc.trees{rank1+1, rank2+1} = trees;
+ end
+ end
+ end
+ end
+
+ properties
+ tol = 1e-14
+ trees
+ testWeight
+ end
+
+ methods (Test)
+ function trees_properties(tc)
+ for i = 1:size(tc.trees, 1)
+ for j = 1:size(tc.trees, 2)
+ if i == 1 && j == 1 || isempty(tc.trees{i,j})
+ continue;
+ end
+ verifyTrue(tc, all(isallowed(tc.trees{i,j})), ...
+ 'Generated invalid fusion trees.');
+ end
+ end
+ end
+
+ function braiding(tc)
+ for i = 1:numel(tc.trees)
+ if isempty(tc.trees{i}) || tc.trees{i}.legs < 2
+ continue;
+ end
+ if rand < tc.testWeight
+ f = tc.trees{i};
+ ps = perms(1:f.legs);
+ for p = ps(randperm(size(ps, 2), min(size(ps, 2), 5)), :).'
+ lvl = randperm(f.legs);
+ indout = randi([0 f.legs]);
+
+ [c1, f1] = braid(f, p.', lvl, [indout, f.legs - indout]);
+
+ % basic properties
+ verifyEqual(tc, size(c1), [length(f) length(f1)], ...
+ 'Coefficients have the wrong size.');
+ verifyEqual(tc, full(abs(c1).^2 * qdim(f1.coupled)), ...
+ qdim(f.coupled), 'AbsTol', tc.tol, ...
+ 'Braiding must preserve centernorm.');
+ verifyEqual(tc, isallowed(f1), true(length(f1), 1), ...
+ 'Output trees are not allowed.');
+
+ % compatible with fusiontensor
+ if issymmetric(braidingstyle(f))
+ a1_cell = fusiontensor(f);
+ a2_cell = fusiontensor(f1);
+ for j = 1:length(f)
+ a1 = permute(a1_cell{j}, p.');
+ a2 = zeros(size(a1));
+ [~, col, val] = find(c1(j, :));
+ for k = 1:length(val)
+ a2 = a2 + val(k) * a2_cell{col(k)};
+ end
+ verifyEqual(tc, a2, a1, 'AbsTol', tc.tol, ...
+ 'Braiding should be compatible with fusiontensors.');
+ end
+ end
+
+ % invertible
+ [c2, f2] = braid(f1, invperm(p.'), lvl(p), f.rank);
+ verifyEqual(tc, c1 * c2, speye(length(f)), ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol, ...
+ 'Braiding should be invertible.');
+ verifyEqual(tc, f2, f, 'Braiding should be invertible.');
+ end
+ end
+ end
+ end
+ % function repartitioning(tc)
+ % for i = 1:numel(tc.trees)
+ % if isempty(tc.trees{i}) || tc.trees{i}.legs < 2
+ % continue;
+ % end
+ % if rand < tc.testWeight
+ % f = tc.trees{i};
+ % for n = 0:f.legs
+ % [f1, c1] = repartition(f, [n f.legs-n]);
+ % verifyEqual(tc, full(abs(c1).^2 * qdim(f1.coupled)), ...
+ % qdim(f.coupled), 'AbsTol', tc.tol, 'RelTol', tc.tol, ...
+ % 'Repartition must preserve centernorm.');
+ % verifyEqual(tc, isallowed(f1), true(size(f1), 1), ...
+ % 'Output trees are not allowed.');
+ %
+ % [f2, c2] = repartition(f1, f.rank);
+ % verifyEqual(tc, c1 * c2, speye(size(f)), 'AbsTol', tc.tol, ...
+ % 'RelTol', tc.tol, 'Repartition should be invertible.');
+ % verifyEqual(tc, f, f2, 'Repartition should be invertible.');
+ %
+ % if issymmetric(braidingstyle(f))
+ % a1_cell = arrayfun(@double, f, 'UniformOutput', false);
+ % a2_cell = arrayfun(@double, f1, 'UniformOutput', false);
+ % for j = 1:length(f)
+ % a1 = a1_cell{j};
+ % a2 = zeros(size(a1));
+ % [~, col, val] = find(c1(j, :));
+ % for k = 1:length(val)
+ % a2 = a2 + val(k) * a2_cell{col(k)};
+ % end
+ % verifyEqual(tc, a2, a1, 'AbsTol', tc.tol, ...
+ % 'Repartition should be compatible with fusiontensors.');
+ % end
+ % end
+ % end
+ % end
+ % end
+ % end
+
+ % end
+ %
+ % function permute_fusiontensor(testCase, smallset, legs)
+ % trees = generateFusionTrees(testCase, smallset, legs);
+ % ps = perms(1:legs);
+ % for p = ps(randperm(size(ps, 2), min(size(ps, 2), 5)), :).'
+ % [t1, c1] = permute(trees, p.');
+ % for j = 1:length(trees)
+ % array1 = permute(cast(slice(trees, j), 'double'), ...
+ % [p.' legs+1]);
+ % array2 = zeros(size(array1));
+ % [~, col, val] = find(c1(j,:));
+ % for k = 1:length(val)
+ % array2 = array2 + val(k) * cast(slice(t1, col(k)), 'double');
+ % end
+ % verifyEqual(testCase, array1, array2, 'AbsTol', testCase.tol, ...
+ % 'Permute should be compatible with fusiontensors.');
+ % end
+ % end
+ % end
+ %
+ % function permute_properties(testCase, smallset, legs)
+ % trees = generateFusionTrees(testCase, smallset, legs);
+ %
+ % treeargs = cell(2, legs);
+ % for i = 1:legs
+ % treeargs{1,i} = unique(trees.uncoupled(:, i));
+ % treeargs{2,i} = trees.arrows(i);
+ % end
+ % coupled = unique(trees.coupled);
+ %
+ % ps = perms(1:legs);
+ % for p = ps(randperm(size(ps, 2), min(size(ps, 2), 5)), :).'
+ % [t1, c1] = permute(trees, p.');
+ %
+ % verifyEqual(testCase, size(c1), [length(trees) length(t1)], ...
+ % 'Coefficients have the wrong size.');
+ % verifyEqual(testCase, full(vecnorm(c1)), ones(1, length(trees)), ...
+ % 'AbsTol', testCase.tol, 'Norm of coefficients should be 1.');
+ % verifyEqual(testCase, c1 * c1', speye(length(trees)), ...
+ % 'AbsTol', testCase.tol, 'Permute should be unitary.');
+ % verifyEqual(testCase, c1' * c1, speye(length(t1)), ...
+ % 'AbsTol', testCase.tol, 'Permute should be unitary.');
+ %
+ % verifyTrue(testCase, issorted(t1), ...
+ % 'Permute should return sorted trees.');
+ %
+ % tempargs = treeargs(:, p.');
+ % t3 = FusionTree.new(tempargs{:});
+ % lia = ismember(t3.coupled, coupled);
+ % t3.charges = t3.charges(lia, :);
+ % if ~isempty(t3.vertices)
+ % t3.vertices = t3.vertices(lia,:);
+ % end
+ % verifyEqual(testCase, t1, t3, ...
+ % 'Permute should transform full basis to full basis.');
+ %
+ % [t2, c2] = permute(t1, invperm(p));
+ % verifyEqual(testCase, c2, c1', 'AbsTol', testCase.tol, ...
+ % 'RelTol', testCase.tol, 'Permute should be invertible.');
+ % verifyEqual(testCase, t2, trees, ...
+ % 'Permute should be invertible.');
+ % end
+ % end
+ %
+ % function yangbaxter(testCase, smallset, legs)
+ % trees = generateFusionTrees(testCase, smallset, legs);
+ % for i = 1:legs-2
+ % [t1, c1] = artinbraid(trees, i);
+ % [t1, c2] = artinbraid(t1, i+1);
+ % [t1, c3] = artinbraid(t1, i);
+ %
+ % [t2, c4] = artinbraid(trees, i+1);
+ % [t2, c5] = artinbraid(t2, i);
+ % [t2, c6] = artinbraid(t2, i+1);
+ %
+ % verifyEqual(testCase, ...
+ % c1 * c2 * c3, c4 * c5 * c6, 'AbsTol', testCase.tol, ...
+ % 'Yang-Baxter equation not satisfied.');
+ % verifyEqual(testCase, t1, t2, ...
+ % 'Yang-Baxter equation not satisfied.');
+ % end
+ % end
+ end
+
+ % methods
+ % function trees = generateFusionTrees(testCase, smallset, legs)
+ % % generateFusionTrees - Generate some trees for testing.
+ % % trees = generateFusionTrees(testCase, smallset, legs)
+ %
+ % rng(123);
+ % args = cell(2, legs);
+ % for i = 1:legs
+ % args{1, i} = smallset(randperm(length(smallset), ...
+ % randi([1 length(smallset)])));
+ % args{2, i} = randi([0 1]);
+ % end
+ % trees = FusionTree.new(args{:});
+ %
+ % % get reasonable amount of trees:
+ % while length(trees) > testCase.maxTrees
+ % coupleds = trees.coupled;
+ % coupled = unique(trees.coupled);
+ % coupled = coupled(randi([1 length(coupled)]));
+ % lia = ismember(coupleds, coupled);
+ % if all(lia)
+ % break
+ % end
+ % trees.charges = trees.charges(~lia,:);
+ % if ~isempty(trees.vertices)
+ % trees.vertices = trees.vertices(~lia,:);
+ % end
+ % trees = sort(trees);
+ % end
+ % end
+ % end
+
+end
diff --git a/test/TestSolvers.m b/test/TestSolvers.m
new file mode 100644
index 0000000..1afb8b1
--- /dev/null
+++ b/test/TestSolvers.m
@@ -0,0 +1,64 @@
+classdef TestSolvers < matlab.unittest.TestCase
+ % TestLinsolve - Unit tests for linsolve.
+
+ properties
+ tol = 1e-10
+ end
+
+ properties (TestParameter)
+ spaces = struct(...
+ 'cartesian', CartesianSpace.new([3 4]), ...
+ 'complex', ComplexSpace.new(3, false, 4, true, 2, false), ...
+ 'Z2', GradedSpace.new(Z2(0, 1), [1 1], false, Z2(0, 1), [1 2], true, ...
+ Z2(0, 1), [3 2], true), ...
+ 'U1', GradedSpace.new(U1(0, 1, -1), [1 2 2], false, U1(0, 1, -1), [3 1 1], false), ...
+ 'SU2', GradedSpace.new(SU2(1, 2), [3 1], false, SU2(1, 3), [2 1], false) ...
+ )
+ end
+
+ methods (Test)
+ function linsolve(tc, spaces)
+ A = Tensor.randnc(spaces, spaces);
+ A = normalize((A + A') / 2);
+ while cond(A) > 20
+ A = Tensor.randnc(spaces, spaces);
+ A = normalize((A + A') / 2);
+ end
+
+ xin = normalize(Tensor.randnc(spaces, []));
+ b = A * xin;
+
+ for alg = ["bicgstab", "bicgstabl", "gmres"]
+ [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);
+
+ 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);
+ end
+ end
+
+ function eigsolve(tc, spaces)
+ rng(123);
+ A = Tensor.randc(spaces, spaces) - Tensor.eye(spaces, spaces) .* (1 + 1i);
+ A = (A + A') ./ 2;
+
+ x0 = Tensor.randc(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(isapprox(A * v, v * d));
+
+ [v, d, flag] = eigsolve(A, x0, 3, 'IsSymmetric', true);
+ tc.verifyTrue(flag == 0);
+ tc.verifyTrue(isapprox(A * v, v * d));
+ end
+ end
+end
+
diff --git a/test/TestTensor.m b/test/TestTensor.m
new file mode 100644
index 0000000..60b4fdc
--- /dev/null
+++ b/test/TestTensor.m
@@ -0,0 +1,279 @@
+classdef TestTensor < matlab.unittest.TestCase
+ % TestTensor - Unit tests for tensors.
+
+ properties
+ tol = 1e-12
+ end
+
+ properties (TestParameter)
+ spaces = struct(...
+ 'cartesian', CartesianSpace(3, [], 4, [], 5, [], 6, [], 7, []), ...
+ 'complex', ComplexSpace(3, false, 4, true, 5, false, 6, false, 7, true), ...
+ 'Z2', GradedSpace.new(Z2(0, 1), [1 1], false, Z2(0, 1), [1 2], true, ...
+ Z2(0, 1), [3 2], true, Z2(0, 1), [2 3], false, Z2(0, 1), [2 5], false), ...
+ 'U1', GradedSpace.new(U1(0, 1, -1), [1 2 2], false, U1(0, 1, -1), [3 1 1], false, ...
+ U1(0, 1, -1), [2 2 1], true, U1(0, 1, -1), [1, 2, 3], false, ...
+ 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) ...
+ )
+ end
+
+ methods (Test)
+ function basic_linear_algebra(tc, spaces)
+ t1 = Tensor.rand(spaces(1:3), spaces(4:5));
+
+ tc.verifyTrue(isapprox(norm(t1)^2, dot(t1, t1), ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol), 'Norm and dot incompatible.');
+
+ a = rand();
+ 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), ...
+ '2*t and t+t incompatible.')
+
+ tc.verifyTrue(isapprox(-t1, t1 .* (-1), 'AbsTol', tc.tol), ...
+ '-t and t .* (-1) incompatible.');
+
+ tc.verifyTrue(isapprox(t1 .* (1/a), t1 ./ a, 'AbsTol', tc.tol), ...
+ '.* and ./ are incompatible');
+
+ tc.verifyTrue(isapprox(norm(normalize(t1)), 1), ...
+ 'normalize should result in unit norm.');
+
+ t2 = Tensor.rand(spaces(1:3), spaces(4:5));
+ b = rand();
+ 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 matrix_functions(tc, spaces)
+ for i = 1:3
+ t = Tensor.randnc(spaces(1:i), spaces(1:i));
+
+ assertTrue(tc, isapprox(t*t, t^2, 'AbsTol', tc.tol, 'RelTol', tc.tol));
+ assertTrue(tc, isapprox(t*t*t, t^3, 'AbsTol', tc.tol, 'RelTol', tc.tol));
+
+ assertTrue(tc, isapprox((t^(1/2))^2, t, ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol));
+ assertTrue(tc, isapprox((t^(1/3))^3, t, ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol));
+
+ assertTrue(tc, isapprox(sqrtm(t)^2, t, 'AbsTol', tc.tol, 'RelTol', tc.tol));
+
+ at = 0.01 * normalize(t);
+ assertTrue(tc, isapprox(expm(at), ...
+ 1 + at + at^2/2 + at^3/6 + at^4/24 + at^5/120, ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol));
+
+ assertTrue(tc, isapprox(t * inv(t), inv(t) * t, ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol) && ...
+ isapprox(t * inv(t), t.eye(t.codomain, t.domain), ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol));
+ end
+ end
+
+ function permute_via_inner(tc, spaces)
+ rng(213);
+ t1 = Tensor.rand(spaces, []);
+ t2 = Tensor.rand(spaces, []);
+
+ inner = dot(t1, t2);
+
+ 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]);
+ 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]);
+ tc.assertTrue(all(dims(t2, p.') == dims(t4)), ...
+ 'Incorrect size after permutation.');
+ tc.assertTrue(...
+ isapprox(dot(t3, t4), inner, 'AbsTol', tc.tol, 'RelTol', tc.tol), ...
+ 'Permute should preserve inner products.');
+ end
+ end
+ end
+
+ function permute_via_conversion(tc, spaces)
+ t = Tensor.rand(spaces, []);
+ a = double(t);
+ rng(123);
+ tc.assertTrue(all(dims(t) == size(a, 1:nspaces(t))));
+ for k = 0:5
+ ps = perms(1:5).';
+ for p = ps(:, randperm(size(ps, 2), min(size(ps, 2), 10)))
+ t2 = permute(t, p.', [k 5-k]);
+ a2 = double(t2);
+ tc.assertTrue(all(dims(t2) == size(a2, 1:nspaces(t))));
+ tc.assertTrue(all(dims(t2) == size(a, p.')));
+ tc.assertTrue(...
+ isapprox(permute(a, p.'), a2, 'Abstol', tc.tol, 'RelTol', tc.tol), ...
+ 'Permute should be compatible with conversion.');
+ end
+ end
+ end
+
+ function multiplication_via_conversion(tc, spaces)
+ t1 = Tensor.randnc(spaces(1), spaces(2));
+ t2 = Tensor.randnc(spaces(2), spaces(3));
+
+ t3 = t1 * t2;
+ tc.assertTrue(isapprox(double(t3), double(t1) * double(t2)));
+
+ t1 = Tensor.randnc(spaces(1), spaces(2:3));
+ t2 = Tensor.randnc(spaces(2:3), spaces(4));
+
+ tc.assertTrue(isapprox(double(t1 * t2), ...
+ tensorprod(double(t1), double(t2), [2 3], [2 1], 'NumDimensionsA', 3)));
+
+ W1 = spaces(1:3);
+ W2 = spaces(4:5);
+
+ t1 = Tensor.randnc(W1, W1);
+ t2 = Tensor.randnc(W1, W2);
+
+ t1_array = double(t1);
+ t2_array = double(t2);
+
+ tc.assertTrue(isapprox(double(t1 * t2), ...
+ tensorprod(t1_array, t2_array, [4 5 6], [3 2 1], ...
+ 'NumDimensionsA', 6), ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol));
+ end
+
+ function tensorprod_via_conversion(tc, 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));
+ end
+
+ function orthogonalize(tc, spaces)
+ t = Tensor.randnc(spaces, []);
+
+ %% Left orthogonalize
+ p1 = [3 4 2];
+ p2 = [1 5];
+
+ for alg = ["qr", "qrpos", "ql", "qlpos", "polar", "svd"]
+ [Q, R] = leftorth(t, p1, p2, alg);
+
+ assertTrue(tc, ...
+ isapprox(Q * R, permute(t, [p1 p2], [length(p1) length(p2)]), ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol), ...
+ sprintf('Q and R not a valid %s factorization.', alg));
+
+ assertTrue(tc, isisometry(Q, 'left', ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol), ...
+ 'Q should be a left isometry.');
+
+ switch alg
+ case 'polar'
+ tc.assertTrue(isposdef(R), 'R should be positive definite.');
+
+ case {'qr', 'qrpos'}
+ tc.assertTrue(istriu(R), 'R should be upper triangular.');
+
+ case {'ql', 'qlpos'}
+ tc.assertTrue(istril(R), 'R should be lower triangular.');
+
+ end
+ end
+
+
+ %% Right orthogonalize
+ p1 = [3 4];
+ p2 = [2 1 5];
+ for alg = ["rq", "rqpos", "lq", "lqpos", "polar", "svd"]
+ [L, Q] = rightorth(t, p1, p2, alg);
+
+ assertTrue(tc, ...
+ isapprox(L * Q, permute(t, [p1 p2], [length(p1) length(p2)]), ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol), ...
+ sprintf('Q and R not a valid %s factorization.', alg));
+
+ assertTrue(tc, isisometry(Q, 'right', ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol), ...
+ 'Q should be a right isometry.');
+
+ switch alg
+ case 'polar'
+ assertTrue(tc, isposdef(L));
+
+ case {'rq', 'rqpos'}
+ assertTrue(tc, istriu(L));
+
+ case {'lq', 'lqpos'}
+ assertTrue(tc, istril(L));
+
+ end
+ end
+ end
+
+ function nullspace(tc, spaces)
+ t = Tensor.randnc(spaces, []);
+
+ %% 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])) < ...
+ 100 * eps(norm(t)), ...
+ 'N should be a left nullspace.');
+ assertTrue(tc, isisometry(N, 'left', ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol), ...
+ 'N should be a left isometry.');
+ end
+
+
+ %% Right nullspace
+ 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') < ...
+ 100 * eps(norm(t)), ...
+ 'N should be a right nullspace.');
+ assertTrue(tc, isisometry(N, 'right', ...
+ 'AbsTol', tc.tol, 'RelTol', tc.tol), ...
+ 'N should be a right isometry.');
+ end
+ end
+
+ function singularvalues(tc, spaces)
+ t = Tensor.randnc(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), ...
+ 'USV should be a factorization.');
+ assertTrue(tc, isisometry(U), ...
+ 'U should be an isometry.');
+ assertTrue(tc, isisometry(V), ...
+ 'V should be an isometry.');
+
+ %% truncation
+
+ end
+
+ function eigenvalues(tc, spaces)
+ for i = 1:3
+ t = Tensor.randnc(spaces(1:i), spaces(1:i));
+ [V, D] = eig(t);
+ tc.assertTrue(isapprox(t * V, V * D, 'AbsTol', tc.tol, 'RelTol', tc.tol));
+ [V, D, W] = eig(t);
+ tc.assertTrue(isapprox(W' * t, D * W', 'AbsTol', tc.tol, 'RelTol', tc.tol));
+ end
+ end
+ end
+end
+