Matrix product operators
========================

-------------------------------------------------------------------------------

Dense matrix product operators
------------------------------

There are 3 classes in this module: ``MPOSite``, ``DenseMPO``, ``DenseMPOList``

**DenseMPO**

This is the standard textbook-like MPO that acts on specified sites (``DenseMPO.sites``).
It is a list of ``MPOSite``-s

::

    | | | |
   -O-O-O-O-
    | | | |

**MPOSite**

Each tensor in the ``DenseMPO`` is stored as ``MPOSite``. For example,
a 4-body ``DenseMPO`` is going to be a list of 4 ``MPOSite``-s.

::

    |
   -O-
    |

**DenseMPOList**

A list of ``DenseMPO``-s. Here, the ``DenseMPOList`` is usually
used to represent Hamiltonians. Hamiltonians are special because
they are normally a sum of polynomially-many tensor product operator (TPO) terms.
For local hamiltonians, each TPO acts on a finite number of sites.
Hence we can store a Hamiltonian as a list of TPO terms, where
each TPO is represented as a ``DenseMPO``. For example,
the Ising model contains nearest-neighbour :math:`\sigma_x\sigma_x` and
:math:`\sigma_z` terms, and every 2-body TPO term :math:`\sigma_x\sigma_x` and
local term :math:`\sigma_z` would be stored as a separate ``DenseMPO``
added to a ``DenseMPOList``.

::

   | |       | |       |
  -O-O-  ,  -O-O-  ,  -O-  ,  ...
   | |       | |       |

Therefore, the number of sites in each individual ``DenseMPO`` in a
``DenseMPOList`` does not correspond to the total number of sites - instead, it
corresponds to the number of sites on which the individual Hamiltonian
term is acting on.

**Some useful attributes/functions**


Initialize ``MPOSite`` with ``MPOSite([list of sites], tensor_str_name, pstrength=None, prefactor=1.0, operators=op_dict, params={})``

- ``MPOSite.site`` = on which site in a system does this MPO site act
- ``MPOSite.operator`` = get an actual tensor
- ``MPOSite.pstrength``, ``MPOSite.weight`` = prefactors that multiply the elements of an operator.
The difference between ``pstrength`` and ``weight`` is that ``pstrength`` is parametrized via params dictionary
and ``weight`` is a number. The default prefactor is 1, i.e. ``pstrength=None`` and ``weight=1``.

Initialize ``DenseMPO`` with ``DenseMPO([list of MPOSite-s], tensor_backend)``

- ``DenseMPO.num_sites`` = on how many sites does this MPO act
- ``DenseMPO.sites`` = list of sites on which MPO acts
- ``DenseMPO[ind].operator`` = get tensor on site ind
- ``DenseMPO.append(MPOSite)`` = add ``MPOSite`` to ``DenseMPO``

Initialize ``DenseMPOList`` with ``DenseMPOList()`` and then append ``DenseMPO``-s

- ``DenseMPOList[ind]`` = get ``ind``-th ``DenseMPO`` in a list
- ``DenseMPOList.from_model_prepared(\*args, \*\*kwargs)`` = create a ``DenseMPOList`` object from a given Hamiltonian model

**See the more detailed documentation of individual functions below**

.. autoclass:: qtealeaves.mpos.MPOSite
   :members:

.. autoclass:: qtealeaves.mpos.DenseMPO
   :members:

.. autoclass:: qtealeaves.mpos.DenseMPOList
   :members:

-------------------------------------------------------------------------------

Indexed tensor product operators
--------------------------------

This is the default MPO representation for the Hamiltonians and the
observables which the library uses during computationally extensive operations
in simulations. (The other option are sparse MPOs, but indexed tensor product
operators (iTPOs) for now allow more generality.) Here is the short introduction
to iTPO framework.

A Hamiltonian MPO (or any MPO operator) is often a sum of tensor product terms (TPOs),
where every TPO term contains *n* operators acting on *n* sites. Therefore, as it is
the case with dense MPOs (see above), we can treat the Hamiltonian as a list of
TPO terms.

Very often, however, we have only few different operator tensors building the Hamiltonian:
the Ising Hamiltonian is, for example, built only from :math:`\sigma_x` and
:math:`\sigma_z` operators. Therefore, to save memory, we don't have to define our
Hamiltonian MPO by saving every operator at every site (as it is done in a ``DenseMPOList``),
but instead we can assign every operator tensor an index ``ii`` and store only the information
of type: "operator ``ii`` acting on site ``x`` with the prefactor ``w``", where ``ii`` points
to e.g. :math:`\sigma_x` tensor. Every TPO term is assigned a unique integer TPO ID going
from 0 to number of TPO terms. The same TPO IDs are assigned to the corresponding operators
inside TPO terms, so we can keep track which operator belongs to which TPO term.
This way of representing MPOs is what we call the indexed TPO picture.

Another difference between iTPOs with respect to ``DenseMPO`` / ``DenseMPOLists`` is that local
terms are treated differently in the iTPO picture. All the local terms
acting on the same site are contracted into a single local term of same dimensions.

There are 3 classes in this module: ``ITPOTerm``, ``ITPOSites``, ``ITPO``

**ITPOTerm**

``ITPOTerm`` contains all operators acting on a single site, each coming
from a different TPO term in the MPO. The "Term" in the name is a bit misleading,
as ``ITPOTerm`` does not refer to the TPO term (which usually acts on more than one site),
but instead refers to a collection of operators acting on a single site.
``ITPOTerm`` can contain one local term, which is treated separately and is stored
under ``ITPOTerm._local``.

**ITPO**

The full MPO, containing all its TPO terms. In general, it has two parts: ``ITPO.site_terms``
and ``ITPO.eff_ops``. ``ITPO.site_terms`` are an instance of ``ITPOSites`` (description below)
and they represent the TPO terms coming from a Hamiltonian or any operator acting on the
physical sites of the tensor network. ``ITPO.eff_ops``, on the other hand, represent all the TPO
terms inside the effective operators around each tensor in a tensor network. Therefore,
``ITPO.eff_ops`` depend on a TN ansatz. In a simulation, ``ITPO.site_terms`` are usually the input
Hamiltonian and ``ITPO.eff_ops`` are computed by contracting this Hamiltonian with tensors in a TN.

**ITPOSites**

Represent the TPO terms coming from a Hamiltonian or any operator acting on physical sites of the
tensor network. Stored as a list of ``ITPOTerm``-s, such that there is one ``ITPOTerm`` per
system site.

**Some useful attributes/functions/remarks**

ITPOTerm acts on a single site, but there is no attribute inside of it which tell
which site it is. This info is instead stored in ``ITPO`` and ``ITPOSites``.

- ``ITPOTerm._tensors`` : following the iTPO convention, this is a dictionary with all the tensors, where the key is
  the operator string which defines it and the value is a corresponding tensor
- ``ITPOTerm._operators`` : a dictionary, with the key being a TPO ID of the corresponding TPO term in the parent ITPO
  and the value is the corresponding operator string
- ``ITPOTerm.weights`` : list of prefactors for every operator tensor in ``ITPOTerm``, ordered by TPO IDs
- ``ITPOTerm._local`` : tensor of a local term, if any

The easiest way to create an ``ITPO`` is to first create a ``DenseMPOList``, initialize
the empty ITPO with ``itpo = ITPO(num_sites)``, and then convert ``DenseMPOList`` to ``ITPO`` with
``itpo.add_dense_mpo_list(DenseMPOList)``

- ``ITPO.site_terms`` : ``ITPOSites``, i.e. a list of ``ITPOTerm``-s that act on physical sites in a TN
- ``ITPO.eff_ops`` : a dictionary of effective operators around every tensor in a tensor
  network. The keys in a dictionary specify which tensor we are looking at, with
  the convention differing for different TNs (e.g. for MPS, it's just the index of
  the tensor, for TTN it's ``(layer_idx, tensor_idx)``). The value for each iterm in
  ``ITPO.eff_ops`` is again a dictionary containing three ``ITPOTerm``-s, as there are
  three effective operators per tensor. The convention for keys of this dictionary
  again differs for different TNs, but (roughly speaking) it's supposed to specify
  whether the corresponding ``ITPOTerm`` is left, down, or right effective operator.
  Upon initialization, ``ITPO.eff_ops`` is an empty dictionary. The actual effective
  operators are computed once ``ITPO.contr_to_eff_op()`` is called.

**See the more detailed documentation of individual functions below**

.. autoclass:: qtealeaves.mpos.ITPOTerm
   :members:

.. autoclass:: qtealeaves.mpos.ITPOSites
   :members:

.. autoclass:: qtealeaves.mpos.ITPO
   :members:

-------------------------------------------------------------------------------

Sparse matrix operators
-----------------------

.. autoclass:: qtealeaves.mpos.SparseMatrixOperator
   :members:

.. autoclass:: qtealeaves.mpos.SparseMatrixOperatorPy
   :members:

-------------------------------------------------------------------------------

Sparse matrix product operators
-------------------------------

.. autoclass:: qtealeaves.mpos.SparseMPO
   :members:

.. autoclass:: qtealeaves.mpos.SparseMatrixProductOperator
   :members:

-------------------------------------------------------------------------------

Abstract effective operators
----------------------------

.. autoclass:: qtealeaves.mpos._AbstractEffectiveOperators
   :members:

-------------------------------------------------------------------------------

Disentanglers
-------------

.. autoclass:: qtealeaves.mpos.DELayer
   :members:

-------------------------------------------------------------------------------

TTN Projector
-------------


