r"""This module contains a base class for a Matrix Product State (MPS).

An MPS looks roughly like this::

    |   -- B[0] -- B[1] -- B[2] -- ...
    |       |       |      |

We use the following label convention for the `B` (where arrows indicate `qconj`)::

    |  vL ->- B ->- vR
    |         |
    |         ^
    |         p

We store one 3-leg tensor `_B[i]` with labels ``'vL', 'vR', 'p'`` for each of the `L` sites
``0 <= i < L``.
Additionally, we store ``L+1`` singular value arrays `_S[ib]` on each bond ``0 <= ib <= L``,
independent of the boundary conditions.
``_S[ib]`` gives the singlur values on the bond ``i-1, i``.
However, be aware that e.g. :attr:`~tenpy.networks.mps.MPS.chi` returns only the dimensions of the
:attr:`~tenpy.networks.mps.MPS.nontrivial_bonds` depending on the boundary conditions.

The matrices and singular values always represent a normalized state
(i.e. ``np.linalg.norm(psi._S[ib]) == 1`` up to roundoff errors),
but (for finite MPS) we keep track of the norm in :attr:`~tenpy.networks.mps.MPS.norm`
(which is respected by :meth:`~tenpy.networks.mps.MPS.overlap`, ...).

Valid MPS boundary conditions (not to confuse with `bc_coupling` of
:class:`tenpy.models.model.CouplingModel`)  are the following:

==========  ===================================================================================
`bc`        description
==========  ===================================================================================
'finite'    Finite MPS, ``G0 s1 G1 ... s{L-1} G{l-1}``. This is acchieved
            by using a trivial left and right bond ``s[0] = s[-1] = np.array([1.])``.
'segment'   Generalization of 'finite', describes an MPS embedded in left and right
            environments. The left environment is described by ``chi[0]`` *orthonormal* states
            which are weighted by the singular values ``s[0]``. Similar, ``s[L]`` weight some
            right orthonormal states. You can think of the left and right states to be
            generated by additional MPS, such that the overall structure is something like
            ``... s L s L [s0 G0 s1 G1 ... s{L-1} G{L-1} s{L}] R s R s R ...``
            (where we save the part in the brackets ``[ ... ]`` ).
'infinite'  infinite MPS (iMPS): we save a 'MPS unit cell' ``[s0 G0 s1 G1 ... s{L-1} G{L-1}]``
            which is repeated periodically, identifying all indices modulo ``self.L``.
            In particular, the last bond ``L`` is identified with ``0``.
            (The MPS unit cell can differ from a lattice unit cell).
            bond is identified with the first one.
==========  ===================================================================================

An MPS can be in different 'canonical forms' (see :cite:`vidal2004,schollwoeck2011`).
To take care of the different canonical forms, algorithms should use functions like
:meth:`~tenpy.networks.mps.MPS.get_theta`, :meth:`~tenpy.networks.mps.MPS.get_B`
and :meth:`~tenpy.networks.mps.MPS.set_B` instead of accessing them directly,
as they return the `B` in the desired form (which can be chosen as an argument).
The values of the tuples for the form correspond to the exponent of the singular values
on the left and right.
To keep track of a "mixed" canonical form ``A A A s B B``, we save the tuples for each
site of the MPS in :attr:`MPS.form`.

======== ========== ==========================================================================
`form`   tuple      description
======== ========== ==========================================================================
``'B'``  (0, 1)     right canonical: ``_B[i] = -- Gamma[i] -- s[i+1]--``
                    The default form, which algorithms asssume.
``'C'``  (0.5, 0.5) symmetric form: ``_B[i] = -- s[i]**0.5 -- Gamma[i] -- s[i+1]**0.5--``
``'A'``  (1, 0)     left canonical: ``_B[i] = -- s[i] -- Gamma[i] --``.
``'G'``  (0, 0)     Save only ``_B[i] = -- Gamma[i] --``.
``'Th'`` (1, 1)     Form of a local wave function `theta` with singular value on both sides.
                    ``psi.get_B(i, 'Th') is equivalent to ``psi.get_theta(i, n=1)``.
``None`` ``None``   General non-canoncial form.
                    Valid form for initialization, but you need to call
                    :meth:`~tenpy.networks.mps.MPS.canonical_form` (or similar)
                    before using algorithms.
======== ========== ==========================================================================
"""
# Copyright 2018-2021 TeNPy Developers, GNU GPLv3

import numpy as np
import warnings
import random
from functools import reduce
import logging
logger = logging.getLogger(__name__)

from ..linalg import np_conserved as npc
from ..linalg import sparse
from .site import GroupedSite, group_sites
from ..tools.misc import to_iterable, to_array, get_recursive
from ..tools.math import lcm, speigs, entropy
from ..tools.params import asConfig
from ..tools.cache import DictCache
from ..tools import hdf5_io
from ..algorithms.truncation import TruncationError, svd_theta

__all__ = ['MPS', 'MPSEnvironment', 'TransferMatrix', 'InitialStateBuilder', 'build_initial_state']


class MPS:
    r"""A Matrix Product State, finite (MPS) or infinite (iMPS).

    Parameters
    ----------
    sites : list of :class:`~tenpy.networks.site.Site`
        Defines the local Hilbert space for each site.
    Bs : list of :class:`~tenpy.linalg.np_conserved.Array`
        The 'matrices' of the MPS. Labels are ``vL, vR, p`` (in any order).
    SVs : list of 1D array
        The singular values on *each* bond. Should always have length `L+1`.
        Entries out of :attr:`nontrivial_bonds` are ignored.
    bc : ``'finite' | 'segment' | 'infinite'``
        Boundary conditions as described in the tabel of the module doc-string.
    form : (list of) {``'B' | 'A' | 'C' | 'G' | 'Th' | None`` | tuple(float, float)}
        The form of the stored 'matrices', see table in module doc-string.
        A single choice holds for all of the entries.

    Attributes
    ----------
    sites : list of :class:`~tenpy.networks.site.Site`
        Defines the local Hilbert space for each site.
    bc : {'finite', 'segment', 'infinite'}
        Boundary conditions as described in above table.
    form : list of {``None``, tuple(float, float)}
        Describes the canonical form on each site.
        ``None`` means non-canonical form.
        For ``form = (nuL, nuR)``, the stored ``_B[i]`` are
        ``s**form[0] -- Gamma -- s**form[1]`` (in Vidal's notation).
    chinfo : :class:`~tenpy.linalg.np_conserved.ChargeInfo`
        The nature of the charge.
    dtype : type
        The data type of the ``_B``.
    norm : float
        The norm of the state, i.e. ``sqrt(<psi|psi>)``.
        Ignored for (normalized) :meth:`expectation_value`, but important for :meth:`overlap`.
    grouped : int
        Number of sites grouped together, see :meth:`group_sites`.
    segment_boundaries : tuple of :class:`~tenpy.linalg.np_conserved.Array` | (None, None)
        Only defined for 'segment' `bc` if :meth:`canonical_form_finite` has been called.
        If defined, it contains the `U_L` and `V_R` that would be returned by that function.
    _B : list of :class:`npc.Array`
        The 'matrices' of the MPS. Labels are ``vL, vR, p`` (in any order).
        We recommend using :meth:`get_B` and :meth:`set_B`, which will take care of the different
        canonical forms.
    _S : list of {``None``, 1D array, :class:`~tenpy.linalg.np_conserved.Array}
        The singular values on each virtual bond, length ``L+1``.
        May be ``None`` if the MPS is not in canonical form.
        Otherwise, ``_S[i]`` is to the left of ``_B[i]``.
        We recommend using :meth:`get_SL`, :meth:`get_SR`, :meth:`set_SL`, :meth:`set_SR`, which
        takes proper care of the boundary conditions.
        Sometimes (e.g. during DMRG with an enabled mixer), entries may temporarily be
        a non-diagonal :class:`tenpy.linalg.np_conserved.Array` to be inserted between the
        left canonical 'A' tensors on the left and rigth-canonical _B[i] on the right.
    _valid_forms : dict
        Class attribute.
        Mapping for canonical forms to a tuple ``(nuL, nuR)`` indicating that
        ``self._Bs[i] = s[i]**nuL -- Gamma[i] -- s[i]**nuR`` is saved.
    _valid_bc : tuple of str
        Class attribute. Possible valid boundary conditions.
    _transfermatrix_keep : int
        How many states to keep at least when diagonalizing a :class:`TransferMatrix`.
        Important if the state develops a near-degeneracy.
    _p_label, _B_labels : list of str
        Class attribute. `_p_label` defines the physical legs of the B-tensors, `_B_labels` lists
        all the labels of the B tensors. Used by methods like :meth:`get_theta` to avoid
        the necessity of re-implementations for derived classes like the
        :class:`~tenpy.networks.purification_mps.Purification_MPS` if just the number of physical
        legs changed.
    """
    # Canonical form conventions: the saved B = s**nu[0]--Gamma--s**nu[1].
    # For the canonical forms, ``nu[0] + nu[1] = 1``
    _valid_forms = {
        'A': (1., 0.),
        'C': (0.5, 0.5),
        'B': (0., 1.),
        'G': (0., 0.),  # like Vidal's `Gamma`.
        'Th': (1., 1.),
        None: None,  # means 'not in any canonical form'
    }

    # valid boundary conditions. Don't overwrite this!
    _valid_bc = ('finite', 'segment', 'infinite')
    # the "physical" labels for each B
    _p_label = ['p']
    # All labels of each tensor in _B (order is used!)
    _B_labels = ['vL', 'p', 'vR']

    def __init__(self, sites, Bs, SVs, bc='finite', form='B', norm=1.):
        self.sites = list(sites)
        self.chinfo = self.sites[0].leg.chinfo
        self.dtype = dtype = np.find_common_type([B.dtype for B in Bs], [])
        self.form = self._parse_form(form)
        self.bc = bc  # one of ``'finite', 'periodic', 'segment'``.
        self.norm = norm
        self.grouped = 1
        self.segment_boundaries = (None, None)

        # make copies of Bs and SVs
        self._B = [B.astype(dtype, copy=True).itranspose(self._B_labels) for B in Bs]
        self._S = [None] * (self.L + 1)
        for i in range(self.L + 1)[self.nontrivial_bonds]:
            if isinstance(SVs[i], npc.Array):
                self._S[i] = SVs[i].copy()
            else:
                self._S[i] = np.array(SVs[i], dtype=np.float64)
        if self.bc == 'infinite':
            self._S[-1] = self._S[0]
        elif self.bc == 'finite':
            self._S[0] = self._S[-1] = np.ones([1], dtype=np.float64)
        self._transfermatrix_keep = 1
        self.test_sanity()

    def test_sanity(self):
        """Sanity check, raises ValueErrors, if something is wrong."""
        if self.bc not in self._valid_bc:
            raise ValueError("invalid boundary condition: " + repr(self.bc))
        if len(self._B) != self.L:
            raise ValueError("wrong len of self._B")
        if len(self._S) != self.L + 1:
            raise ValueError("wrong len of self._S")
        assert len(self.form) == self.L
        for f in self.form:
            if f is not None:
                assert isinstance(f, tuple)
                assert len(f) == 2
        for i, B in enumerate(self._B):
            if B.get_leg_labels() != self._B_labels:
                raise ValueError("B has wrong labels {0!r}, expected {1!r}".format(
                    B.get_leg_labels(), self._B_labels))
            if len(self._S[i+1].shape) == 1:
                if self._S[i].shape[-1] != B.get_leg('vL').ind_len or \
                        self._S[i+1].shape[0] != B.get_leg('vR').ind_len:
                    raise ValueError("shape of B incompatible with len of singular values")
                if not self.finite or i + 1 < self.L:
                    B2 = self._B[(i + 1) % self.L]
                    B.get_leg('vR').test_contractible(B2.get_leg('vL'))
            else:
                assert len(self._S[i+1].shape) == 2 # special case during DMRG with mixer,
                # important for simulation resume while mixer is on
                # we should have a well-defined form everywhere
                B = self.get_B(i, form='Th')
                B2 = self.get_B(i+1, form='B')
                # and be able to contract Th-B
                B.get_leg('vR').test_contractible(B2.get_leg('vL'))
                # (but not necessarily A-B, as we have it on the first bond at DMRG checkpoints)
            form = self.form[i]
            if form is not None:
                nuL, nuR = form
        if self.bc == 'finite':
            if len(self._S[0]) != 1 or len(self._S[-1]) != 1:
                raise ValueError("non-trivial outer bonds for finite MPS")
        elif self.bc == 'infinite':
            if np.any(self._S[self.L] != self._S[0]):
                raise ValueError("iMPS with S[0] != S[L]")

    def copy(self):
        """Returns a copy of `self`.

        The copy still shares the sites, chinfo, and LegCharges of the B tensors, but the values of
        B and S are deeply copied.
        """
        # __init__ makes deep copies of B, S
        cp = self.__class__(self.sites, self._B, self._S, self.bc, self.form, self.norm)
        cp.grouped = self.grouped
        cp._transfermatrix_keep = self._transfermatrix_keep
        cp.segment_boundaries = self.segment_boundaries
        return cp

    def save_hdf5(self, hdf5_saver, h5gr, subpath):
        """Export `self` into a HDF5 file.

        This method saves all the data it needs to reconstruct `self` with :meth:`from_hdf5`.

        Specifically, it saves
        :attr:`sites`,
        :attr:`chinfo` (under these names),
        :attr:`_B` as ``"tensors"``,
        :attr:`_S` as ``"singular_values"``,
        :attr:`bc` as ``"boundary_condition"``, and
        :attr:`form` converted to a single array of shape (L, 2) as ``"canonical_form"``,
        Moreover, it saves :attr:`norm`, :attr:`L`, :attr:`grouped` and
        :attr:`_transfermatrix_keep` (as "transfermatrix_keep") as HDF5 attributes, as well as
        the maximum of :attr:`chi` under the name "max_bond_dimension".

        Parameters
        ----------
        hdf5_saver : :class:`~tenpy.tools.hdf5_io.Hdf5Saver`
            Instance of the saving engine.
        h5gr : :class`Group`
            HDF5 group which is supposed to represent `self`.
        subpath : str
            The `name` of `h5gr` with a ``'/'`` in the end.
        """
        hdf5_saver.save(self.sites, subpath + "sites")
        hdf5_saver.save(self._B, subpath + "tensors")
        hdf5_saver.save(self._S, subpath + "singular_values")
        hdf5_saver.save(self.bc, subpath + "boundary_condition")
        hdf5_saver.save(np.array(self.form), subpath + "canonical_form")
        hdf5_saver.save(self.chinfo, subpath + "chinfo")
        segment_boundaries = getattr(self, "segment_boundaries", (None, None))
        hdf5_saver.save(self.segment_boundaries, subpath + "segment_boundaries")
        h5gr.attrs["norm"] = self.norm
        h5gr.attrs["grouped"] = self.grouped
        h5gr.attrs["transfermatrix_keep"] = self._transfermatrix_keep
        h5gr.attrs["L"] = self.L  # not needed for loading, but still usefull metadata
        h5gr.attrs["max_bond_dimension"] = np.max(self.chi)  # same

    @classmethod
    def from_hdf5(cls, hdf5_loader, h5gr, subpath):
        """Load instance from a HDF5 file.

        This method reconstructs a class instance from the data saved with :meth:`save_hdf5`.

        Parameters
        ----------
        hdf5_loader : :class:`~tenpy.tools.hdf5_io.Hdf5Loader`
            Instance of the loading engine.
        h5gr : :class:`Group`
            HDF5 group which is represent the object to be constructed.
        subpath : str
            The `name` of `h5gr` with a ``'/'`` in the end.

        Returns
        -------
        obj : cls
            Newly generated class instance containing the required data.
        """
        obj = cls.__new__(cls)  # create class instance, no __init__() call
        hdf5_loader.memorize_load(h5gr, obj)

        obj.sites = hdf5_loader.load(subpath + "sites")
        obj._B = hdf5_loader.load(subpath + "tensors")
        obj._S = hdf5_loader.load(subpath + "singular_values")
        obj.bc = hdf5_loader.load(subpath + "boundary_condition")
        form = hdf5_loader.load(subpath + "canonical_form")
        obj.form = [tuple(f) for f in form]
        obj.norm = hdf5_loader.get_attr(h5gr, "norm")

        obj.grouped = hdf5_loader.get_attr(h5gr, "grouped")
        obj._transfermatrix_keep = hdf5_loader.get_attr(h5gr, "transfermatrix_keep")
        obj.chinfo = hdf5_loader.load(subpath + "chinfo")
        obj.dtype = np.find_common_type([B.dtype for B in obj._B], [])
        if "segment_boundaries" in h5gr:
            obj.segment_boundaries = hdf5_loader.load(subpath + "segment_boundaries")
        else:
            obj.segment_boundaries = (None, None)
        obj.test_sanity()
        return obj

    @classmethod
    def from_lat_product_state(cls, lat, p_state, **kwargs):
        """Construct an MPS from a product state given in lattice coordinates.

        This is a wrapper around :meth:`from_product_state`.
        The purpuse is to make the `p_state` argument independent of the `order` of the `Lattice`,
        and specify it in terms of lattice indices instead.

        Parameters
        ----------
        lat : :class:`~tenpy.models.lattice.Lattice`
            The underlying lattice defining the geometry and Hilbert Space.
        p_state : array_like of {int | str | 1D array}
            Defines the product state to be represented.
            Should be of dimension `lat.dim`+1, entries are indexed by lattice indices.
            Entries of the array as for the `p_state` argument of :meth:`from_product_state`.
            It gets tiled to the shape ``lat.shape``, if it is smaller.
        **kwargs :
            Other keyword arguments as definied in :meth:`from_product_state`.
            `bc` is set by default from ``lat.bc_MPS``.

        Returns
        -------
        product_mps : :class:`MPS`
            An MPS representing the specified product state.

        Examples
        --------
        Let's first consider a :class:`~tenpy.models.lattice.Ladder` composed of a
        :class:`~tenpy.networks.site.SpinHalfSite` and a
        :class:`~tenpy.networks.site.FermionSite`.

        .. doctest : MPS.from_lat_product_state

            >>> spin_half = tenpy.networks.site.SpinHalfSite()
            >>> fermion = tenpy.networks.site.FermionSite()
            >>> ladder_i = tenpy.models.lattice.Ladder(2, [spin_half, fermion], bc_MPS="infinite")

        To initialize a state of up-spins on the spin sites and half-filled ferions, you can use:

        .. doctest : MPS.from_lat_product_state

            >>> p_state = [["up", "empty"], ["up", "full"]]
            >>> psi = tenpy.networks.MPS.from_lat_product_state(ladder_i, p_state)

        Note that the same `p_state` works for a finite lattice of even length, say ``L=10``, as
        well. We then just "tile" in x-direction, i.e., repeat the specified state 5 times:

        .. doctest : MPS.from_lat_product_state

            >>> ladder_f = tenpy.models.lattice.Ladder(10, [spin_half, fermion], bc_MPS="finite")
            >>> psi = tenpy.networks.MPS.from_lat_product_state(ladder_f, p_state)

        You can also easily half-fill a :class:`~tenpy.models.lattice.Honeycomb`, for example
        with only the `A` sites occupied, or as stripe parallel to the x-direction (`stripe_x`,
        alternating along `y` axis),
        or as stripes parallel to the y-direction (`stripe_y`, alternating along `x` axis).

        .. doctest : MPS.from_lat_product_state

            >>> honeycomb = tenpy.models.lattice.Honeycomb([4, 4], [fermion, fermion], bc_MPS="finite")
            >>> p_state_only_A = [[["empty", "full"]]]
            >>> psi_only_A = tenpy.networks.MPS.from_lat_product_state(honeycomb, p_state_only_A)
            >>> p_state_stripe_x = [[["empty", "empty"],
            ...                      ["full", "full"]]]
            >>> psi_stripe_x = tenpy.networks.MPS.from_lat_product_state(honeycomb, p_state_stripe_x)
            >>> p_state_stripe_y = [[["empty", "empty"]],
            ...                      [["full", "full"]]]
            >>> psi_stripe_y = tenpy.networks.MPS.from_lat_product_state(honeycomb, p_state_stripe_y)
        """
        kwargs.setdefault("bc", lat.bc_MPS)
        p_state = np.array(p_state, dtype=object)
        if p_state.ndim == len(lat.shape):  # == lat.dim + 1
            p_state = to_array(p_state, shape=lat.shape)  # tile to lattice shape
            p_state_flat = p_state[tuple(lat.order.T)]  # "advanced" numpy indexing
        elif p_state.ndim == len(lat.shape) + 1:
            # extra dimension could be from purely 1D array entries
            # make sure this is the case by converting to float
            p_state = np.array(p_state, kwargs.get("dtype", np.float64))
            # tile to lattice shape, ignore last dimension
            p_state = to_array(p_state, shape=lat.shape + (None, ))
            inds = tuple(lat.order.T) + (slice(None), )
            p_state_flat = p_state[inds]  # "advanced" numpy indexing
        else:
            raise ValueError("wrong dimension of `p_state`. Expected {d:d}-dimensional array of "
                             "(string, int, or 1D array)".format(d=lat.dim + 1))
        from ..models.lattice import HelicalLattice
        if isinstance(lat, HelicalLattice):
            order = lat.regular_lattice.order
            for start in range(0, lat.regular_lattice.N_sites, lat.N_sites):
                shifted_inds = tuple(order[start:start + lat.N_sites, :].T)
                if p_state.ndim == len(lat.shape) + 1:
                    shifted_inds = shifted_inds + (slice(None), )
                shifted_p_state_flat = p_state[shifted_inds]
                if not np.all(p_state_flat == shifted_p_state_flat):
                    raise ValueError("`p_state` not translation invariant w.r.t. HelicalLattice")
        return cls.from_product_state(lat.mps_sites(), p_state_flat, **kwargs)

    @classmethod
    def from_product_state(cls,
                           sites,
                           p_state,
                           bc='finite',
                           dtype=np.float64,
                           permute=True,
                           form='B',
                           chargeL=None):
        """Construct a matrix product state from a given product state.

        Parameters
        ----------
        sites : list of :class:`~tenpy.networks.site.Site`
            The sites defining the local Hilbert space.
        p_state : list of {int | str | 1D array}
            Defines the product state to be represented; one entry for each `site` of the MPS.
            An entry of `str` type is translated to an `int` with the help of
            :meth:`~tenpy.networks.site.Site.state_labels`.
            An entry of `int` type represents the physical index of the state to be used.
            An entry which is a 1D array defines the complete wavefunction on that site; this
            allows to make a (local) superposition.
        bc : {'infinite', 'finite', 'segmemt'}
            MPS boundary conditions. See docstring of :class:`MPS`.
        dtype : type or string
            The data type of the array entries.
        permute : bool
            The :class:`~tenpy.networks.Site` might permute the local basis states if charge
            conservation gets enabled.
            If `permute` is True (default), we permute the given `p_state` locally according to
            each site's :attr:`~tenpy.networks.Site.perm`.
            The `p_state` entries should then always be given as if `conserve=None` in the Site.
        form : (list of) {``'B' | 'A' | 'C' | 'G' | None`` | tuple(float, float)}
            Defines the canonical form. See module doc-string.
            A single choice holds for all of the entries.
        chargeL : charges
            Leg charges at bond 0, which are purely conventional.

        Returns
        -------
        product_mps : :class:`MPS`
            An MPS representing the specified product state.

        Examples
        --------
        Example to get a Neel state for a :class:`~tenpy.models.tf_ising.TFIChain`:

        .. doctest :: MPS.from_product_state

            >>> from tenpy.networks.mps import MPS
            >>> L = 10
            >>> M = tenpy.models.tf_ising.TFIChain({'L': L})
            >>> p_state = ["up", "down"] * (L//2)  # repeats entries L/2 times
            >>> psi = MPS.from_product_state(M.lat.mps_sites(), p_state, bc=M.lat.bc_MPS)

        The meaning of the labels ``"up","down"`` is defined by the :class:`~tenpy.networks.Site`,
        in this example a :class:`~tenpy.networks.site.SpinHalfSite`.

        Extending the example, we can replace the spin in the center with one with arbitrary
        angles ``theta, phi`` in the bloch sphere.
        However, note that you can not write this bloch state (for ``theta != 0, pi``) when
        conserving symmetries, as the two physical basis states correspond to different symmetry
        sectors.

        .. doctest :: MPS.from_product_state

            >>> spin = tenpy.networks.site.SpinHalfSite(conserve=None)
            >>> p_state = ["up", "down"] * (L//2)  # repeats entries L/2 times
            >>> theta, phi = np.pi/4, np.pi/6
            >>> bloch_sphere_state = np.array([np.cos(theta/2), np.exp(1.j*phi)*np.sin(theta/2)])
            >>> p_state[L//2] = bloch_sphere_state   # replace one spin in center
            >>> psi = MPS.from_product_state([spin]*L, p_state, bc=M.lat.bc_MPS, dtype=complex)

        Note that for the more general :class:`~tenpy.models.spins.SpinChain`,
        the order of the two entries for the ``bloch_sphere_state`` would be *exactly the opposite*
        (when we keep the the north-pole of the bloch sphere being the up-state).
        The reason is that the `SpinChain` uses the general :class:`~tenpy.networks.site.SpinSite`,
        where the states are orderd ascending from ``'down'`` to ``'up'``.
        The :class:`~tenpy.networks.site.SpinHalfSite` on the other hand uses the order
        ``'up', 'down'`` where that the Pauli matrices look as usual.
        """
        sites = list(sites)
        L = len(sites)
        p_state = list(p_state)
        if len(p_state) != L:
            raise ValueError("Length of p_state does not match number of sites.")
        ci = sites[0].leg.chinfo
        Bs = []
        chargeL = ci.make_valid(chargeL)  # sets to zero if `None`
        legL = npc.LegCharge.from_qflat(ci, [chargeL])  # (no need to bunch)
        for p_st, site in zip(p_state, sites):
            perm = permute
            if isinstance(p_st, str):
                p_st = site.state_labels[p_st]  # translate labels into "int"
                perm = False
            try:
                iter(p_st)
            except TypeError:
                # just an int for p_st
                B = np.zeros((site.dim, 1, 1), dtype)
                B[p_st, 0, 0] = 1.0
            else:  # iter works
                if len(p_st) != site.dim:
                    raise ValueError("p_state incompatible with local dim:" + repr(p_st))
                B = np.array(p_st, dtype).reshape((site.dim, 1, 1))
            if perm:
                B = B[site.perm, :, :]
            Bs.append(B)
        SVs = [[1.]] * (L + 1)
        return cls.from_Bflat(sites, Bs, SVs, bc, dtype, False, form, legL)

    @classmethod
    def from_Bflat(cls,
                   sites,
                   Bflat,
                   SVs=None,
                   bc='finite',
                   dtype=None,
                   permute=True,
                   form='B',
                   legL=None):
        """Construct a matrix product state from a set of numpy arrays `Bflat` and singular vals.

        Parameters
        ----------
        sites : list of :class:`~tenpy.networks.site.Site`
            The sites defining the local Hilbert space.
        Bflat : iterable of numpy ndarrays
            The matrix defining the MPS on each site, with legs ``'p', 'vL', 'vR'``
            (physical, virtual left/right).
        SVs : list of 1D array | ``None``
            The singular values on *each* bond. Should always have length `L+1`.
            By default (``None``), set all singular values to the same value.
            Entries out of :attr:`nontrivial_bonds` are ignored.
        bc : {'infinite', 'finite', 'segmemt'}
            MPS boundary conditions. See docstring of :class:`MPS`.
        dtype : type or string
            The data type of the array entries. Defaults to the common dtype of `Bflat`.
        permute : bool
            The :class:`~tenpy.networks.Site` might permute the local basis states if charge
            conservation gets enabled.
            If `permute` is True (default), we permute the given `Bflat` locally according to
            each site's :attr:`~tenpy.networks.Site.perm`.
            The `p_state` argument should then always be given as if `conserve=None` in the Site.
        form : (list of) {``'B' | 'A' | 'C' | 'G' | None`` | tuple(float, float)}
            Defines the canonical form of `Bflat`. See module doc-string.
            A single choice holds for all of the entries.
        leg_L : LegCharge | ``None``
            Leg charges at bond 0, which are purely conventional.
            If ``None``, use trivial charges.

        Returns
        -------
        mps : :class:`MPS`
            An MPS with the matrices `Bflat` converted to npc arrays.
        """
        sites = list(sites)
        L = len(sites)
        Bflat = list(Bflat)
        if len(Bflat) != L:
            raise ValueError("Length of Bflat does not match number of sites.")
        ci = sites[0].leg.chinfo
        if legL is None:
            legL = npc.LegCharge.from_qflat(ci, [ci.make_valid(None)] * Bflat[0].shape[1])
            legL = legL.bunch()[1]
        if SVs is None:
            SVs = [np.ones(B.shape[1]) / np.sqrt(B.shape[1]) for B in Bflat]
            SVs.append(np.ones(Bflat[-1].shape[2]) / np.sqrt(Bflat[-1].shape[2]))
        Bs = []
        if dtype is None:
            dtype = np.dtype(np.common_type(*Bflat))
        for i, site in enumerate(sites):
            B = np.array(Bflat[i], dtype)
            if permute:
                B = B[site.perm, :, :]
            # calculate the LegCharge of the right leg
            legs = [site.leg, legL, None]  # other legs are known
            legs = npc.detect_legcharge(B, ci, legs, None, qconj=-1)
            B = npc.Array.from_ndarray(B, legs, dtype)
            B.iset_leg_labels(['p', 'vL', 'vR'])
            Bs.append(B)
            legL = legs[-1].conj()  # prepare for next `i`
        if bc == 'infinite':
            # for an iMPS, the last leg has to match the first one.
            # so we need to gauge `qtotal` of the last `B` such that the right leg matches.
            chdiff = Bs[-1].get_leg('vR').charges[0] - Bs[0].get_leg('vL').charges[0]
            Bs[-1] = Bs[-1].gauge_total_charge('vR', ci.make_valid(chdiff))
        return cls(sites, Bs, SVs, form=form, bc=bc)

    @classmethod
    def from_full(cls,
                  sites,
                  psi,
                  form=None,
                  cutoff=1.e-16,
                  normalize=True,
                  bc='finite',
                  outer_S=None):
        """Construct an MPS from a single tensor `psi` with one leg per physical site.

        Performs a sequence of SVDs of psi to split off the `B` matrices and obtain the singular
        values, the result will be in canonical form.
        Obviously, this is only well-defined for `finite` or `segment` boundary conditions.

        Parameters
        ----------
        sites : list of :class:`~tenpy.networks.site.Site`
            The sites defining the local Hilbert space.
        psi : :class:`~tenpy.linalg.np_conserved.Array`
            The full wave function to be represented as an MPS.
            Should have labels ``'p0', 'p1', ...,  'p{L-1}'``.
            Additionally, it may have (or must have for 'segment' `bc`) the legs ``'vL', 'vR'``,
            which are trivial for 'finite' `bc`.
        form  : ``'B' | 'A' | 'C' | 'G' | None``
            The canonical form of the resulting MPS, see module doc-string.
            ``None`` defaults to 'A' form on the first site and 'B' form on all following sites.
        cutoff : float
            Cutoff of singular values used in the SVDs.
        normalize : bool
            Whether the resulting MPS should have 'norm' 1.
        bc : 'finite' | 'segment'
            Boundary conditions.
        outer_S : None | (array, array)
            For 'semgent' `bc` the singular values on the left and right of the considered segment,
            `None` for 'finite' boundary conditions.

        Returns
        -------
        psi_mps : :class:`MPS`
            MPS representation of `psi`, in canonical form and possibly normalized.
        """
        if form is not None and form not in ['B', 'A', 'C', 'G']:
            raise ValueError("Invalid form: " + repr(form))
        if bc != 'finite' and bc != 'segment':
            raise ValueError("Wrong boundary conditions: " + repr(bc))
        # perform SVDs to bring it into 'B' form, afterwards change the form.
        L = len(sites)
        assert (L >= 2)
        B_list = [None] * L
        S_list = [None] * (L + 1)
        norm = 1. if normalize else npc.norm(psi)
        if not psi.has_label('vL'):
            psi = psi.add_trivial_leg(0, label='vL', qconj=+1)
        elif bc == 'finite' and psi.get_leg('vL').ind_len != 1:
            raise ValueError("non-trivial left leg for 'finite' bc!")
        if not psi.has_label('vR'):
            psi = psi.add_trivial_leg(len(psi.get_leg_labels()), label='vR', qconj=-1)
        elif bc == 'finite' and psi.get_leg('vR').ind_len != 1:
            raise ValueError("non-trivial left leg for 'finite' bc!")
        labels = ['vL'] + ['p' + str(i) for i in range(L)] + ['vR']
        psi.itranspose(labels)
        # combine legs from left
        for i in range(0, L - 1):
            psi = psi.combine_legs([0, 1])  # combines the legs until `i`
        # now psi has only three legs: ``'(((vL.p0).p1)...p{L-2})', 'p{L-1}', 'vR'``
        for i in range(L - 1, 0, -1):
            # split off B[i]
            psi = psi.combine_legs([labels[i + 1], 'vR'])
            psi, S, B = npc.svd(psi, inner_labels=['vR', 'vL'], cutoff=cutoff)
            S /= np.linalg.norm(S)  # normalize
            if i > 1:
                psi.iscale_axis(S, 1)
            B_list[i] = B.split_legs(1).replace_label(labels[i + 1], 'p')
            S_list[i] = S
            psi = psi.split_legs(0)
        # psi is now the first `B` in 'A' form
        B_list[0] = psi.replace_label(labels[1], 'p')
        B_form = ['A'] + ['B'] * (L - 1)
        if bc == 'finite':
            S_list[0] = S_list[-1] = np.ones([1], dtype=np.float64)
        elif outer_S is not None:
            S_list[0], S_list[-1] = outer_S
        res = cls(sites, B_list, S_list, bc=bc, form=B_form, norm=norm)
        if form is not None:
            res.convert_form(form)
        return res

    @classmethod
    def from_singlets(cls,
                      site,
                      L,
                      pairs,
                      up='up',
                      down='down',
                      lonely=[],
                      lonely_state='up',
                      bc='finite'):
        """Create an MPS of entangled singlets.

        Parameters
        ----------
        site : :class:`~tenpy.networks.site.Site`
            The `site` defining the local Hilbert space, taken uniformly for all sites.
        L : int
            The number of sites.
        pairs : list of (int, int)
            Pairs of sites to be entangled; the returned MPS will have a singlet
            for each pair in `pairs`.
        up, down : int | str
            A singlet is defined as ``(|up down> - |down up>)/2**0.5``,
            ``up`` and ``down`` give state indices or labels defined on the corresponding site.
        lonely : list of int
            Sites which are not included into a singlet pair.
        lonely_state : int | str
            The state for the lonely sites.
        bc : {'infinite', 'finite', 'segmemt'}
            MPS boundary conditions. See docstring of :class:`MPS`.

        Returns
        -------
        singlet_mps : :class:`MPS`
            An MPS representing singlets on the specified pairs of sites.
        """
        # sort each pair s.t. i < j
        pairs = [((i, j) if i < j else (j, i)) for (i, j) in pairs]
        # sort by smaller site of the pair
        pairs.sort(key=lambda x: x[0])
        pairs.append((L, L))
        lonely = sorted(lonely) + [L]
        # generate building block tensors
        up = site.state_index(up)
        down = site.state_index(down)
        lonely_state = site.state_index(lonely_state)
        mask = np.zeros(site.dim, dtype=np.bool_)
        mask[up] = mask[down] = True
        Open = npc.diag(1., site.leg)[:, mask]
        Close = np.zeros([site.dim, site.dim], dtype=np.float_)
        Close[up, down] = 1.
        Close[down, up] = -1.
        Close = npc.Array.from_ndarray(Close, [site.leg, site.leg])  # no conj() !
        Close = Close[mask, :]
        Id = npc.eye_like(Close, 0)
        Lonely = np.zeros(site.dim, dtype=np.float_)
        Lonely[lonely_state] = 1
        Lonely = npc.Array.from_ndarray(Lonely, [site.leg])
        Bs = []
        Ss = [np.ones(1)]
        forms = []
        open_singlets = []  # the k-th open singlet should be closed at site open_singlets[k]
        Ts = []  # the tensors on the current site
        labels_L = []
        for i in range(L):
            labels_R = labels_L[:]
            next_Ts = Ts[:]
            if i == pairs[0][0]:  # open a new singlet
                j = pairs[0][1]
                lbl = 's{0:d}-{1:d}'.format(i, j)
                pairs.pop(0)
                open_singlets.append(j)
                next_Ts.append(Id.copy().iset_leg_labels([lbl + 'L', lbl]))
                Open.iset_leg_labels(['p', lbl])
                Ts.append(Open.copy(deep=False))
                labels_R.append(lbl)
                forms.append('A')
            elif i == lonely[0]:  # just a lonely state
                Ts.append(Lonely)
                lonely.pop(0)
                forms.append('B')
            else:  # close a singlet
                k = open_singlets.index(i)
                Close.iset_leg_labels([labels_L[k] + 'L', 'p'])
                Ts[k] = Close
                next_Ts.pop(k)
                open_singlets.pop(k)
                labels_R.pop(k)
                forms.append('B')
            # generate `B` from `Ts`
            B = reduce(npc.outer, Ts)
            labels_L = [lbl_ + 'L' for lbl_ in labels_L]
            if len(labels_L) > 0 and len(labels_R) > 0:
                B = B.combine_legs([labels_L, labels_R], new_axes=[0, 2], qconj=[+1, -1])
                B.iset_leg_labels(['vL', 'p', 'vR'])
            elif len(labels_L) == 0 and len(labels_R) == 0:
                B = B.add_trivial_leg(0, label='vL', qconj=+1)
                B = B.add_trivial_leg(2, label='vR', qconj=-1)
                B.iset_leg_labels(['vL', 'p', 'vR'])
            elif len(labels_L) == 0:
                B = B.combine_legs([labels_R], new_axes=[1], qconj=[-1])
                B.iset_leg_labels(['p', 'vR'])
                B = B.add_trivial_leg(0, label='vL', qconj=+1)
            else:  # len(labels_R) == 0
                B = B.combine_legs([labels_L], new_axes=[0], qconj=[+1])
                B.iset_leg_labels(['vL', 'p'])
                B = B.add_trivial_leg(2, label='vR', qconj=-1)
            Bs.append(B)
            N = 2**len(labels_R)
            Ss.append(np.ones(N) / (N**0.5))
            Ts = next_Ts
            labels_L = labels_R
        return cls([site] * L, Bs, Ss, bc=bc, form=forms)

    @property
    def L(self):
        """Number of physical sites; for an iMPS the len of the MPS unit cell."""
        return len(self.sites)

    @property
    def dim(self):
        """List of local physical dimensions."""
        return [site.dim for site in self.sites]

    @property
    def finite(self):
        """Distinguish MPS vs iMPS.

        True for an MPS (``bc='finite', 'segment'``), False for an iMPS (``bc='infinite'``).
        """
        assert (self.bc in self._valid_bc)
        return self.bc != 'infinite'

    @property
    def chi(self):
        """Dimensions of the (nontrivial) virtual bonds."""
        # s.shape[0] == len(s) for 1D numpy array, but works also for a 2D npc Array.
        return [min(s.shape) for s in self._S[self.nontrivial_bonds]]

    @property
    def nontrivial_bonds(self):
        """Slice of the non-trivial bond indices, depending on ``self.bc``."""
        if self.bc == 'finite':
            return slice(1, self.L)
        elif self.bc == 'segment':
            return slice(0, self.L + 1)
        elif self.bc == 'infinite':
            return slice(0, self.L)

    def get_B(self, i, form='B', copy=False, cutoff=1.e-16, label_p=None):
        """Return (view of) `B` at site `i` in canonical form.

        Parameters
        ----------
        i : int
            Index choosing the site.
        form : ``'B' | 'A' | 'C' | 'G' | 'Th' | None`` | tuple(float, float)
            The (canonical) form of the returned B.
            For ``None``, return the matrix in whatever form it is.
            If any of the tuple entry is None, also don't scale on the corresponding axis.
        copy : bool
            Whether to return a copy even if `form` matches the current form.
        cutoff : float
            During DMRG with a mixer, `S` may be a matrix for which we need the inverse.
            This is calculated as the Penrose pseudo-inverse, which uses a cutoff for the
            singular values.
        label_p : None | str
            Ignored by default (``None``).
            Otherwise replace the physical label ``'p'`` with ``'p'+label_p'``.
            (For derived classes with more than one "physical" leg, replace all the physical leg
            labels accordingly.)

        Returns
        -------
        B : :class:`~tenpy.linalg.np_conserved.Array`
            The MPS 'matrix' `B` at site `i` with leg labels ``'vL', 'p', 'vR'``.
            May be a view of the matrix (if ``copy=False``),
            or a copy (if the form changed or ``copy=True``).

        Raises
        ------
        ValueError : if self is not in canoncial form and `form` is not None.
        """
        i = self._to_valid_index(i)
        new_form = self._to_valid_form(form)
        old_form = self.form[i]
        B = self._B[i]
        if copy:
            B = B.copy()
        if new_form is not None and old_form != new_form:
            if old_form is None:
                raise ValueError("can't convert form of non-canonical state!")
            if new_form[0] is not None and new_form[0] - old_form[0] != 0.:
                B = self._scale_axis_B(B, self.get_SL(i), new_form[0] - old_form[0], 'vL', cutoff)
            if new_form[1] is not None and new_form[1] - old_form[1] != 0.:
                B = self._scale_axis_B(B, self.get_SR(i), new_form[1] - old_form[1], 'vR', cutoff)
        if label_p is not None:
            B = self._replace_p_label(B, label_p)
        return B

    def set_B(self, i, B, form='B'):
        """Set `B` at site `i`.

        Parameters
        ----------
        i : int
            Index choosing the site.
        B : :class:`~tenpy.linalg.np_conserved.Array`
            The 'matrix' at site `i`. No copy is made!
            Should have leg labels ``'vL', 'p', 'vR'`` (not necessarily in that order).
        form : ``'B' | 'A' | 'C' | 'G' | 'Th' | None`` | tuple(float, float)
            The (canonical) form of the `B` to set.
            ``None`` stands for non-canonical form.
        """
        i = self._to_valid_index(i)
        self.form[i] = self._to_valid_form(form)
        self.dtype = np.find_common_type([self.dtype, B.dtype], [])
        self._B[i] = B.itranspose(self._B_labels)

    def set_svd_theta(self, i, theta, trunc_par=None, update_norm=False):
        """SVD a two-site wave function `theta` and save it in `self`.

        Parameters
        ----------
        i : int
            `theta` is the wave function on sites `i`, `i` + 1.
        theta : :class:`~tenpy.linalg.np_conserved.Array`
            The two-site wave function with labels combined into ``"(vL.p0)", "(p1.vR)"``,
            ready for svd.
        trunc_par : None | dict
            Parameters for truncation, see :cfg:config:`truncation`.
            If ``None``, no truncation is done.
        update_norm : bool
            If ``True``, multiply the norm of `theta` into :attr:`norm`.
        """
        i0 = self._to_valid_index(i)
        i1 = self._to_valid_index(i0 + 1)
        self.dtype = np.find_common_type([self.dtype, theta.dtype], [])
        qtotal_LR = [self._B[i0].qtotal, None]
        if trunc_par is None:
            U, S, VH = npc.svd(theta, qtotal_LR=qtotal_LR, inner_labels=['vR', 'vL'])
            renorm = np.linalg.norm(S)
            S /= renorm
            err = None
            if update_norm:
                self.norm *= renorm
        else:
            U, S, VH, err, renorm = svd_theta(theta, trunc_par, qtotal_LR)
            if update_norm:
                self.norm *= renorm
        U = U.split_legs().ireplace_label('p0', 'p')
        VH = VH.split_legs().ireplace_label('p1', 'p')
        self._B[i0] = U.itranspose(self._B_labels)
        self.form[i0] = self._valid_forms['A']
        self._B[i1] = VH.itranspose(self._B_labels)
        self.form[i1] = self._valid_forms['B']
        self.set_SR(i, S)
        return err

    def get_SL(self, i):
        """Return singular values on the left of site `i`"""
        i = self._to_valid_index(i)
        return self._S[i]

    def get_SR(self, i):
        """Return singular values on the right of site `i`"""
        i = self._to_valid_index(i)
        return self._S[i + 1]

    def set_SL(self, i, S):
        """Set singular values on the left of site `i`"""
        i = self._to_valid_index(i)
        self._S[i] = S
        if not self.finite and i == 0:
            self._S[self.L] = S

    def set_SR(self, i, S):
        """Set singular values on the right of site `i`"""
        i = self._to_valid_index(i)
        self._S[i + 1] = S
        if not self.finite and i == self.L - 1:
            self._S[0] = S

    def get_op(self, op_list, i):
        """Given a list of operators, select the one corresponding to site `i`.

        Parameters
        ----------
        op_list : (list of) {str | npc.array}
            List of operators from which we choose. We assume that ``op_list[j]`` acts on site
            ``j``. If the length is shorter than `L`, we repeat it periodically.
            Strings are translated using :meth:`~tenpy.networks.site.Site.get_op` of site `i`.
        i : int
            Index of the site on which the operator acts.

        Returns
        -------
        op : npc.array
            One of the entries in `op_list`, not copied.
        """
        if self.finite and (i > self.L or i < 0):
            raise ValueError("i = {0:d} out of bounds for finite MPS".format(i))
        op = op_list[i % len(op_list)]
        if (isinstance(op, str)):
            op = self.sites[i % self.L].get_op(op)
        return op

    def get_theta(self, i, n=2, cutoff=1.e-16, formL=1., formR=1.):
        """Calculates the `n`-site wavefunction on ``sites[i:i+n]``.

        Parameters
        ----------
        i : int
            Site index.
        n : int
            Number of sites. The result lives on ``sites[i:i+n]``.
        cutoff : float
            During DMRG with a mixer, `S` may be a matrix for which we need the inverse.
            This is calculated as the Penrose pseudo-inverse, which uses a cutoff for the
            singular values.
        formL : float
            Exponent for the singular values to the left.
        formR : float
            Exponent for the singular values to the right.

        Returns
        -------
        theta : :class:`~tenpy.linalg.np_conserved.Array`
            The n-site wave function with leg labels ``vL, p0, p1, .... p{n-1}, vR``.
            In Vidal's notation (with s=lambda, G=Gamma):
            ``theta = s**form_L G_i s G_{i+1} s ... G_{i+n-1} s**form_R``.
        """
        i = self._to_valid_index(i)
        for j in range(i, i + n):
            if self.form[j % self.L] is None:
                raise ValueError("can't calculate theta for non-canonical form")
        if n == 1:
            return self.get_B(i, (1., 1.), True, cutoff, '0')
        elif n < 1:
            raise ValueError("n needs to be larger than 0")
        # n >= 2: contract some B's
        theta = self.get_B(i, (formL, None), False, cutoff, '0')  # right form as stored
        _, old_fR = self.form[i]
        for k in range(1, n):  # non-empty range
            j = self._to_valid_index(i + k)
            new_fR = None if k + 1 < n else formR  # right form as stored, except for last B
            B = self.get_B(j, (1. - old_fR, new_fR), False, cutoff, str(k))
            _, old_fR = self.form[j]
            theta = npc.tensordot(theta, B, axes=['vR', 'vL'])
        return theta

    def convert_form(self, new_form='B'):
        """Tranform self into different canonical form (by scaling the legs with singular values).

        Parameters
        ----------
        new_form : (list of) {``'B' | 'A' | 'C' | 'G' | 'Th' | None`` | tuple(float, float)}
            The form the stored 'matrices'. The table in module doc-string.
            A single choice holds for all of the entries.

        Raises
        ------
        ValueError : if trying to convert from a ``None`` form. Use :meth:`canonical_form` instead!
        """
        new_forms = self._parse_form(new_form)
        for i, new_form in enumerate(new_forms):
            new_B = self.get_B(i, form=new_form, copy=False)  # calculates the desired form.
            self.set_B(i, new_B, form=new_form)

    def increase_L(self, new_L=None):
        """Modify `self` inplace to enlarge the MPS unit cell; in place.

        .. deprecated :: 0.5.1
            This method will be removed in version 1.0.0.
            Use the equivalent ``psi.enlarge_mps_unit_cell(new_L//psi.L)`` instead of
            ``psi.increase_L(new_L)``.

        Parameters
        ----------
        new_L : int
            New number of sites. Needs to be an integer multiple of :attr:`L`.
            Defaults to ``2*self.L``.
        """
        old_L = self.L
        if new_L is None:
            new_L = 2 * old_L
        if new_L % old_L:
            raise ValueError("new_L = {0:d} not a multiple of old L={1:d}".format(new_L, old_L))
        factor = new_L // old_L
        warnings.warn(
            "use `psi.enlarge_mps_unit_cell(factor=new_L//psi.L)` "
            "instead of `psi.increase_L(new_L)`.", FutureWarning, 2)
        self.enlarge_mps_unit_cell(factor)

    def enlarge_mps_unit_cell(self, factor=2):
        """Repeat the unit cell for infinite MPS boundary conditions; in place.

        Parameters
        ----------
        factor : int
            The new number of sites in the unit cell will be increased from `L` to ``factor*L``.
        """
        if int(factor) != factor:
            raise ValueError("`factor` should be integer!")
        if factor <= 1:
            raise ValueError("can't shrink!")
        if self.bc == 'segment':
            raise ValueError("can't enlarge segment MPS")
        self.sites = factor * self.sites
        self._B = factor * self._B
        self._S = factor * self._S[:-1] + [self._S[-1]]
        self.form = factor * self.form
        self.test_sanity()

    def roll_mps_unit_cell(self, shift=1):
        """Shift the section we define as unit cellof an infinite MPS; in place.

        Suppose we have a unit cell with tensors ``[A, B, C, D]`` (repeated on both sites).
        With ``shift = 1``, the new unit cell will be ``[D, A, B, C]``,
        whereas ``shift = -1`` will give ``[B, C, D, A]``.

        Parameters
        ----------
        shift : int
            By how many sites to move the tensors to the right.
        """
        if self.finite:
            raise ValueError("makes only sense for infinite boundary conditions")
        inds = np.roll(np.arange(self.L), shift)
        self.sites = [self.sites[i] for i in inds]
        self.form = [self.form[i] for i in inds]
        self._B = [self._B[i] for i in inds]
        self._S = [self._S[i] for i in inds]
        self._S.append(self._S[0])

    def spatial_inversion(self):
        """Perform a spatial inversion along the MPS.

        Exchanges the first with the last tensor and so on,
        i.e., exchange site `i` with site ``L-1 - i``.
        This is equivalent to a mirror/reflection with the bond left of L/2 (even L) or the site
        (L-1)/2 (odd L) as a fixpoint.
        For infinite MPS, the bond between MPS unit cells is another fix point.
        """
        self.sites = self.sites[::-1]
        self.form = [(f if f is None else (f[1], f[0])) for f in self.form[::-1]]
        self._B = [
            B.replace_labels(['vL', 'vR'], ['vR', 'vL']).transpose(self._B_labels)
            for B in self._B[::-1]
        ]
        self._S = self._S[::-1]
        self.test_sanity()
        return self

    def group_sites(self, n=2, grouped_sites=None):
        """Modify `self` inplace to group sites.

        Group each `n` sites together using the :class:`~tenpy.networks.site.GroupedSite`.
        This might allow to do TEBD with a Trotter decomposition,
        or help the convergence of DMRG (in case of too long range interactions).

        Parameters
        ----------
        n : int
            Number of sites to be grouped together.
        grouped_sites : None | list of :class:`~tenpy.networks.site.GroupedSite`
            The sites grouped together.

        See also
        --------
        group_split : Reverts the grouping.
        """
        self.convert_form('B')
        if grouped_sites is None:
            grouped_sites = group_sites(self.sites, n, charges='same')
        else:
            assert grouped_sites[0].n_sites == n
        Bs = []
        Ss = []
        i = 0
        B_form = self._valid_forms['B']
        for gs in grouped_sites:
            n_sites = gs.n_sites
            new_B = self.get_theta(i, gs.n_sites, formL=B_form[0], formR=B_form[1])
            comb_legs = [[lbl + str(k) for k in range(n_sites)] for lbl in self._p_label]
            # comb_legs = [['p0', 'p1', ... ]] for usual MPS
            axes = list(range(1, 1 + len(self._p_label)))  # [1]
            new_B = new_B.combine_legs(comb_legs, new_axes=axes, qconj=[+1] * len(axes))
            new_B.legs[1].test_equal(gs.leg)  # test legcharge compatibility
            Bs.append(new_B.iset_leg_labels(self._B_labels))  # ['vL', 'p', 'vR']
            Ss.append(self._S[i])
            i += n_sites
        Ss.append(self._S[-1])  # right-most singular values: need L+1 entries
        self._B = Bs
        self._S = Ss
        self.sites = grouped_sites
        self.form = [B_form] * len(grouped_sites)
        self.grouped = self.grouped * n

    def group_split(self, trunc_par=None):
        """Modify `self` inplace to split previously grouped sites.

        Parameters
        ----------
        trunc_par : dict
            Parameters for truncation, see :cfg:config:`truncation`.
            Defaults to ``{'chi_max': max(self.chi)}``.

        Returns
        -------
        trunc_err : :class:`~tenpy.algorithms.truncation.TruncationError`
            The error introduced by the truncation for the splitting.

        See also
        --------
        group_sites : Should have been used before to combine sites.
        """
        if trunc_par is None:
            trunc_par = {}
        self.convert_form('B')
        if self.L > 1:
            trunc_par.setdefault('chi_max', max(self.chi))
        n0 = self.sites[0].n_sites
        sites = []
        Bs = []
        Ss = []
        trunc_err = TruncationError()
        for i, gs in enumerate(self.sites):
            sites.extend(gs.sites)
            n = gs.n_sites
            Ss_new = []
            Bs_new = []
            B_gr = self.get_B(i, form='B').transpose(self._B_labels)  # vL, p, vR
            B_gr.idrop_labels(self._p_label)  # avoid warning: split label not called '(...)'
            n_p_label = len(self._p_label)
            split_legs = list(range(1, 1 + n_p_label))
            transp = [i for k in range(n) for i in range(1 + k, 1 + n * n_p_label, n)]
            transp = ['vL'] + transp + ['vR']
            B_gr = B_gr.split_legs(split_legs).itranspose(transp)
            theta = self.get_theta(i, n=1)
            theta.idrop_labels(self._get_p_label('0'))  # avoid warning
            theta = theta.split_legs(split_legs).itranspose(transp)
            # for usual MPS, B_gr and theta have legs vL p0 p1 ... p{n-1} vR
            # for MPS with legs p, q, they have legs vL p0 q0 p1 q1 ... q{-n-1} vR
            combine = [list(range(B_gr.rank - n_p_label - 1)), list(range(-n_p_label - 1, 0, +1))]
            # combine = [[0, 1, .... {n-1}], [-2, -1]] for usual MPS
            axes_contr = [combine[1], list(range(1, 2 + n_p_label))]
            for j in range(n - 1, 0, -1):
                # split off the right-most physical leg and vR from theta
                # theta: vL p0 ... pj vR
                theta = theta.combine_legs(combine, qconj=[+1, -1])
                U, S, V, err, _ = svd_theta(theta, trunc_par, inner_labels=['vR', 'vL'])
                Ss_new.append(S)
                trunc_err += err
                theta = U.split_legs(0)  # vL p0 ... pj-1 vR
                for _ in range(n_p_label):
                    combine[0].pop()
                B = V.split_legs(1).iset_leg_labels(self._B_labels)  # vL p vR
                B_gr = npc.tensordot(B_gr, B.conj(), axes=axes_contr)  # vL p0 ... pj-1 vR
                Bs_new.append(B)
            Bs_new.append(B_gr.iset_leg_labels(self._B_labels))  # inversion free :)
            Ss_new.append(self.get_SL(i))
            Bs.extend(Bs_new[::-1])
            Ss.extend(Ss_new[::-1])
        Ss.append(self._S[-1])
        self.sites = sites
        self._B = Bs
        self._S = Ss
        self.grouped = max(self.grouped // n0, 1)
        self.form = [self._valid_forms['B']] * len(sites)
        self.test_sanity()
        return trunc_err

    def get_grouped_mps(self, blocklen):
        r"""Like :meth:`group_sites`, but make a copy.

        Parameters
        ----------
        blocklen: int
            Number of subsequent sites to be combined; `n` in :meth:`group_sites`.

        Returns
        -------
        grouped_MPS :
            New MPS object with bunched sites.
        """
        groupedMPS = self.copy()
        groupedMPS.group_sites(n=blocklen)
        return groupedMPS

    def extract_segment(self, first, last):
        """Extract an segment from a finite or infinite MPS.

        Parameters
        ----------
        first, last : int
            The first and last site to *include* into the segment.

        Returns
        -------
        psi_segment : :class:`MPS`
            Copy of self with 'segment' boundary conditions.
        """
        L = self.L
        sites = [self.sites[i % L] for i in range(first, last + 1)]
        B = [self.get_B(i) for i in range(first, last + 1)]
        S = [self.get_SL(i) for i in range(first, last + 1)]
        S.append(self.get_SR(last))
        # note: __init__ makes deep copies of B, S
        cp = self.__class__(sites, B, S, 'segment', 'B', self.norm)
        cp.grouped = self.grouped
        return cp

    def get_total_charge(self, only_physical_legs=False):
        """Calculate and return the `qtotal` of the whole MPS (when contracted).

        If set, the :attr:`segment_boundaries` are included (unless `only_physical_legs` is True).

        Parameters
        ----------
        only_physical_legs : bool
            For ``'finite'`` boundary conditions, the total charge can be gauged away
            by changing the LegCharge of the trivial legs on the left and right of the MPS.
            This option allows to project out the trivial legs to get the actual "physical"
            total charge.

        Returns
        -------
        qtotal : charges
            The sum of the `qtotal` of the individual `B` tensors.
        """
        tensors = self._B
        U, V = self.segment_boundaries
        if U is not None:
            assert V is not None
            tensors = tensors + [U, V]
        qtotal = np.sum([B.qtotal for B in tensors], axis=0)
        if only_physical_legs:
            if self.bc != 'finite':
                raise ValueError("`only_physical_legs` not supported for bc=" + repr(self.bc))
            qtotal -= self._B[0].get_leg('vL').get_charge(0)
            qtotal -= self._B[-1].get_leg('vR').get_charge(0)  # takes qconj into account
        return self.chinfo.make_valid(qtotal)

    def gauge_total_charge(self, qtotal=None, vL_leg=None, vR_leg=None):
        """Gauge the legcharges of the virtual bonds such that the MPS has a total `qtotal`.

        Parameters
        ----------
        qtotal : (list of) charges
            If a single set of charges is given, it is the desired total charge of the MPS
            (which :meth:`get_total_charge` will return afterwards).
            By default (``None``), use 0 charges, unless vL_leg and vR_leg are specified, in which
            case we adjust the total charge to match these legs.
        vL_leg, vR_leg: None | LegCharge
            Desired new virtual leg on the very left and right.
            Needs to have the same block strucuture as the current legs, but can have shifted
            charge entries.
            For infinite MPS, we need `vL_leg` to be the conjugate leg of `vR_leg`.
            For segment MPS, these legs are the *outer-most* legs, possibly including the
            :attr:`segment_boundaries`.
        """
        if self.chinfo.qnumber == 0:
            return
        if self.segment_boundaries[0] is not None:
            raise NotImplementedError("could be implemented.... do you need this?")
        if vL_leg is not None:
            vL_chdiff = vL_leg.get_charge(0) - self._B[0].get_leg('vL').get_charge(0)
        if vR_leg is not None:
            vR_chdiff = vR_leg.get_charge(0) - self._B[-1].get_leg('vR').get_charge(0)
        if qtotal is None:
            if vL_leg is not None and vR_leg is not None:
                qtotal = self.get_total_charge() + vL_chdiff + vR_chdiff
        qtotal = self.chinfo.make_valid(qtotal)
        if qtotal.ndim == 1:
            qtotal_factor = np.array([0] * (self.L - 1) + [1], npc.QTYPE)
            qtotal = qtotal_factor[:, np.newaxis] * qtotal[np.newaxis, :]
        if qtotal.shape != (self.L, self.chinfo.qnumber):
            raise ValueError("wrong shape of `qtotal`")
        if vL_leg is not None:
            B = self._B[0]
            if np.any(vL_chdiff != 0):
                # adjust left leg
                self._B[0] = B.gauge_total_charge('vL', B.qtotal + vL_chdiff, vL_leg.qconj)
            B.get_leg('vL').test_equal(vL_leg)
        for i in range(self.L):
            B = self._B[i]
            desired_qtotal = qtotal[i]
            chdiff = B.qtotal - desired_qtotal
            if np.any(chdiff != 0):
                self._B[i] = B.gauge_total_charge('vR', desired_qtotal)
                if i + 1 != self.L:  # this 'vR' is contracted with the 'vL' of the next B
                    # so we need to adjust the next B as well
                    nextB = self._B[i + 1]
                    self._B[i + 1] = nextB.gauge_total_charge('vL', nextB.qtotal + chdiff)
                    self._B[i].get_leg('vR').test_contractible(self._B[i + 1].get_leg('vL'))
        # just to check
        assert np.all(self.get_total_charge() == self.chinfo.make_valid(np.sum(qtotal, 0)))
        if vR_leg is not None:
            # check that the charges match
            self._B[-1].get_leg('vR').test_equal(vR_leg)
        if self.bc == 'infinite':
            self._B[0].get_leg('vL').test_contractible(self._B[-1].get_leg('vR'))
        # done

    def entanglement_entropy(self, n=1, bonds=None, for_matrix_S=False):
        r"""Calculate the (half-chain) entanglement entropy for all nontrivial bonds.

        Consider a bipartition of the sytem into :math:`A = \{ j: j <= i_b \}` and
        :math:`B = \{ j: j > i_b\}` and the reduced density matrix :math:`\rho_A = tr_B(\rho)`.
        The von-Neumann entanglement entropy is defined as
        :math:`S(A, n=1) = -tr(\rho_A \log(\rho_A)) = S(B, n=1)`.
        The generalization for ``n != 1, n>0`` are the Renyi entropies:
        :math:`S(A, n) = \frac{1}{1-n} \log(tr(\rho_A^2)) = S(B, n=1)`

        This function calculates the entropy for a cut at different bonds `i`, for which the
        the eigenvalues of the reduced density matrix :math:`\rho_A` and :math:`\rho_B` is given
        by the squared schmidt values `S` of the bond.

        Parameters
        ----------
        n : int/float
            Selects which entropy to calculate;
            `n=1` (default) is the ususal von-Neumann entanglement entropy.
        bonds : ``None`` | (iterable of) int
            Selects the bonds at which the entropy should be calculated.
            ``None`` defaults to ``range(0, L+1)[self.nontrivial_bonds]``,
            i.e., ``range(1, L)`` for 'finite' MPS and ``range(0, L)`` for 'infinite' MPS.
        for_matrix_S : bool
            Switch calculate the entanglement entropy even if the `_S` are matrices.
            Since :math:`O(\chi^3)` is expensive compared to the ususal :math:`O(\chi)`,
            we raise an error by default.

        Returns
        -------
        entropies : 1D ndarray
            Entanglement entropies for half-cuts.
            `entropies[j]` contains the entropy for a cut at bond ``bonds[j]``,
            i.e. between sites ``bonds[j]-1`` and ``bonds[j]``.
            For **infinite** systems with default ``bonds=None``,
            this means that ``entropies[0]`` will be a cut left of site 0 and is the one you should
            look at to e.g. study the scaling of the entanglement with `chi` or to extract
            the topological entanglement entropy - don't take the average over bonds,
            in particular if you have 2D cylinders or ladders.
            On the contrary, for **finite** systems with ``bonds=None``, take the central value
            of the returned array ``entropies[len(entropies)//2)] == entropies[(L-1)//2]``
            (and **not** just ``entropies[L//2]``) to extract the half-chain entanglement entropy.
        """
        if bonds is None:
            nt = self.nontrivial_bonds
            bonds = range(nt.start, nt.stop)
        res = []
        for ib in bonds:
            s = self._S[ib]
            if len(s.shape) == 1:
                res.append(entropy(s**2, n))
            else:
                if for_matrix_S:
                    # explicitly calculate Schmidt values by diagonalizing (s^dagger s)
                    s = npc.eigvalsh(npc.tensordot(s.conj(), s, axes=[0, 0]))
                    res.append(entropy(s, n))
                else:
                    raise ValueError("entropy with non-diagonal schmidt values")
        return np.array(res)

    def entanglement_entropy_segment(self, segment=[0], first_site=None, n=1):
        r"""Calculate entanglement entropy for general geometry of the bipartition.

        This function is similar as :meth:`entanglement_entropy`,
        but for more general geometry of the region `A` to be a segment of a *few* sites.

        This is acchieved by explicitly calculating the reduced density matrix of `A`
        and thus works only for small segments.
        The alternative :meth:`entanglement_entropy_segment2` might work for larger segments
        at small enough bond dimensions.

        Parameters
        ----------
        segment : list of int
            Given a first site `i`, the region ``A_i`` is defined to be ``[i+j for j in segment]``.
        first_site : ``None`` | (iterable of) int
            Calculate the entropy for segments starting at these sites.
            ``None`` defaults to ``range(L-segment[-1])`` for finite
            or `range(L)` for infinite boundary conditions.
        n : int | float
            Selects which entropy to calculate;
            `n=1` (default) is the ususal von-Neumann entanglement entropy,
            otherwise the `n`-th Renyi entropy.

        Returns
        -------
        entropies : 1D ndarray
            ``entropies[i]`` contains the entropy for the the region ``A_i`` defined above.
        """
        segment = np.sort(segment)
        if first_site is None:
            if self.finite:
                first_site = range(0, self.L - segment[-1])
            else:
                first_site = range(self.L)
        comb_legs = [
            self._get_p_labels(len(segment), False),
            self._get_p_labels(len(segment), True)
        ]
        res = []
        for i0 in first_site:
            rho = self.get_rho_segment(segment + i0)
            rho = rho.combine_legs(comb_legs, qconj=[+1, -1])
            p = npc.eigvalsh(rho)
            res.append(entropy(p, n))
        return np.array(res)

    def entanglement_entropy_segment2(self, segment, n=1):
        r"""Calculate entanglement entropy for general geometry of the bipartition.

        This function is similar to :meth:`entanglement_entropy_segment`,
        but allows more sites in `segment`.
        The trick is to exploit that for a pure state (which the MPS represents) and a bipartition
        into regions A and B, the entropy is the same in both regions, :math:`S(A) = S(B)`.
        Hence we can trace out the specified segment and obtain :math:`\rho_B = tr_A(rho)`, where
        A is the specified `segment`.
        The price is a *huge* computation cost of :math:`O(chi^6 d^{3x})` where `x` is the number
        of physical legs not included into `segment` between `min(segment)` and `max(segment)`.

        Parameters
        ----------
        segment : list of int
            The site indices specifying region `A`. We calculate and diagonalize
            the full reduced density matrix of the *complement* of `A`.
        n : int | float
            Selects which entropy to calculate;
            `n=1` (default) is the ususal von-Neumann entanglement entropy,
            otherwise the `n`-th Renyi entropy.

        Returns
        -------
        entropy : float
            The entropy for the the region defined by the `segment`
            (or equivalently it's complement).
        """
        segment = np.sort(segment)
        if len(segment) < 8:
            warnings.warn("inefficient: use `entanglement_entropy_segment` instead!", stacklevel=2)
        assert np.all(segment[1:] != segment[:-1])  # duplicates in segment
        N_ol = 0  # number of open legs within the segment
        i0 = segment[0]
        rho = self.get_theta(i0, 1)
        rho = npc.tensordot(rho,
                            rho.conj(),
                            axes=(self._get_p_labels(1), self._get_p_labels(1, True)))
        not_in_segment = 0
        ax_p = self._get_p_label('')
        ax_pstar = self._get_p_label('*')
        for i in range(i0 + 1, segment[-1] + 1):
            is_in_segment = (segment[i - i0 - not_in_segment] == i)
            if is_in_segment:
                B = self.get_B(i, form='B')
                rho = npc.tensordot(rho, B, axes=['vR', 'vL'])
                rho = npc.tensordot(rho, B.conj(), axes=(['vR*'] + ax_p, ['vL*'] + ax_pstar))
            else:
                B = self.get_B(i, form='B', label_p=str(not_in_segment))
                rho = npc.tensordot(rho, B, axes=['vR', 'vL'])
                rho = npc.tensordot(rho, B.conj(), axes=['vR*', 'vL*'])
                not_in_segment += 1
        comb_legs = (['vL', 'vR'] + self._get_p_labels(not_in_segment),
                     ['vL*', 'vR*'] + self._get_p_labels(not_in_segment, star=True))
        rho = rho.combine_legs(comb_legs, qconj=[+1, -1])
        p = npc.eigvalsh(rho)
        return entropy(p, n)

    def entanglement_spectrum(self, by_charge=False):
        r"""return entanglement energy spectrum.

        Parameters
        ----------
        by_charge : bool
            Wheter we should sort the spectrum on each bond by the possible charges.

        Returns
        -------
        ent_spectrum : list
            For each (non-trivial) bond the entanglement spectrum.
            If `by_charge` is ``False``, return (for each bond) a sorted 1D ndarray
            with the convention :math:`S_i^2 = e^{-\xi_i}`, where :math:`S_i` labels a Schmidt
            value and :math:`\xi_i` labels the entanglement 'energy' in the returned spectrum.
            If `by_charge` is True, return a a list of tuples ``(charge, sub_spectrum)``
            for each possible charge on that bond.
        """
        if by_charge:
            res = []
            for i in range(self.L + 1)[self.nontrivial_bonds]:
                ss = -2. * np.log(self._S[i])
                if i < self.L:
                    leg = self._B[i].get_leg('vL')
                else:  # i == L: segment b.c.
                    leg = self._B[i - 1].get_leg('vR').conj()
                spectrum = [(leg.get_charge(qi), np.sort(ss[leg.get_slice(qi)]))
                            for qi in range(leg.block_number)]
                res.append(spectrum)
            return res
        else:
            return [np.sort(-2. * np.log(ss)) for ss in self._S[self.nontrivial_bonds]]

    def get_rho_segment(self, segment):
        """Return reduced density matrix for a segment.

        Note that the dimension of rho_A scales exponentially in the length of the segment.

        Parameters
        ----------
        segment : iterable of int
            Sites for which the reduced density matrix is to be calculated.
            Assumed to be sorted.

        Returns
        -------
        rho : :class:`~tenpy.linalg.np_conserved.Array`
            Reduced density matrix of the segment sites.
            Labels ``'p0', 'p1', ..., 'pk', 'p0*', 'p1*', ..., 'pk*'`` with ``k=len(segment)``.
        """
        if len(segment) > 12:
            warnings.warn("{0:d} sites in the segment, that's much!".format(len(segment)),
                          stacklevel=2)
        if len(segment) > 20:
            raise ValueError("too large segment; this is exponentially expensive!")
        segment = np.sort(segment)
        if np.all(segment[1:] == segment[:-1] + 1):  # consecutive
            theta = self.get_theta(segment[0], segment[-1] - segment[0] + 1)
            rho = npc.tensordot(theta, theta.conj(), axes=(['vL', 'vR'], ['vL*', 'vR*']))
            return rho
        rho = self.get_theta(segment[0], 1)
        rho = npc.tensordot(rho, rho.conj(), axes=('vL', 'vL*'))
        k = 1
        contract_axes = (['vR*'] + self._p_label, ['vL*'] + self._get_p_label('*'))
        for i in range(segment[0] + 1, segment[-1]):
            B = self.get_B(i)
            if i == segment[k]:
                B = self._replace_p_label(B, str(k))
                k += 1
                rho = npc.tensordot(rho, B, axes=('vR', 'vL'))
                rho = npc.tensordot(rho, B.conj(), axes=('vR*', 'vL*'))
            else:
                rho = npc.tensordot(rho, B, axes=('vR', 'vL'))
                rho = npc.tensordot(rho, B.conj(), axes=contract_axes)
        B = self._replace_p_label(self.get_B(segment[-1]), str(k))
        rho = npc.tensordot(rho, B, axes=('vR', 'vL'))
        rho = npc.tensordot(rho, B.conj(), axes=(['vR*', 'vR'], ['vL*', 'vR*']))
        return rho

    def probability_per_charge(self, bond=0):
        """Return probabilites of charge value on the left of a given bond.

        For example for particle number conservation, define
        :math:`N_b = sum_{i<b} n_i` for a given bond `b`.
        This function returns the possible values of `N_b` as rows of `charge_values`,
        and for each row the probabilty that this combination occurs in the given state.

        Parameters
        ----------
        bond : int
            The bond to be considered. The returned charges are summed on the left of this bond.

        Returns
        -------
        charge_values : 2D array
            Columns correspond to the different charges in `self.chinfo`.
            Rows are the different charge fluctuations at this bond
        probabilities : 1D array
            For each row of `charge_values` the probablity for these values of charge fluctuations.
        """
        if self.bc == 'segment' and bond == self.L:
            S = self.get_SR(self.L - 1)**2
            leg = self.get_B(self.L - 1, form=None).get_leg('vR').conj()
        else:  # usually the case
            S = self.get_SL(bond)**2
            leg = self.get_B(bond, form=None).get_leg('vL')
        assert leg.qconj == +1
        if not leg.is_blocked():
            raise ValueError("leg not blocked: can have duplicate entries in charge values")
        ps = []
        for qi in range(leg.block_number):
            sl = leg.get_slice(qi)
            ps.append(np.sum(S[sl]))
        ps = np.array(ps)
        if abs(np.sum(ps) - 1.) > 1.e-10:
            warnings.warn("Probability_per_charge: Sum of probabilites not 1. Canonical form?",
                          stacklevel=2)
        return leg.charges.copy(), ps

    def average_charge(self, bond=0):
        r"""Return the average charge for the block on the left of a given bond.

        For example for particle number conservation, define
        :math:`N_b = sum_{i<b} n_i` for a given bond `b`.
        Then this function returns :math:`<\psi| N_b |\psi>`.

        Parameters
        ----------
        bond : int
            The bond to be considered.
            The returned charges are summed over the sites left of `bond`.

        Returns
        -------
        average_charge : 1D array
            For each type of charge in :attr:`chinfo`
            the average value when summing the charge values over sites left of the given bond.
        """
        charges, ps = self.probability_per_charge(bond)
        return np.sum(ps[:, np.newaxis] * charges, axis=0)

    def charge_variance(self, bond=0):
        r"""Return the charge variance on the left of a given bond.

        For example for particle number conservation, define
        :math:`N_b = sum_{i<b} n_i` for a given bond `b`.
        Then this function returns :math:`<\psi| N_b^2 |\psi> - (<\psi| N_b |\psi>)^2`.

        Parameters
        ----------
        bond : int
            The bond to be considered.
            The returned charges are summed over the sites left of `bond`.

        Returns
        -------
        average_charge : 1D array
            For each type of charge in :attr:`chinfo`
            the variance of of the charge values left of the given bond.
        """
        charges_mean = self.average_charge(bond)
        charges, ps = self.probability_per_charge(bond)
        return np.sum(ps[:, np.newaxis] * (charges - charges_mean[np.newaxis, :])**2, axis=0)

    def mutinf_two_site(self, max_range=None, n=1):
        """Calculate the two-site mutual information :math:`I(i:j)`.

        Calculates :math:`I(i:j) = S(i) + S(j) - S(i,j)`,
        where :math:`S(i)` is the single site entropy on site :math:`i`
        and :math:`S(i,j)` the two-site entropy on sites :math:`i,j`.

        Parameters
        ----------
        max_range : int
            Maximal distance ``|i-j|`` for which the mutual information should be calculated.
            ``None`` defaults to `L-1`.
        n : float
            Selects the entropy to use, see :func:`~tenpy.tools.math.entropy`.

        Returns
        -------
        coords : 2D array
            Coordinates for the mutinf array.
        mutinf : 1D array
            ``mutinf[k]`` is the mutual information :math:`I(i:j)` between the
            sites ``i, j = coords[k]``.
        """
        #  Basically the code of get_rho_segment and entanglement_entropy,
        #  but optimized to run in O(L*max_range)
        if max_range is None:
            max_range = self.L
        S_i = self.entanglement_entropy_segment(n=n)  # single-site entropy
        legs_ij = self._get_p_labels(2, False), self._get_p_labels(2, True)
        # = (['p0', 'p1'], ['p0*', 'p1*'])
        contr_legs = (
            ['vR*'] + self._get_p_label('1'),  # ['vL', 'p1']
            ['vL*'] + self._get_p_label('1*'))  # ['vL*', 'p1*']
        mutinf = []
        coord = []
        for i in range(self.L):
            rho = self.get_theta(i, 1)
            rho = npc.tensordot(rho, rho.conj(), axes=('vL', 'vL*'))
            jmax = i + max_range + 1
            if self.finite:
                jmax = min(jmax, self.L)
            for j in range(i + 1, jmax):
                B = self._replace_p_label(self.get_B(j, form='B'), '1')  # 'vL', 'vR', 'p1'
                rho = npc.tensordot(rho, B, axes=['vR', 'vL'])
                rho_ij = npc.tensordot(rho, B.conj(), axes=(['vR*', 'vR'], ['vL*', 'vR*']))
                rho_ij = rho_ij.combine_legs(legs_ij, qconj=[+1, -1])
                S_ij = entropy(npc.eigvalsh(rho_ij), n)
                mutinf.append(S_i[i] + S_i[j % self.L] - S_ij)
                coord.append((i, j))
                if j + 1 < jmax:
                    rho = npc.tensordot(rho, B.conj(), axes=contr_legs)
        return np.array(coord), np.array(mutinf)

    def overlap(self, other, charge_sector=None, ignore_form=False, **kwargs):
        """Compute overlap ``<self|other>``.

        Parameters
        ----------
        other : :class:`MPS`
            An MPS with the same physical sites.
        charge_sector : None | charges | ``0``
            Selects the charge sector in which the dominant eigenvector of the TransferMatrix is.
            ``None`` stands for *all* sectors, ``0`` stands for the sector of zero charges.
            If a sector is given, it *assumes* the dominant eigenvector is in that charge sector.
        ignore_form : bool
            If ``False`` (default), take into account the canonical form :attr:`form` at each site.
            If ``True``, we ignore the canonical form (i.e., whether the MPS is in left, right,
            mixed or no canonical form) and just contract all the :attr:`_B` as they are.
            (This can give different results!)
        **kwargs :
            Further keyword arguments given to :meth:`TransferMatrix.eigenvectors`;
            only used for infinite boundary conditions.

        Returns
        -------
        overlap : dtype.type
            The contraction ``<self|other> * self.norm * other.norm``
            (i.e., taking into account the :attr:`norm` of both MPS).
            For an infinite MPS, ``<self|other>`` is the overlap per unit cell, i.e.,
            the largest eigenvalue of the TransferMatrix.
        """
        if self.finite:
            if ignore_form:
                # Use TransferMatrix with option to ignore the form
                TM = TransferMatrix(self, other, charge_sector=charge_sector, form=None)
                res = TM.matvec(TM.initial_guess(1.))  # apply transfer matrix to identity
                return npc.trace(res, 0, 1) * self.norm * other.norm
            else:
                env = MPSEnvironment(self, other)
                return env.full_contraction(0)
        else:  # infinite
            form = None if ignore_form else 'B'
            TM = TransferMatrix(self, other, charge_sector=charge_sector, form=form)
            ov, _ = TM.eigenvectors(**kwargs)
            return ov[0] * self.norm * other.norm

    def expectation_value(self, ops, sites=None, axes=None):
        """Expectation value ``<psi|ops|psi>/<psi|psi>`` of (n-site) operator(s).

        Given the MPS in canonical form, it calculates n-site expectation values.
        For example the contraction for a two-site (`n` = 2) operator on site `i` would look like::

            |          .--S--B[i]--B[i+1]--.
            |          |     |     |       |
            |          |     |-----|       |
            |          |     | op  |       |
            |          |     |-----|       |
            |          |     |     |       |
            |          .--S--B*[i]-B*[i+1]-.

        Parameters
        ----------
        ops : (list of) { :class:`~tenpy.linalg.np_conserved.Array` | str }
            The operators, for wich the expectation value should be taken,
            All operators should all have the same number of legs (namely `2 n`).
            If less than `self.L` operators are given, we repeat them periodically.
            Strings (like ``'Id', 'Sz'``) are translated into single-site operators defined by
            :attr:`sites`.
        sites : None | list of int
            List of site indices. Expectation values are evaluated there.
            If ``None`` (default), the entire chain is taken (clipping for finite b.c.)
        axes : None | (list of str, list of str)
            Two lists of each `n` leg labels giving the physical legs of the operator used for
            contraction. The first `n` legs are contracted with conjugated `B`,
            the second `n` legs with the non-conjugated `B`.
            ``None`` defaults to ``(['p'], ['p*'])`` for single site operators (`n` = 1), or
            ``(['p0', 'p1', ... 'p{n-1}'], ['p0*', 'p1*', .... 'p{n-1}*'])`` for `n` > 1.

        Returns
        -------
        exp_vals : 1D ndarray
            Expectation values, ``exp_vals[i] = <psi|ops[i]|psi>``, where ``ops[i]`` acts on
            site(s) ``j, j+1, ..., j+{n-1}`` with ``j=sites[i]``.

        Examples
        --------
        Let's prepare a state in alternating ``|+z>, |+x>`` states:

        .. doctest :: MPS.expectation_value

            >>> spin_half = tenpy.networks.site.SpinHalfSite(conserve=None)
            >>> p_state = ['up', [np.sqrt(0.5), -np.sqrt(0.5)]]*3
            >>> psi = tenpy.networks.mps.MPS.from_product_state([spin_half]*6, p_state)

        One site examples (n=1):

        .. doctest :: MPS.expectation_value

            >>> Sz = psi.expectation_value('Sz')
            >>> print(Sz)
            [0.5 0.  0.5 0.  0.5 0. ]
            >>> Sx = psi.expectation_value('Sx')
            >>> print(Sx)
            [ 0.  -0.5  0.  -0.5  0.  -0.5]
            >>> print(psi.expectation_value(['Sz', 'Sx']))
            [ 0.5 -0.5  0.5 -0.5  0.5 -0.5]
            >>> print(psi.expectation_value('Sz', sites=[0, 3, 4]))
            [0.5 0.  0.5]

        Two site example (n=2), assuming homogeneous sites:

        .. doctest :: MPS.expectation_value

            >>> SzSx = npc.outer(psi.sites[0].Sz.replace_labels(['p', 'p*'], ['p0', 'p0*']),
            ...                  psi.sites[1].Sx.replace_labels(['p', 'p*'], ['p1', 'p1*']))
            >>> print(psi.expectation_value(SzSx))  # note: len L-1 for finite bc, or L for infinite
            [-0.25  0.   -0.25  0.   -0.25]

        Example measuring <psi|SzSx|psi2> on each second site, for inhomogeneous sites:

        .. doctest :: MPS.expectation_value

            >>> SzSx_list = [npc.outer(psi.sites[i].Sz.replace_labels(['p', 'p*'], ['p0', 'p0*']),
            ...                        psi.sites[i+1].Sx.replace_labels(['p', 'p*'], ['p1', 'p1*']))
            ...              for i in range(0, psi.L-1, 2)]
            >>> print(psi.expectation_value(SzSx_list, range(0, psi.L-1, 2)))
            [-0.25 -0.25 -0.25]
        """
        ops, sites, n, (op_ax_p, op_ax_pstar) = self._expectation_value_args(ops, sites, axes)
        ax_p = ['p' + str(k) for k in range(n)]
        ax_pstar = ['p' + str(k) + '*' for k in range(n)]
        E = []
        for i in sites:
            op = self.get_op(ops, i)
            op = op.replace_labels(op_ax_p + op_ax_pstar, ax_p + ax_pstar)
            theta = self.get_theta(i, n)
            C = npc.tensordot(op, theta, axes=[ax_pstar, ax_p])  # C has same labels as theta
            E.append(npc.inner(theta, C, axes='labels', do_conj=True))
        return np.real_if_close(np.array(E))

    def expectation_value_term(self, term, autoJW=True):
        r"""Expectation value  ``<psi|op_{i0}op_{i1}...op_{iN}|psi>/<psi|psi>``.

        Calculates the expectation value of a tensor product of single-site operators
        acting on different sites `i0`, `i1`, ... (not necessarily next to each other).
        In other words, evaluate the expectation value of a term ``op0_i0 op1_i1 op2_i2 ...``.

        For example the contraction of three one-site operators on sites `i0`,
        `i1=i0+1`, `i2=i0+3` would look like::

            |          .--S--B[i0]---B[i0+1]--B[i0+2]--B[i0+3]--.
            |          |     |       |        |        |        |
            |          |    op1     op2       |       op3       |
            |          |     |       |        |        |        |
            |          .--S--B*[i0]--B*[i0+1]-B*[i0+2]-B*[i0+3]-.

        Parameters
        ----------
        term : list of (str, int)
            List of tuples ``op, i`` where `i` is the MPS index of the site the operator
            named `op` acts on.
            The order inside `term` determines the order in which they act
            (in the mathematical convention: the last operator in `term` is right-most,
            so it acts first on a ket).
        autoJW : bool
            If True (default), automatically insert Jordan Wigner strings for Fermions as needed.

        Returns
        -------
        exp_val : float/complex
            The expectation value of the tensorproduct of the given onsite operators,
            ``<psi|op_i0 op_i1 ... op_iN |psi>/<psi|psi>``,
            where ``|psi>`` is the represented MPS.

        See also
        --------
        correlation_function : efficient way to evaluate many correlation functions.

        Examples
        --------

        .. testsetup :: MPS.expectation_value_term

            spin_half = tenpy.networks.site.SpinHalfSite(conserve=None)
            psi = tenpy.networks.mps.MPS.from_product_state([spin_half]*8, ['up']*8)

        .. doctest :: MPS.expectation_value_term

            >>> a = psi.expectation_value_term([('Sx', 2), ('Sz', 4)])
            >>> b = psi.expectation_value_term([('Sz', 4), ('Sx', 2)])
            >>> c = psi.expectation_value_multi_sites(['Sz', 'Id', 'Sx'], i0=2)
            >>> assert a == b == c
        """
        # strategy: translate term into a list "ops" to be used for `expectation_value_multi_sites`
        ops, i_min, has_extra_JW = self._term_to_ops_list(term, autoJW)
        if has_extra_JW:
            raise ValueError("Odd number of operators which need a Jordan Wigner string")
        return self.expectation_value_multi_sites(ops, i_min)

    def _term_to_ops_list(self, term, autoJW=True, i_offset=0, JW_from_right=False):
        """Translate a `term` to a list of operators (one per site).

        Parameters
        ----------
        term : list of (str, int)
            List of tuples ``op, i`` where `i` is the MPS index of the site the operator
            named `op` acts on.
            The order inside `term` determines the order in which they act
            (in the mathematical convention: the last operator in `term` is right-most,
            so it acts first on a ket).
            If autoJW is False, we also accept npc arrays for `op`.
        autoJW : bool
            If True (default), automatically insert Jordan Wigner strings for Fermions as needed.
        i_offset : int
            Offset to be added to the site-indices in the `term`.
        JW_from_right : bool | None
            If set to True, a JW-string is coming in from the right. Corresponding `JW` operators
            are added to `ops`.
            If `None`, use

        Returns
        -------
        ops : list of :class:`~tenpy.linalg.np_conserved.Array`
            Operators, one per site starting with `i_min`, i.e. ``ops[i]`` acts on `i_min`+`i`.
        i_min : int
            Index of the left-most site on which `ops` act (including the `i_offset`).
        has_extra_JW : bool
            True if there is an odd number of terms which require a Jordan-Wigner string,
            i.e., if there is a JW string coming out to the left.
            If `JW_from_right` was initially `None`, it is the value chosen for `JW_from_right`.
        """
        assert not (JW_from_right and not autoJW)
        term = list(term)
        i_min = min([t[1] for t in term])
        i_max = max([t[1] for t in term])
        ops = [[] for i in range((i_max - i_min + 1))]
        count_JW = 0
        for op, i in term:
            j = i - i_min  # index in ops
            ops[j].append(op)
            if autoJW and self.sites[self._to_valid_index(i + i_offset)].op_needs_JW(op):
                count_JW += 1
                for k in range(j):
                    ops[k].append('JW')
        if JW_from_right is None:
            JW_from_right = (count_JW % 2 == 1)
            if JW_from_right:
                count_JW -= 1  # still return True for `has_extra_JW`
        if JW_from_right:
            count_JW += 1
            for op_i in ops:
                op_i.append('JW')
        for j in range(len(ops)):
            site = self.sites[self._to_valid_index(j + i_min + i_offset)]
            ops[j] = site.multiply_operators(ops[j])
        return ops, i_min + i_offset, (count_JW % 2 == 1)

    def expectation_value_multi_sites(self, operators, i0):
        r"""Expectation value  ``<psi|op0_{i0}op1_{i0+1}...opN_{i0+N}|psi>/<psi|psi>``.

        Calculates the expectation value of a tensor product of single-site operators
        acting on different sites next to each other.
        In other words, evaluate the expectation value of a term
        ``op0_i0 op1_{i0+1} op2_{i0+2} ...``, looking like this (with `op` short for `operators`,
        for ``len(operators)=3``):

            |          .--S--B[i0]---B[i0+1]--B[i0+2]--B[i0+3]--.
            |          |     |       |        |        |        |
            |          |     op[0]   op[1]    op[2]    op[3]    |
            |          |     |       |        |        |        |
            |          .--S--B*[i0]--B*[i0+1]-B*[i0+2]-B*[i0+3]-.


        .. warning ::
            This function does *not* automatically add Jordan-Wigner strings!
            For correct handling of fermions, use :meth:`expectation_value_term` instead.

        Parameters
        ----------
        operators : List of { :class:`~tenpy.linalg.np_conserved.Array` | str }
            List of one-site operators. This method calculates the
            expectation value of the n-sites operator given by their tensor
            product.
        i0 : int
            The left most index on which an operator acts, i.e.,
            ``operators[i]`` acts on site ``i + i0``.

        Returns
        -------
        exp_val : float/complex
            The expectation value of the tensorproduct of the given onsite operators,
            ``<psi|operators[0]_{i0} operators[1]_{i0+1} ... |psi>/<psi|psi>``,
            where ``|psi>`` is the represented MPS.
        """
        C = self._corr_ops_LP(operators, i0)
        exp_val = npc.trace(C, 'vR*', 'vR')
        return np.real_if_close(exp_val)

    def _corr_ops_LP(self, operators, i0):
        """Contract the left part of a correlation function.

        Same as :meth:`expectation_value_multi_sites`, but with the right-most legs left open,
        with labels ``'vR*', 'vR'``.
        """
        op = operators[0]
        if (isinstance(op, str)):
            op = self.sites[self._to_valid_index(i0)].get_op(op)
        theta = self.get_B(i0, 'Th')
        C = npc.tensordot(op, theta, axes=['p*', 'p'])
        axes = [['vL*'] + self._get_p_label('*'), ['vL'] + self._p_label]
        C = npc.tensordot(theta.conj(), C, axes=axes)
        axes[1][0] = 'vR*'
        for j in range(1, len(operators)):
            op = operators[j]  # the operator
            is_str = isinstance(op, str)
            i = i0 + j  # the site it acts on
            B = self.get_B(i, form='B')
            C = npc.tensordot(C, B, axes=['vR', 'vL'])
            if not (is_str and op == 'Id'):
                if is_str:
                    op = self.sites[self._to_valid_index(i)].get_op(op)
                C = npc.tensordot(op, C, axes=['p*', 'p'])
            C = npc.tensordot(B.conj(), C, axes=axes)
        return C

    def _corr_ops_RP(self, operators, i0):
        """Contract the right part of a correlation function.

        Same as :meth:`expectation_value_multi_sites`, but with the left-most part open
        and **excluding** the singular values `S`, with legs ``'vL', 'vL*'``.
        """
        op = operators[-1]
        imax = i0 + len(operators) - 1
        C = npc.eye_like(self.get_B(imax, 'B'), 'vR', ['vL*', 'vL'])
        axes = [self._p_label + ['vL*'], self._get_p_label('*') + ['vR*']]
        for j in reversed(range(len(operators))):
            op = operators[j]  # the operator
            is_str = isinstance(op, str)
            i = i0 + j  # the site it acts on
            B = self.get_B(i, form='B')
            C = npc.tensordot(B, C, axes=['vR', 'vL'])
            if not (is_str and op == 'Id'):
                if is_str:
                    op = self.sites[self._to_valid_index(i)].get_op(op)
                C = npc.tensordot(op, C, axes=['p*', 'p'])
            C = npc.tensordot(C, B.conj(), axes=axes)
        return C

    def expectation_value_terms_sum(self, term_list, prefactors=None):
        """Calculate expectation values for a bunch of terms and sum them up.

        This is equivalent to the following expression::

            sum([self.expectation_value_term(term)*strength for term, strength in term_list])

        However, for effiency, the term_list is converted to an MPO and the expectation value
        of the MPO is evaluated.

        .. note ::
            Due to the way MPO expectation values are evaluated for infinite systems,
            it works only if all terms in the `term_list` start within the MPS unit cell.

        .. deprecated:: 0.4.0
            `prefactor` will be removed in version 1.0.0.
            Instead, directly give just ``TermList(term_list, prefactors)`` as argument.

        Parameters
        ----------
        term_list : :class:`~tenpy.networks.terms.TermList`
            The terms and prefactors (`strength`) to be summed up.
        prefactors :
            Instead of specifying a :class:`~tenpy.networks.terms.TermList`,
            one can also specify the term_list and strength separately.
            This is deprecated.

        Returns
        -------
        terms_sum : list of (complex) float
            Equivalent to the expression
            ``sum([self.expectation_value_term(term)*strength for term, strength in term_list])``.
        _mpo :
            Intermediate results: the generated MPO.
            For a finite MPS, ``terms_sum = _mpo.expectation_value(self)``, for an infinite MPS
            ``terms_sum = _mpo.expectation_value(self) * self.L``

        See also
        --------
        expectation_value_term : evaluates a single `term`.
        tenpy.networks.mpo.MPO.expectation_value : expectation value density of an MPO.
        """
        from . import mpo, terms
        if prefactors is not None:
            warnings.warn(
                "Deprecated argument prefactors: replace arguments with "
                "``TermList(term_list, prefactors)``.", FutureWarning, 2)
            term_list = terms.TermList(term_list, prefactors)
        L = self.L
        if not self.finite:
            for term in term_list.terms:
                if not 0 <= min([i for _, i in term]) < L:
                    raise ValueError("term doesn't start in MPS unit cell: " + repr(term))
        # conversion
        ot, ct = term_list.to_OnsiteTerms_CouplingTerms(self.sites)
        bc = 'finite' if self.finite else 'infinite'
        mpo_graph = mpo.MPOGraph.from_terms((ot, ct), self.sites, bc)
        mpo_ = mpo_graph.build_MPO()
        terms_sum = mpo_.expectation_value(self, max_range=ct.max_range())
        if not self.finite:
            terms_sum = terms_sum * self.L
        return terms_sum, mpo_

    def correlation_function(self,
                             ops1,
                             ops2,
                             sites1=None,
                             sites2=None,
                             opstr=None,
                             str_on_first=True,
                             hermitian=False,
                             autoJW=True):
        r"""Correlation function  ``<psi|op1_i op2_j|psi>/<psi|psi>`` of single site operators.

        Given the MPS in canonical form, it calculates 2-site correlation functions.
        For examples the contraction for a two-site operator on site `i` would look like::

            |          .--S--B[i]--B[i+1]--...--B[j]---.
            |          |     |     |            |      |
            |          |     |     |            op2    |
            |          |     op1   |            |      |
            |          |     |     |            |      |
            |          .--S--B*[i]-B*[i+1]-...--B*[j]--.

        Onsite terms are taken in the order ``<psi | op1 op2 | psi>``.

        If `opstr` is given and ``str_on_first=True``, it calculates::

            |           for i < j                               for i > j
            |
            |          .--S--B[i]---B[i+1]--...- B[j]---.     .--S--B[j]---B[j+1]--...- B[i]---.
            |          |     |      |            |      |     |     |      |            |      |
            |          |     opstr  opstr        op2    |     |     op2    |            |      |
            |          |     |      |            |      |     |     |      |            |      |
            |          |     op1    |            |      |     |     opstr  opstr        op1    |
            |          |     |      |            |      |     |     |      |            |      |
            |          .--S--B*[i]--B*[i+1]-...- B*[j]--.     .--S--B*[j]--B*[j+1]-...- B*[i]--.

        For ``i==j``, no `opstr` is included.
        For ``str_on_first=False``, the `opstr` on site ``min(i, j)`` is always left out.

        Strings (like ``'Id', 'Sz'``) in the arguments are translated into single-site
        operators defined by the :class:`~tenpy.networks.site.Site` on which they act.
        Each operator should have the two legs ``'p', 'p*'``.


        .. warning ::
            This function is only evaluating correlation functions by moving right, and hence
            can be inefficient if you try to vary the left end while fixing the right end.
            In that case, you might be better off (=faster evaluation) by using
            :meth:`term_correlation_function_left` with a small for loop over the right indices.

        Parameters
        ----------
        ops1 : (list of) { :class:`~tenpy.linalg.np_conserved.Array` | str }
            First operator of the correlation function (acting after ops2).
            If a list is given, ``ops1[i]`` acts on site `i` of the MPS.
        ops2 : (list of) { :class:`~tenpy.linalg.np_conserved.Array` | str }
            Second operator of the correlation function (acting before ops1).
            If a list is given, ``ops2[j]`` acts on site `j` of the MPS.
        sites1 : None | int | list of int
            List of site indices `i`; a single `int` is translated to ``range(0, sites1)``.
            ``None`` defaults to all sites ``range(0, L)``.
            Is sorted before use, i.e. the order is ignored.
        sites2 : None | int | list of int
            List of site indices; a single `int` is translated to ``range(0, sites2)``.
            ``None`` defaults to all sites ``range(0, L)``.
            Is sorted before use, i.e. the order is ignored.
        opstr : None | (list of) { :class:`~tenpy.linalg.np_conserved.Array` | str }
            Ignored by default (``None``).
            Operator(s) to be inserted between ``ops1`` and ``ops2``.
            If less than :attr:`L` operators are given, we repeat them periodically.
            If given as a list, ``opstr[r]`` is inserted at site `r` (independent of `sites1` and
            `sites2`).
        str_on_first : bool
            Whether the `opstr` is included on the site ``min(i, j)``.
            Note the order, which is chosen that way to handle fermionic Jordan-Wigner strings
            correctly. (In other words: choose ``str_on_first=True`` for fermions!)
        hermitian : bool
            Optimization flag: if ``sites1 == sites2`` and ``Ops1[i]^\dagger == Ops2[i]``
            (which is not checked explicitly!), the resulting ``C[x, y]`` will be hermitian.
            We can use that to avoid calculations, so ``hermitian=True`` will run faster.
        autoJW : bool
            *Ignored* if `opstr` is given.
            If `True`, auto-determine if a Jordan-Wigner string is needed.
            Works only if exclusively strings were used for `op1` and `op2`.

        Returns
        -------
        C : 2D ndarray
            The correlation function ``C[x, y] = <psi|ops1[i] ops2[j]|psi>``,
            where ``ops1[i]`` acts on site ``i=sites1[x]`` and ``ops2[j]`` on site ``j=sites2[y]``.
            If `opstr` is given, it gives (for ``str_on_first=True``):

            - For ``i < j``: ``C[x, y] = <psi|ops1[i] prod_{i <= r < j} opstr[r] ops2[j]|psi>``.
            - For ``i > j``: ``C[x, y] = <psi|prod_{j <= r < i} opstr[r] ops1[i] ops2[j]|psi>``.
            - For ``i = j``: ``C[x, y] = <psi|ops1[i] ops2[j]|psi>``.

            The condition ``<= r`` is replaced by a strict ``< r``, if ``str_on_first=False``.

        Examples
        --------
        Let's prepare a state in alternating ``|+z>, |+x>`` states:

        .. doctest :: MPS.correlation_function

            >>> spin_half = tenpy.networks.site.SpinHalfSite(conserve=None)
            >>> p_state = ['up', [np.sqrt(0.5), -np.sqrt(0.5)]]*3
            >>> psi = tenpy.networks.mps.MPS.from_product_state([spin_half]*6, p_state, "infinite")

        Default arguments calculate correlations for all `i` and `j` within the MPS unit cell.
        To evaluate the correlation function for a single `i`, you can use ``sites1=[i]``.
        Alternatively, you can use :meth:`term_correlation_function_right`
        (or :meth:`term_correlation_function_left`):

        .. doctest :: MPS.correlation_function

            >>> psi.correlation_function("Sz", "Sx")  # doctest: +SKIP
            array([[ 0.  , -0.25,  0.  , -0.25,  0.  , -0.25],
                   [ 0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ],
                   [ 0.  , -0.25,  0.  , -0.25,  0.  , -0.25],
                   [ 0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ],
                   [ 0.  , -0.25,  0.  , -0.25,  0.  , -0.25],
                   [ 0.  ,  0.  ,  0.  ,  0.  ,  0.  ,  0.  ]])
            >>> psi.correlation_function("Sz", "Sx", [0])
            array([[ 0.  , -0.25,  0.  , -0.25,  0.  , -0.25]])
            >>> corr1 = psi.correlation_function("Sz", "Sx", [0], range(1, 10))
            >>> corr2 = psi.term_correlation_function_right([("Sz", 0)], [("Sx", 0)], 0, range(1, 10))
            >>> assert np.all(np.abs(corr2 - corr1) < 1.e-12)

        For fermions, it auto-determines that/whether a Jordan Wigner string is needed:

        .. doctest :: MPS.correlation_function

            >>> fermion = tenpy.networks.site.FermionSite(conserve='N')
            >>> p_state = ['empty', 'full'] * 3
            >>> psi = tenpy.networks.mps.MPS.from_product_state([fermion]*6, p_state, "finite")
            >>> CdC = psi.correlation_function("Cd", "C")  # optionally: use `hermitian=True`
            >>> psi.correlation_function("C", "Cd")[1, 2] == -CdC[2, 1]
            True
            >>> np.all(np.diag(CdC) == psi.expectation_value("Cd C"))  # "Cd C" is equivalent to "N"
            True

        See also
        --------
        expectation_value_term : for a single combination of `i` and `j` of ``A_i B_j```.
        term_correlation_function_right : for correlations between multi-site terms, fix left term.
        term_correlation_function_left : for correlations between multi-site terms, fix right term.
        """
        if opstr is not None:
            autoJW = False
        ops1, ops2, sites1, sites2, opstr = self._correlation_function_args(
            ops1, ops2, sites1, sites2, opstr)
        if ((len(sites1) > 2 * len(sites2) and min(sites2) > max(sites1) - len(sites2))
                or (len(sites2) > 2 * len(sites1) and min(sites1) > max(sites2) - len(sites1))):
            warnings.warn(
                "Inefficent evaluation of MPS.correlation_function(), "
                "it's probably faster to use MPS.term_correlation_function_left()",
                stracklevel=2)
        if autoJW and not all([isinstance(op1, str) for op1 in ops1]):
            warnings.warn("Non-string operator: can't auto-determine Jordan-Wigner!", stacklevel=2)
            autoJW = False
        if autoJW:
            need_JW = []
            for i in sites1:
                need_JW.append(self.sites[i % self.L].op_needs_JW(ops1[i % len(ops1)]))
            for j in sites2:
                need_JW.append(self.sites[j % self.L].op_needs_JW(ops1[j % len(ops1)]))
            if any(need_JW):
                if not all(need_JW):
                    raise ValueError("Some, but not any operators need 'JW' string!")
                if not str_on_first:
                    raise ValueError("Need Jordan Wigner string, but `str_on_first`=False`")
                opstr = ['JW']
        if hermitian and np.any(sites1 != sites2):
            warnings.warn("MPS correlation function can't use the hermitian flag", stacklevel=2)
            hermitian = False
        C = np.empty((len(sites1), len(sites2)), dtype=complex)
        for x, i in enumerate(sites1):
            # j > i
            j_gtr = sites2[sites2 > i]
            if len(j_gtr) > 0:
                C_gtr = self._corr_up_diag(ops1, ops2, i, j_gtr, opstr, str_on_first, True)
                C[x, (sites2 > i)] = C_gtr
                if hermitian:
                    C[x + 1:, x] = np.conj(C_gtr)
            # j == i
            j_eq = sites2[sites2 == i]
            if len(j_eq) > 0:
                # on-site correlation function
                op12 = npc.tensordot(self.get_op(ops1, i), self.get_op(ops2, i), axes=['p*', 'p'])
                C[x, (sites2 == i)] = self.expectation_value(op12, i, [['p'], ['p*']])
        if not hermitian:
            #  j < i
            for y, j in enumerate(sites2):
                i_gtr = sites1[sites1 > j]
                if len(i_gtr) > 0:
                    C[(sites1 > j), y] = self._corr_up_diag(ops2, ops1, j, i_gtr, opstr,
                                                            str_on_first, False)
                    # exchange ops1 and ops2 : they commute on different sites,
                    # but we apply opstr after op1 (using the last argument = False)
        return np.real_if_close(C)

    def term_correlation_function_right(self,
                                        term_L,
                                        term_R,
                                        i_L=0,
                                        j_R=None,
                                        autoJW=True,
                                        opstr=None):
        """Correlation function between (multi-site) terms, moving the right term, fix left term.

        For ``term_L = [('A', 0), ('B', 1)]`` and ``term_R = [('C', 0), ('D', 1)]``,
        calculate the correlation function :math:`A_{i+0} B_{i+1} C_{j+0} D_{j+1}`
        for fixed `i` and varying `j` according to `i_L`/`j_R`.
        The terms may not overlap.
        For fermions, the order of the terms is following the usual mathematical convention,
        where term_R acts first on a physical ket.

        Parameters
        ----------
        term_L, term_R : list of (str, int)
            Each a term representing a sum of operators on different sites, e.g.,
            ``[('Sz', 0), ('Sz', 1)]`` or ``[('Cd', 0), ('C', 1)]``.
        i_L : int
            Offset added to the indices of `term_L`.
        j_R : list of int | None
            List of offsets to be added to the indices of `term_R`.
            Is sorted before use, i.e. the order is ignored.
            For **finite** MPS, `None` defaults to ``range(j0, L)``,
            where `j0` is chosen such that `term_R` starts one site right of the `term_L`.
            For **infinite** MPS, `None` defaults to ``range(L, 11*L, L)``, i.e.,
            one term per MPS unit cell for a distance of up to 10 unit cells.
        autoJW : bool
            Whether to automatically take care of Jordan-Wigner strings.
        opstr : str
            Force an intermediate operator string to used inbetween the terms.
            Can only be used in combination with ``autoJW=False``.

        Returns
        -------
        corrs : 1D array
            Values of the correlation function, one for each entry in the list `j_R`.

        See also
        --------
        correlation_function : varying both `i` and `j` at once.
        term_list_correlation_function_right : generalization to sums of terms on the left/right.
        """
        assert opstr is None or not autoJW
        if j_R is None:
            if self.finite:
                j0 = i_L + max([t[1] for t in term_L]) + 1 - min([t[1] for t in term_R])
                j_R = range(j0, self.L - max([t[1] for t in term_R] + [0]))
            else:
                j_R = range(self.L, 11 * self.L, self.L)
        else:
            j_R = np.sort(j_R)
        ops_R, j_min, has_extra_JW = self._term_to_ops_list(term_R, autoJW, j_R[0])
        j_min = j_min - j_R[0]
        if autoJW:
            opstr = 'JW' if has_extra_JW else None
        ops_L, i_min, has_extra_JW = self._term_to_ops_list(term_L, autoJW, i_L, has_extra_JW)
        if autoJW and has_extra_JW:
            raise ValueError("Odd total number of operators which need a Jordan Wigner string")
        CL = self._corr_ops_LP(ops_L, i_min)
        i = i_min + len(ops_L)  # CL is contraction strictly left of site `i`
        if i > j_R[0] + j_min:
            raise ValueError("i_L/j_R not such that term_L is left of term_R")
        axes = [['vL*'] + self._get_p_label('*'), ['vR*'] + self._p_label]
        result = []
        for j in j_R:
            j = j + j_min  # start ops_R on site `j`
            assert i <= j
            for k in range(i, j):
                assert i == k
                # contract CL with tensors on site `k`
                B = self.get_B(k, form='B')
                CL = npc.tensordot(CL, B, axes=['vR', 'vL'])
                if opstr is not None:
                    opstr_k = self.sites[self._to_valid_index(k)].get_op(opstr)
                    CL = npc.tensordot(opstr_k, CL, axes=['p*', 'p'])
                CL = npc.tensordot(B.conj(), CL, axes=axes)
                i = k + 1
            CR = self._corr_ops_RP(ops_R, j)
            result.append(npc.inner(CL, CR, axes=[['vR', 'vR*'], ['vL', 'vL*']]))
        return np.real_if_close(result)

    def term_correlation_function_left(self,
                                       term_L,
                                       term_R,
                                       i_L=None,
                                       j_R=0,
                                       autoJW=True,
                                       opstr=None):
        """Correlation function between (multi-site) terms, moving the left term, fix right term.

        Same as :meth:`term_correlation_function_right`, but vary index `i` of the left term
        instead of the `j` of the right term.
        """
        assert opstr is None or not autoJW
        if i_L is None:
            if self.finite:
                raise ValueError("No default set for finite MPS")
            else:
                i_L = range(-self.L, -11 * self.L, -self.L)
        else:
            i_L = np.sort(i_L)[::-1]
        ops_R, j_min, has_extra_JW = self._term_to_ops_list(term_R, autoJW, j_R)
        if autoJW:
            opstr = 'JW' if has_extra_JW else None
        ops_L, i_min, has_extra_JW = self._term_to_ops_list(term_L, autoJW, i_L[0], has_extra_JW)
        i_min = i_min - i_L[0]
        if autoJW and has_extra_JW:
            raise ValueError("Odd total number of operators which need a Jordan Wigner string")
        CR = self._corr_ops_RP(ops_R, j_min)
        j = j_min  # CR is contraction including site `j`
        if i_L[0] + i_min + len(ops_L) - 1 > j:
            raise ValueError("i_L/j_R not such that term_L is left of term_R")
        axes = [self._p_label + ['vL*'], self._get_p_label('*') + ['vR*']]
        result = []
        for i in i_L:
            i0 = i + i_min + len(ops_L) - 1  # CL of term_L includes site `i0` as right-most
            assert i0 <= j
            for k in range(j - 1, i0, -1):
                # contract CR with tensors on site `k`
                B = self.get_B(k, form='B')
                CR = npc.tensordot(B, CR, axes=['vR', 'vL'])
                if opstr is not None:
                    opstr_k = self.sites[self._to_valid_index(k)].get_op(opstr)
                    CR = npc.tensordot(opstr_k, CR, axes=['p*', 'p'])
                CR = npc.tensordot(CR, B.conj(), axes=axes)
                j = k
            CL = self._corr_ops_LP(ops_L, i + i_min)
            result.append(npc.inner(CL, CR, axes=[['vR', 'vR*'], ['vL', 'vL*']]))
        return np.real_if_close(result)

    def term_list_correlation_function_right(self,
                                             term_list_L,
                                             term_list_R,
                                             i_L=0,
                                             j_R=None,
                                             autoJW=True,
                                             opstr=None):
        """Correlation function between sums of multi-site terms, moving the right sum of term.

        Generalization of :meth:`term_correlation_function_right` to the case where
        `term_list_L` and `term_R` are sums of terms.
        This function calculates ``<psi|term_list_L[i_L] term_list_R[j]|psi> for j in j_R``.

        **Assumes** that overall terms with an odd number of operators requiring a Jordan-Wigner
        string don't contribute.
        (In systems conserving the fermionic particle number (parity), this is true.)

        Parameters
        ----------
        term_list_L, term_list_R : :class:`~tenpy.networks.terms.TermList`
            Each a `TermList` representing the sum of terms to be applied.
        i_L : int
            Offset added to all the indices of `term_list_L`.
        j_R : list of int | None
            List of offsets to be added to the indices of `term_list_R`.
            Is sorted before use, i.e. the order is ignored.
            For **finite** MPS, `None` defaults to ``range(j0, L)``,
            where `j0` is chosen such that `term_R` starts one site right of the `term_L`.
            For **infinite** MPS, `None` defaults to ``range(L, 11*L, L)``, i.e.,
            one term per MPS unit cell for a distance of up to 10 unit cells.
        autoJW : bool
            Whether to automatically take care of Jordan-Wigner strings.
        opstr : str
            Force an intermediate operator string to be used inbetween the terms.
            (Even used within the `term_list_L/R` for terms with smaller-than maximal support.)
            Can only be used in combination with ``autoJW=False``.

        Returns
        -------
        corrs : 1D array
            Values of the correlation function, one for each entry in the list `j_R`.

        See also
        --------
        term_correlation_function_right : version for a single term in both `term_list_L/R`.
        """
        assert opstr is None or not autoJW
        min_L, max_L = term_list_L.limits()
        min_R, max_R = term_list_R.limits()  # note: min_R can be negative!
        if j_R is None:
            if self.finite:
                j0 = i_L + max_L + 1 - min_L
                j_R = range(j0, self.L - max(0, max_R))
            else:
                j_R = range(self.L, 11 * self.L, self.L)
        else:
            j_R = np.sort(j_R)
        j0 = j_R[0]
        if i_L + max_L >= j0 + min_R:
            raise ValueError("i_L/i_R not such that term_list_L is left of term_list_R")
        if autoJW:
            opstr_fill = {True: 'JW', False: 'Id'}  # key: whether JW is needed
        else:
            opstr_fill = {False: 'Id' if opstr is None else opstr}
            # True key not needed: we don't check for JW!
        all_ops_R = []
        need_JW_R = []
        for term_R in term_list_R.terms:
            ops_R, j_min, need_JW = self._term_to_ops_list(term_R, autoJW, j0)
            need_JW_R.append(need_JW)
            if j_min > j0 + min_R:
                # fill ops_R such that the left-most op acts at site `j0 + min_R`
                ops_R = [opstr_fill[need_JW]] * (j_min - (j0 + min_R)) + ops_R
            all_ops_R.append(ops_R)
        i = i_L + max_L + 1  # CL is contraction strictly left of site i
        CLs = {}  # (need_JW, qtotal...) -> sum_CL
        # where sum_CL = sum(CL(term_L) * strength) for term, strength in term_list_L
        # with given `qtotal`
        for term_L, strength in term_list_L:
            ops_L, i_min, need_JW = self._term_to_ops_list(term_L, autoJW, i_L, None)
            if i_min + len(ops_L) < i:
                ops_L = ops_L + [opstr_fill[need_JW]] * (i - (i_min + len(ops_L)))
            CL = self._corr_ops_LP(ops_L, i_min)
            key = (need_JW, ) + tuple(CL.qtotal)
            if key not in CLs:
                CLs[key] = strength * CL
            else:
                CLs[key] = CLs[key] + strength * CL
        axes = [['vL*'] + self._get_p_label('*'), ['vR*'] + self._p_label]
        result = []
        for j in j_R:
            j = j + min_R  # start ops_R on site `j`
            assert i <= j
            for k in range(i, j):
                assert i == k
                # contract CL with tensors on site `k`
                B = self.get_B(k, form='B')
                for key, CL in CLs.items():
                    need_JW = key[0]
                    CL = npc.tensordot(CL, B, axes=['vR', 'vL'])
                    if opstr_fill[need_JW] != 'Id':
                        opstr_k = self.sites[self._to_valid_index(k)].get_op(opstr_fill[need_JW])
                        CL = npc.tensordot(opstr_k, CL, axes=['p*', 'p'])
                    CLs[key] = npc.tensordot(B.conj(), CL, axes=axes)
                i = k + 1
            res = 0.
            for ops_R, need_JW, strength in zip(all_ops_R, need_JW_R, term_list_R.strength):
                CR = self._corr_ops_RP(ops_R, j)
                key = (need_JW, ) + tuple(self.chinfo.make_valid(-CR.qtotal))
                CL = CLs.get(key, None)
                if CL is None:
                    continue  # nothing to pair up with
                res = res + strength * npc.inner(CL, CR, axes=[['vR', 'vR*'], ['vL', 'vL*']])
            result.append(res)
        return np.real_if_close(result)

    def sample_measurements(self,
                            first_site=0,
                            last_site=None,
                            ops=None,
                            rng=None,
                            norm_tol=1.e-12):
        """Sample measurement results in the computational basis.

        This function samples projective measurements on a continguous range of sites,
        tracing out the remaining sites.

        Note that for infinite boundary conditions, the probablility of sampling
        a set of `sigmas` is **not** ``|psi.overlap(MPS.from_product_state(sigmas, ...))|^2``,
        because the latter would poject to the set `sigmas` on *each* (translated) MPS unit cell,
        while this function is only projecting to them in a *single* MPS unit cell.

        Parameters
        ----------
        first_site, last_site : int
            Take measurements on the sites in ``range(first_site, last_site + 1)``.
            `last_site` defaults to :attr:`L` - 1.
        ops : list of str
            If not None, sample in the eigenbasis of
            ``self.sites[i].get_op(ops[(i - first_site) % len(ops)])`` and directly return the
            corresponding eigenvalue in `sigmas`.
        rng : :class:`numpy.random.Generator`
            The random number generator; if None, a new `numpy.random.default_rng()` is generated.
        norm_tol : float
            Tolerance

        Returns
        -------
        sigmas : list of int | list of float
            On each site the index of the local basis that was measured,
            as specified in the corrsponding :class:`~tenpy.networks.site.Site` in :attr:`sites`.
            Note that this can change depending on whether/what charges you conserve!
            Explicitly specifying the measurement operator will avoid that issue.
        weight : float
            The weight ``sqrt(trace(|psi><psi|sigmas...><sigmas...|))``, i.e.,
            the probability of measuring ``|sigmas...>`` is ``weigth**2``.
            For a finite system where we sample all sites (i.e., the trace over the compliment of
            the sites is trivial), this is the actual overlap ``<sigmas...|psi>``
            including the phase.
        """
        if tuple(self._p_label) != ('p', ):
            raise NotImplementedError("Only works for a single physical 'p' leg")
        if last_site is None:
            last_site = self.L - 1
        if rng is None:
            rng = np.random.default_rng()
        sigmas = []
        total_weight = 1.
        theta = self.get_theta(first_site, n=1).replace_label('p0', 'p')
        for i in range(first_site, last_site + 1):
            # theta = wave function in basis vL [sigmas...] p vR
            # where the `sigmas` are already fixed to the measurement results
            i0 = self._to_valid_index(i)
            site = self.sites[i0]
            if ops is not None:
                op_name = ops[(i - first_site) % len(ops)]
                op = site.get_op(op_name).transpose(['p', 'p*'])
                if npc.norm(op - op.conj().transpose()) > 1.e-13:
                    raise ValueError(f"measurement operator {op_name!r} not hermitian")
                W, V = npc.eigh(op)
                theta = npc.tensordot(V.conj(), theta, axes=['p*', 'p']).replace_label('eig*', 'p')
            else:
                W = np.arange(site.dim)
            # perform a projective measurement:
            # trace out rest except site `i`
            rho = npc.tensordot(theta.conj(), theta, [['vL*', 'vR*'], ['vL', 'vR']])
            # probabilities p(sigma) = <sigma|rho|sigma>
            rho_diag = np.abs(np.diag(rho.to_ndarray()))  # abs: real dtype & roundoff err
            if abs(np.sum(rho_diag) - 1.) > norm_tol:
                raise ValueError("not normalized to `norm_tol`")
            rho_diag /= np.sum(rho_diag)
            sigma = rng.choice(site.dim, p=rho_diag)  # randomly select index from probabilities
            sigmas.append(W[sigma])
            theta = theta.take_slice(sigma, 'p')  # project to sigma in theta for remaining rho
            weight = npc.norm(theta)
            total_weight *= weight
            if i != last_site:
                # attach next site to sigma
                theta = theta / npc.norm(theta)
                B = self.get_B(i + 1)
                theta = npc.tensordot(theta, B, axes=['vR', 'vL'])
                # B is right-canonical -> theta still normalized
            elif self.bc == 'finite' and first_site == 0 and last_site == self.L - 1:
                assert theta.shape == (1, 1)
                # already divided by norm, so only include the phase now
                total_weight = total_weight * theta[0, 0] / weight
        return sigmas, total_weight

    def norm_test(self):
        """Check that self is in canonical form.

        Returns
        -------
        norm_error: array, shape (L, 2)
            For each site the norm error to the left and right.
            The error ``norm_error[i, 0]`` is defined as the norm-difference between
            the following networks::

                |   --theta[i]---.       --s[i]--.
                |       |        |    vs         |
                |   --theta*[i]--.       --s[i]--.

            Similarly, ``norm_errror[i, 1]`` is the norm-difference of::

                |   .--theta[i]---         .--s[i+1]--
                |   |    |          vs     |
                |   .--theta*[i]--         .--s[i+1]--
        """
        err = np.empty((self.L, 2), dtype=float)
        lbl_R = (self._get_p_label('0') + ['vR'], self._get_p_label('0*') + ['vR*'])
        lbl_L = (['vL'] + self._get_p_label('0'), ['vL*'] + self._get_p_label('0*'))
        for i in range(self.L):
            th = self.get_theta(i, 1)
            rho_L = npc.tensordot(th, th.conj(), axes=lbl_R)
            S = self.get_SL(i)
            if isinstance(S, npc.Array):  # during DMRG with mixer, S may be a 2D npc.Array
                if S.rank != 2:
                    raise ValueError("Expect 2D npc.Array or 1D numpy ndarray")
                rho_L2 = npc.tensordot(S, S.conj(), axes=['vR', 'vR*'])
            else:
                rho_L2 = npc.diag(S**2, rho_L.get_leg('vL'), dtype=rho_L.dtype)
            err[i, 0] = npc.norm(rho_L - rho_L2)
            rho_R = npc.tensordot(th, th.conj(), axes=lbl_L)
            S = self.get_SR(i)
            if isinstance(S, npc.Array):
                if S.rank != 2:
                    raise ValueError("Expect 2D npc.Array or 1D numpy ndarray")
                rho_R2 = npc.tensordot(S, S.conj(), axes=['vL', 'vL*'])
            else:
                rho_R2 = npc.diag(S**2, rho_R.get_leg('vR'), dtype=rho_L.dtype)
            err[i, 1] = npc.norm(rho_R - rho_R2)
        return err

    def canonical_form(self, **kwargs):
        """Bring self into canonical 'B' form, (re-)calculate singular values.

        Simply calls :meth:`canonical_form_finite` or :meth:`canonical_form_infinite`.
        Keyword arguments are passed on to the corrsponding specialized versions.
        """
        if self.finite:
            return self.canonical_form_finite(**kwargs)
        else:
            return self.canonical_form_infinite(**kwargs)

    def canonical_form_finite(self, renormalize=True, cutoff=0., envs_to_update=None):
        """Bring a finite (or segment) MPS into canonical form (in place).

        If any site is in :attr:`form` ``None``, it does *not* use any of the singular values `S`
        (for 'finite' boundary conditions, or only the very left `S` for 'segment' b.c.).
        If all sites have a `form`, it respects the `form` to ensure
        that one `S` is included per bond.
        The final state is always in right-canonical 'B' form.

        Performs one sweep left to right doing QR decompositions, and one sweep right to left
        doing SVDs calculating the singular values.

        Parameters
        ----------
        renormalize: bool
            Whether a change in the norm should be discarded or used to update :attr:`norm`.
        cutoff : float | None
            Cutoff of singular values used in the SVDs.
        envs_to_update : None | list of :class:`MPSEnvironment`
            Clear the environments; for segment also update the left/rightmost LP/RP.

        Returns
        -------
        U_L, V_R : :class:`~tenpy.linalg.np_conserved.Array`
            Only returned for ``'segment'`` boundary conditions.
            The unitaries defining the new left and right Schmidt states in terms of the old ones,
            with legs ``'vL', 'vR'``.
        """
        assert (self.finite)
        L = self.L
        assert (L > 2)  # otherwise implement yourself...
        # normalize very left singular values
        S = self.get_SL(0)
        if self.bc == 'segment':
            if S is None:
                raise ValueError("Need S[0] and S[L] for segment boundary conditions.")
            self.set_SL(0, S / np.linalg.norm(S))
            S = self.get_SR(L - 1)
            self.set_SR(L - 1, S / np.linalg.norm(S))
        else:  # bc == 'finite':
            self.set_SL(0, np.array([1.]))  # trivial singular value on very left/right
            self.set_SR(L - 1, np.array([1.]))
        # sweep from left to right to bring it into left canonical form.
        if any([(f is None) for f in self.form]):
            # ignore any 'S' and canonical form
            M = self.get_B(0, form=None)
            form = None
        else:
            # we actually had a canonical form before, so we should *not* ignore the 'S'
            M = self.get_B(0, form='Th')
            form = 'B'  # for other 'M'
        Q, R = npc.qr(M.combine_legs(['vL'] + self._p_label), inner_labels=['vR', 'vL'])
        # Q = unitary, R has to be multiplied to the right
        self.set_B(0, Q.split_legs(0), form='A')
        for i in range(1, L - 1):
            M = self.get_B(i, form)
            M = npc.tensordot(R, M, axes=['vR', 'vL'])
            Q, R = npc.qr(M.combine_legs(['vL'] + self._p_label), inner_labels=['vR', 'vL'])
            # Q is unitary, i.e. left canonical, R has to be multiplied to the right
            self.set_B(i, Q.split_legs(0), form='A')
        M = self.get_B(L - 1, form)
        M = npc.tensordot(R, M, axes=['vR', 'vL'])
        if self.bc == 'segment':
            # also neet to calculate new singular values on the very right
            U, S, VR_segment = npc.svd(M.combine_legs(['vL'] + self._p_label),
                                       cutoff=cutoff,
                                       inner_labels=['vR', 'vL'])
            S /= np.linalg.norm(S)
            self.set_SR(L - 1, S)
            M = U.scale_axis(S, 1).split_legs(0)
        else:
            VR_segment = None
        # sweep from right to left, calculating all the singular values
        U, S, V = npc.svd(M.combine_legs(['vR'] + self._p_label, qconj=-1),
                          cutoff=cutoff,
                          inner_labels=['vR', 'vL'])
        if not renormalize:
            self.norm = self.norm * np.linalg.norm(S)
        S = S / np.linalg.norm(S)  # normalize
        self.set_SL(L - 1, S)
        self.set_B(L - 1, V.split_legs(1), form='B')
        for i in range(L - 2, -1, -1):
            M = self.get_B(i, 'A')
            M = npc.tensordot(M, U.scale_axis(S, 'vR'), axes=['vR', 'vL'])
            U, S, V = npc.svd(M.combine_legs(['vR'] + self._p_label, qconj=-1),
                              cutoff=cutoff,
                              inner_labels=['vR', 'vL'])
            S = S / np.linalg.norm(S)  # normalize
            self.set_SL(i, S)
            self.set_B(i, V.split_legs(1), form='B')
        if self.bc == 'finite':
            assert len(S) == 1
            self._B[0] *= U[0, 0]  # just a trivial phase factor, but better keep it
        # done with getting to canonical form
        if envs_to_update is not None and self.bc == 'segment':
            VR = VR_segment
            for env in envs_to_update:
                update_ket = env.ket is self
                update_bra = env.bra is self
                if not (update_ket or update_bra):
                    raise ValueError("called `psi.canonical_from_finite(..., envs_to_update), "
                                     "but (one of) the environment doesn't contain that `psi`")
                env.clear()
                if self.bc == 'segment':
                    LP = env.get_LP(0)
                    if update_ket:
                        LP = npc.tensordot(LP, U, axes=['vR', 'vL'])
                    if update_bra:
                        LP = npc.tensordot(U.conj(), LP, axes=['vL*', 'vR*'])
                    env.set_LP(0, LP, env.get_LP_age(0))
                    RP = env.get_RP(env.L - 1)
                    if update_ket:
                        RP = npc.tensordot(VR, RP, axes=['vR', 'vL'])
                    if update_bra:
                        RP = npc.tensordot(RP, VR.conj(), axes=['vL*', 'vR*'])
                    env.set_RP(env.L - 1, RP, env.get_RP_age(env.L - 1))
        if self.bc == 'segment':
            old_UL, old_VR = self.segment_boundaries
            if old_UL is not None:
                new_UL = npc.tensordot(old_UL, U, axes=['vR', 'vL'])
                new_VR = npc.tensordot(VR_segment, old_VR, axes=['vR', 'vL'])
                self.segment_boundaries = (new_UL, new_VR)
            else:
                self.segment_boundaries = (U, VR_segment)
            return U, VR_segment

    def canonical_form_infinite(self, renormalize=True, tol_xi=1.e6):
        """Bring an infinite MPS into canonical form (in place).

        If any site is in :attr:`form` ``None``, it does *not* use any of the singular values `S`.
        If all sites have a `form`, it respects the `form` to ensure
        that one `S` is included per bond.
        The final state is always in right-canonical 'B' form.

        Proceeds in three steps, namely 1) diagonalize right and left transfermatrix on a given
        bond to bring that bond into canonical form, and then
        2) sweep right to left, and 3) left to right to bringing other bonds into canonical form.

        .. warning :
            You might *loose* precision when calling this function.
            When we diagonalize the transfermatrix, we get the singular values squared as
            eigenvalues, with numerical noise on the order of machine precision (usually ~1.e-15).
            Taking the square root, the new singular values are only precise to *half* the machine
            precision (usually ~1.e-7).

        Parameters
        ----------
        renormalize: bool
            Whether a change in the norm should be discarded or used to update :attr:`norm`.
        tol_xi : float
            Raise an error if the correlation length is larger than that
            (which indicates a degenerate "cat" state, e.g., for spontaneous symmetry breaking).
        """
        assert not self.finite
        L = self.L
        i1 = np.argmin(self.chi) % L  # start at this bond
        if any([(f is None) for f in self.form]):
            # ignore any 'S' and canonical form, just state that we are in 'B' form
            self.form = self._parse_form('B')
            self._S[i1] = np.ones(self.chi[i1], dtype=np.float64)  # (later used for guess of Gl)
        else:
            # was in canonical form before; bring back into canonical form
            # -> make sure we don't use multiple S on one bond in our definition of the MPS
            self.convert_form('B')
        Wr_list = [None] * L  # right eigenvectors of TM on each bond after ..._correct_right

        # phase 1: bring bond (i1-1, i1) in canonical form
        # find dominant right eigenvector
        norm, Gr = self._canonical_form_dominant_gram_matrix(i1, False, tol_xi)
        self._B[i1] /= np.sqrt(norm)  # correct norm
        if not renormalize:
            self.norm *= np.sqrt(norm)
        # make Gr diagonal to Wr
        Wr, Kl, Kr = self._canonical_form_correct_right(i1, Gr)
        # guess for Gl
        Gl = npc.tensordot(Kr.scale_axis(self.get_SL(i1)**2, 1), Kl, axes=['vR', 'vL'])
        Gl.iset_leg_labels(['vR*', 'vR'])
        # find dominant left eigenvector
        norm, Gl = self._canonical_form_dominant_gram_matrix(i1, True, tol_xi, Gl)
        if abs(1. - norm) > 1.e-13:
            logger.warning("Although we renormalized the TransferMatrix, "
                           "the largest eigenvalue is not 1")  # (this shouldn't happen)
        self._B[i1] /= np.sqrt(norm)  # correct norm again
        if not renormalize:
            self.norm *= np.sqrt(norm)
        # bring bond to canonical form
        Gl, Yl, Yr = self._canonical_form_correct_left(i1, Gl, Wr)
        Wr = np.ones(Yr.legs[0].ind_len, np.float64)
        # now the bond (i1-1,i1) is in canonical form
        Wr_list[i1] = Wr  # diag(Wr) is right eigenvector on bond (i1-1, i1)

        # phase 2: sweep from right to left; find other right eigenvectors and make them diagonal
        for j1 in range(i1 - 1, i1 - L, -1):
            B1 = self.get_B(j1, 'B')
            axes = [self._p_label + ['vR'], self._get_p_label('*') + ['vR*']]
            Gr = npc.tensordot(B1.scale_axis(Wr, 'vR'), B1.conj(), axes=axes)
            Wr, Kl, Kr = self._canonical_form_correct_right(j1, Gr)
            Wr_list[j1 % L] = Wr

        # phase 3: sweep from left to right; find other left eigenvectors,
        # bring each bond into canonical form
        for j1 in range(i1 - L + 1, i1, +1):
            # find Gl on bond j1-1, j1
            B1 = self.get_B(j1 - 1, 'B')
            Gl = npc.tensordot(
                B1.conj(),  # old B1; now on site j1-1
                npc.tensordot(Gl, B1, axes=['vR', 'vL']),
                axes=[self._get_p_label('*') + ['vL*'], self._p_label + ['vR*']])
            # axes=[['p*', 'vL*'], ['p', 'vR*']])
            Gl, Yl, Yr = self._canonical_form_correct_left(j1, Gl, Wr_list[j1 % L])
        # done

    def correlation_length(self, target=1, tol_ev0=1.e-8, charge_sector=0):
        r"""Calculate the correlation length by diagonalizing the transfer matrix.

        Assumes that `self` is in canonical form.

        Works only for infinite MPS, where the transfer matrix is a useful concept.
        Assuming a single-site unit cell, any correlation function splits into
        :math:`C(A_i, B_j) = A'_i T^{j-i-1} B'_j`
        with some parts left and right and the :math:`j-i-1`-th power of the transfer matrix in
        between. The largest eigenvalue is 1 (if self is properly normalized)
        and gives the dominant contribution of
        :math:`A'_i E_1 * 1^{j-i-1} * E_1^T B'_j = <A> <B>`,
        and the second largest one gives a contribution :math:`\propto \lambda_2^{j-i-1}`.
        Thus :math:`\lambda_2 = \exp(-\frac{1}{\xi})`.

        More general for a `L`-site unit cell we get :math:`\lambda_2 = \exp(-\frac{L}{\xi})`,
        where the `xi` is given in units of 1 lattice spacing in the MPS.

        .. warning ::
            For a higher-dimensional lattice (which the MPS class doesn't know about),
            the correct unit is the lattice spacing in x-direction, and the correct formula is
            :math:`\lambda_2 = \exp(-\frac{L_x}{\xi})`,
            where `L_x` is the number of lattice spacings in the infinite direction within the
            MPS unit cell, e.g. the number of "rings" of a cylinder in the MPS unit cell.
            To get to these units, divide the returned `xi` by the number of sites within a "ring",
            for a lattice given in :attr:`~tenpy.models.lattice.Lattice.N_sites_per_ring`.

        Parameters
        ----------
        target : int
            We look for the `target` + 1 largest eigenvalues.
        tol_ev0 : float
            Print warning if largest eigenvalue deviates from 1 by more than `tol_ev0`.
        charge_sector : None | charges | ``0``
            Selects the charge sector in which the dominant eigenvector of the TransferMatrix is.
            ``None`` stands for *all* sectors, ``0`` stands for the zero-charge sector.
            Defaults to ``0``, i.e., *assumes* the dominant eigenvector is in charge sector 0.

        Returns
        -------
        xi : float | 1D array
            If `target` = 1, return just the correlation length,
            otherwise an array of the `target` largest correlation lengths.
            It is measured in units of a single spacing between sites in the MPS language,
            see the warning above.
        """
        assert (not self.finite)
        T = TransferMatrix(self, self, charge_sector=charge_sector, form='B')
        num = max(target + 1, self._transfermatrix_keep)
        E, _ = T.eigenvectors(num, which='LM')
        E = E[np.argsort(-np.abs(E))]  # sort descending by magnitude
        if charge_sector is not None and charge_sector != 0 and \
                any([c != 0 for c in charge_sector]):
            # need also dominant eigenvector: include 0 charge sector to results
            del T
            T = TransferMatrix(self, self, charge_sector=0, form='B')
            E0, _ = T.eigenvectors(num, which='LM')
            assert abs(E0[0]) > abs(E[0]), "dominant eigenvector in zero charge sector?"
            E = np.array([E0[0]] + list(E))
        if abs(E[0] - 1.) > tol_ev0:
            logger.warning("Correlation length: largest eigenvalue not one. "
                           "Not in canonical form/normalized?")
        if len(E) < 2:
            return 0.  # only a single eigenvector: zero correlation length
        if target == 1:
            return -1. / np.log(abs(E[1] / E[0])) * self.L
        return -1. / np.log(np.abs(E[1:target + 1] / E[0])) * self.L

    def add(self, other, alpha, beta, cutoff=1.e-15):
        """Return an MPS which represents ``alpha|self> + beta |others>``.

        Works only for 'finite', 'segment' boundary conditions.
        For 'segment' boundary conditions, the virtual legs on the very left/right are
        assumed to correspond to each other (i.e. self and other have the same state outside of
        the considered segment).
        Takes into account :attr:`norm`.

        Parameters
        ----------
        other : :class:`MPS`
            Another MPS of the same length to be added with self.
        alpha, beta : complex float
            Prefactors for self and other. We calculate
            ``alpha * |self> + beta * |other>``
        cutoff : float | None
            Cutoff of singular values used in the SVDs.

        Returns
        -------
        sum : :class:`MPS`
            An MPS representing ``alpha|self> + beta |other>``.
            Has same total charge as `self`.
        """
        L = self.L
        assert (other.L == L and L >= 2)  # (if you need this, generalize this function...)
        assert self.finite
        assert self.bc == other.bc
        self._gauge_compatible_vL_vR(other)
        legs = ['vL', 'vR'] + self._p_label
        # alpha and beta appear only on the first site
        alpha = alpha * self.norm
        beta = beta * other.norm
        theta_self = self.get_B(0, 'Th').transpose(legs)
        theta_other = other.get_B(0, 'Th').transpose(legs)
        last_B_self = self.get_B(L - 1).transpose(legs)
        last_B_other = other.get_B(L - 1).transpose(legs)
        U, V = self.segment_boundaries
        if U is not None:
            theta_self = npc.tensordot(U, theta_self, axes=['vR', 'vL']).transpose(legs)
            last_B_self = npc.tensordot(last_B_self, V, axes=['vR', 'vL']).transpose(legs)
        U, V = other.segment_boundaries
        if U is not None:
            theta_other = npc.tensordot(U, theta_other, axes=['vR', 'vL']).transpose(legs)
            last_B_other = npc.tensordot(last_B_other, V, axes=['vR', 'vL']).transpose(legs)
        Bs = [npc.grid_concat([[alpha * theta_self, beta * theta_other]], axes=[0, 1])]
        for i in range(1, L - 1):
            B1 = self.get_B(i).transpose(legs)
            B2 = other.get_B(i).transpose(legs)
            grid = [[B1, npc.zeros([B1.get_leg('vL'), B2.get_leg('vR')] + B1.legs[2:])],
                    [npc.zeros([B2.get_leg('vL'), B1.get_leg('vR')] + B1.legs[2:]), B2]]
            Bs.append(npc.grid_concat(grid, [0, 1]))
        Bs.append(npc.grid_concat([[last_B_self], [last_B_other]], axes=[0, 1]))
        Ss = [np.ones(Bs[0].shape[0])] + [np.ones(B.shape[1]) for B in Bs]
        psi = self.__class__(self.sites, Bs, Ss, self.bc, form=None)  # new class instance
        # bring to canonical form, calculate Ss
        psi.canonical_form_finite(renormalize=False, cutoff=cutoff)
        return psi

    def apply_local_op(self, i, op, unitary=None, renormalize=False, cutoff=1.e-13):
        """Apply a local (one or multi-site) operator to `self`.

        Note that this destroys the canonical form if the local operator is non-unitary.
        Therefore, this function calls :meth:`canonical_form` if necessary.

        Parameters
        ----------
        i : int
            (Left-most) index of the site(s) on which the operator should act.
        op : str | npc.Array
            A physical operator acting on site `i`, with legs ``'p', 'p*'`` for a single-site
            operator or with legs ``['p0', 'p1', ...], ['p0*', 'p1*', ...]`` for an operator
            acting on `n`>=2 sites.
            Strings (like ``'Id', 'Sz'``) are translated into single-site operators defined by
            :attr:`sites`.
        unitary : None | bool
            Whether `op` is unitary, i.e., whether the canonical form is preserved (``True``)
            or whether we should call :meth:`canonical_form` (``False``).
            ``None`` checks whether ``norm(op dagger(op) - identity)`` is smaller than `cutoff`.
        renormalize : bool
            Whether the final state should keep track of the norm (False, default) or be
            renormalized to have norm 1 (True).
        cutoff : float
            Cutoff for singular values if `op` acts on more than one site (see :meth:`from_full`).
            (And used as cutoff for a unspecified `unitary`.)
        """
        i = self._to_valid_index(i)
        if isinstance(op, str):
            op = self.sites[i].get_op(op)
        n = op.rank // 2  # same as int(rank/2)
        if n == 1:
            pstar, p = 'p*', 'p'
        else:
            p = self._get_p_labels(n, False)
            pstar = self._get_p_labels(n, True)
        if unitary is None:
            op_op_dagger = npc.tensordot(op, op.conj(), axes=[pstar, p])
            if n > 1:
                op_op_dagger = op_op_dagger.combine_legs([p, pstar], qconj=[+1, -1])
            unitary = npc.norm(op_op_dagger - npc.eye_like(op_op_dagger)) < cutoff
        if n == 1:
            opB = npc.tensordot(op, self._B[i], axes=['p*', 'p'])
            self.set_B(i, opB, self.form[i])
        else:
            th = self.get_theta(i, n)
            th = npc.tensordot(op, th, axes=[pstar, p])
            # use MPS.from_full to split the sites
            split_th = self.from_full(self.sites[i:i + n], th, None, cutoff, False, 'segment',
                                      (self.get_SL(i), self.get_SR(i + n - 1)))
            for j in range(n):
                self.set_B(i + j, split_th._B[j], split_th.form[j])
            for j in range(n - 1):
                self.set_SR(i + j, split_th._S[j + 1])
        if not unitary:
            self.canonical_form(renormalize=renormalize)

    def apply_product_op(self, ops, unitary=None, renormalize=False):
        """Apply a (global) product of local onsite operators to `self`.

        Note that this destroys the canonical form if any local operator is non-unitary.
        Therefore, this function calls :meth:`canonical_form` if necessary.

        The result is equivalent to the following loop, but more efficient by avoiding
        intermediate calls to :meth:`canonical_form` inside the loop::

            for i, op in enumerate(ops):
                self.apply_local_op(i, op, unitary, renormalize, cutoff)

        Parameters
        ----------
        ops : (list of) str | npc.Array
            List of onsite operators to apply on each site, with legs ``'p', 'p*'``.
            Strings (like ``'Id', 'Sz'``) are translated into single-site operators defined by
            :attr:`sites`.
        unitary : None | bool
            Whether `op` is unitary, i.e., whether the canonical form is preserved (``True``)
            or whether we should call :meth:`canonical_form` (``False``).
            ``None`` checks whether ``max(norm(op dagger(op) - identity) for op in ops) < 1.e-14``
        renormalize : bool
            Whether the final state should keep track of the norm (False, default) or be
            renormalized to have norm 1 (True).
        """
        ops = to_iterable(ops)
        if self.L % len(ops) != 0:
            raise ValueError("len of ops incommensurate with self.L")
        self.convert_form('B')
        for i in range(self.L):
            op = ops[i % len(ops)]
            if isinstance(op, str):
                if op == 'Id':
                    continue  # nothing to do here...
                op = self.sites[i].get_op(op)
            if unitary is None:
                op_op_dagger = npc.tensordot(op, op.conj(), axes=['p*', 'p'])
                if npc.norm(op_op_dagger - npc.eye_like(op_op_dagger)) > 1.e-14:
                    unitary = False
            # actually apply the operator at site i
            self._B[i] = npc.tensordot(op, self._B[i], axes=['p*', 'p'])
        if not unitary:
            self.canonical_form(renormalize=renormalize)

    def perturb(self, randomize_params=None, close_1=True, canonicalize=None):
        """Locally perturb the state a little bit; in place.

        Parameters
        ----------
        randomize_params : dict
            Parameters for the :class:`~tenpy.algorithms.tebd.RandomUnitaryEvolution`.
        close_1 : bool
            Select the default :cfg:option:`RandomUnitaryEvolution.distribution_func` to be used,
            if `close_1` is True, use :func:`~tenpy.linalg.random_matrix.U_close_1` for complex and
            :func:`~tenpy.linalg.random_matrix.O_close_1` for real MPS;
            for `close_1` False use :func:`~tenpy.linalg.random_matrix.CUE` or
            :func:`~tenpy.linalg.random_matrix.CRE`, respectively.
        canonicalize : bool
            Wether to call `psi.canonical_from in the end. Defaults to ``not close_1``.
        """
        from ..algorithms.tebd import RandomUnitaryEvolution
        if randomize_params is None:
            randomize_params = {}
        if close_1:
            func = 'U_close_1' if self.dtype.kind == 'c' else 'O_close_1'
        else:
            func = 'CUE' if self.dtype.kind == 'c' else 'CRE'
        randomize_params.setdefault('distribution_func', func)
        eng = RandomUnitaryEvolution(self, randomize_params)
        eng.run()
        if canonicalize is None:
            canonicalize = not close_1
        if canonicalize:
            self.canonical_form()
        # done

    def swap_sites(self, i, swap_op='auto', trunc_par=None):
        r"""Swap the two neighboring sites `i` and `i+1` (inplace).

        Exchange two neighboring sites: form theta, 'swap' the physical legs and split
        with an svd. While the 'swap' is just a transposition/relabeling for bosons, one needs to
        be careful about the signs from Jordan-Wigner strings for fermions.

        Parameters
        ----------
        i : int
            Swap the two sites at positions `i` and `i+1`.
        swap_op : ``None`` | ``'auto', 'autoInv'`` | :class:`~tenpy.linalg.np_conserved.Array`
            The operator used to swap the phyiscal legs of the two-site wave function `theta`.
            For ``None``, just transpose/relabel the legs.
            Alternative give an npc :class:`~tenpy.linalg.np_conserved.Array`
            which represents the full operator used for the swap.
            Should have legs ``['p0', 'p1', 'p0*', 'p1*']`` whith ``'p0', 'p1*'`` contractible.
            For ``'auto'`` we try to be smart about fermionic signs, see note below.
        trunc_par : dict
            Parameters for truncation, see :cfg:config:`truncation`.

        Returns
        -------
        trunc_err : :class:`~tenpy.algorithms.truncation.TruncationError`
            The error of the represented state introduced by the truncation after the swap.

        Notes
        -----
        For fermions, it's crucial to use the correct `swap_op`.
        The `swap_op` is a two-site operator exchanging 'p0' and 'p1' legs.
        For bosons, this is really just a relabeling (done for ``swap_op=None``).
        Alternatively, you can construct the operator explicitly like this::

            siteL, siteR = psi.sites[i], psi.sites[i+1]
            dL, dR = siteL.dim, siteR.dim
            legL, legR = siteL.leg, siteR.leg
            swap_op_dense = np.eye(dL*dR)
            swap_op = npc.Array.from_ndarray(swap_op_dense.reshape([dL, dR, dL, dR]),
                                             [legL, legR, legL.conj(), legR.conj()],
                                             labels=['p1', 'p0', 'p0*', 'p1*'])

        However, for fermions we need to be very
        careful about the Jordan-Wigner strings. Let's derive how the operator should look like.

        You can write a state as

        .. math ::
            |\psi> = \sum_{[n_j]} \psi_{[n_j]} \prod_j (c^\dagger_j)^{n_j}  |vac>

        where ``[n_j]`` denontes a set of :math:`n_j \in [0, 1]` for
        each physical site `j` and the product over `j` is taken in increasing order.
        Let :math:`P` be the operator switching ``i <-> i+1``, with inverse :math:`P^\dagger`.
        Then:

        .. math ::
            P |\psi> = \sum_{[n_j]} \psi_{[n_i]} P \prod_j (c^\dagger_i)^{n_j} |vac> \\
                 = \sum_{[n_j]} \psi_{[n_i]} P \prod_j (c^\dagger_i)^{n_j}   |vac>

        When :math:`P` acts on the product of :math:`c^\dagger_{i}` operators,
        it commutes :math:`(c^\dagger_i)^{n_i}` with :math:`(c^\dagger_{i+1})^{n_{i+1}}`.
        This gives a a sign :math:`(-1)^{n_i * n_{i+1}}`. We must hence include this sign in
        the swap operator.
        The `n_i` in the equations above is given by :attr:`~tenpy.networks.site.JW_exponent`.
        This leads to the following swap operator used for fermions with ``swap_op='auto'``,
        suitable to just permute sites::

            siteL, siteR = psi.sites[i], psi.sites[i+1]
            dL, dR = siteL.dim, siteR.dim
            legL, legR = siteL.leg, siteR.leg
            n_i_n_j = np.outer(siteL.JW_exponent, siteR.JW_exponent).reshape(dL*dR)
            swap_op_dense = np.diag((-1)**n_i_n_j)
            swap_op = npc.Array.from_ndarray(swap_op_dense.reshape([dL, dR, dL, dR]),
                                             [legL, legR, legL.conj(), legR.conj()],
                                             labels=['p1', 'p0', 'p0*', 'p1*'])


        In some cases you might want to use a more complicated swap operator.
        As outlined in (the appendix of) :cite:`shapourian2017`, a typical hamiltonian of the form
        :math:`H = -t \sum_i c_i^\dagger c_{i+1} + h.c.  + \text{density interaction}`
        is invariant under a reflection :math:`R` acting as :math:`R c^e_x R^\dagger = i c^o_{-x}`
        and :math:`R c^o_x R^\dagger = i c^e_{-x}` for even/odd fermion sites.
        The following code includes the factor of :math:`i`,
        or rather :math:`-i` since we have creation operators, into the swap operator and is used
        with ``swap_op='autoInv'``::

            siteL, siteR = psi.sites[i], psi.sites[i+1]
            dL, dR = siteL.dim, siteR.dim
            legL, legR = siteL.leg, siteR.leg
            n_i = np.outer(siteL.JW_exponent, np.ones(dR)).reshape(dL*dR)
            n_j = np.outer(np.ones(dL), siteR.JW_exponent).reshape(dL*dR)
            swap_op_dense = np.diag((-1)**(n_i * n_j) * (-1.j)**n_i * (-1.j)**n_j)
            swap_op = npc.Array.from_ndarray(swap_op_dense.reshape([dL, dR, dL, dR]),
                                             [legL, legR, legL.conj(), legR.conj()],
                                             labels=['p1', 'p0', 'p0*', 'p1*'])

        """
        if trunc_par is None:
            trunc_par = {}
        siteL, siteR = self.sites[self._to_valid_index(i)], self.sites[self._to_valid_index(i + 1)]
        if isinstance(swap_op, str):
            dL, dR = siteL.dim, siteR.dim
            # site.JW_exponent is just the `n_i` in the equations of the note above.
            n_i = np.outer(siteL.JW_exponent, np.ones(dR)).reshape(dL * dR)
            n_j = np.outer(np.ones(dL), siteR.JW_exponent).reshape(dL * dR)
            if np.any(n_i * n_j):
                legL, legR = siteL.leg, siteR.leg
                if swap_op == 'auto':
                    swap_op_diag = (-1.)**(n_i * n_j)
                elif swap_op == 'autoInv':
                    swap_op_diag = (-1.)**(n_i * n_j) * (-1.j)**n_i * (-1.j)**n_j
                else:
                    raise ValueError("don't understand swap_op = " + repr(swap_op))
                legs = [siteL.leg, siteR.leg, siteL.leg.conj(), siteR.leg.conj()]
                swap_op = npc.Array.from_ndarray(np.diag(swap_op_diag).reshape([dL, dR, dL, dR]),
                                                 legs,
                                                 labels=['p1', 'p0', 'p0*', 'p1*'])
            else:  # at least one site isn't Fermions -> commutes
                swap_op = None  # continue with transposition as for Bosons
        theta = self.get_theta(i, n=2)
        C = self.get_theta(i, n=2, formL=0.)  # inversion free, see also TEBDEngine.update_bond()
        if swap_op is None:
            # just replace the labels, effectively this is a transposition.
            theta.ireplace_labels(['p0', 'p1'], ['p1', 'p0'])
            C.ireplace_labels(['p0', 'p1'], ['p1', 'p0'])
        elif isinstance(swap_op, npc.Array):
            theta = npc.tensordot(swap_op, theta, axes=[['p0*', 'p1*'], ['p0', 'p1']])
            C = npc.tensordot(swap_op, C, axes=(['p0*', 'p1*'], ['p0', 'p1']))
        else:
            raise ValueError("Invalid swap_op: got " + repr(swap_op))
        theta = theta.combine_legs([('vL', 'p0'), ('vR', 'p1')], qconj=[+1, -1])
        U, S, V, err, renormalize = svd_theta(theta, trunc_par, inner_labels=['vR', 'vL'])
        B_R = V.split_legs(1).ireplace_label('p1', 'p')
        B_L = npc.tensordot(C.combine_legs(('vR', 'p1'), pipes=theta.legs[1]),
                            V.conj(),
                            axes=['(vR.p1)', '(vR*.p1*)'])
        B_L.ireplace_labels(['vL*', 'p0'], ['vR', 'p'])
        B_L /= renormalize  # re-normalize to <psi|psi> = 1
        self.set_SR(i, S)
        self.set_B(i, B_L, 'B')
        self.set_B(i + 1, B_R, 'B')
        self.sites[self._to_valid_index(i)] = siteR  # swap 'sites' as well
        self.sites[self._to_valid_index(i + 1)] = siteL
        return err

    def permute_sites(self, perm, swap_op='auto', trunc_par=None, verbose=None):
        """Applies the permutation perm to the state (inplace).

        .. deprecated :: 0.8.0
            Drop / ignore `verbose` argument, never print something.

        Parameters
        ----------
        perm : ndarray[ndim=1, int]
            The applied permutation, such that ``psi.permute_sites(perm)[i] = psi[perm[i]]``
            (where ``[i]`` indicates the `i`-th site).
        swap_op : ``None`` | ``'auto', 'autoInv'`` | :class:`~tenpy.linalg.np_conserved.Array`
            The operator used to swap the phyiscal legs of a two-site wave function `theta`,
            see :meth:`swap_sites`.
        trunc_par : dict
            Parameters for truncation, see :cfg:config:`truncation`.

        Returns
        -------
        trunc_err : :class:`~tenpy.algorithms.truncation.TruncationError`
            The error of the represented state introduced by the truncation after the swaps.
        """
        if verbose is not None:
            warnings.warn("Dropped verbose argument", category=FutureWarning, stacklevel=2)
        perm = list(perm)  # gets modified, so we should copy
        # In order to keep sites close together, we always scan from the left,
        # keeping everything up to `i` in strictly ascending order.
        # => more or less an 'insertion' sort algorithm.
        # Works nicely for permutations like [1,2,3,0,6,7,8,5] (swapping the 0 and 5 around).
        # For [ 2 3 4 5 6 7 0 1], it splits 0 and 1 apart (first swapping the 0 down, then the 1)
        if trunc_par is None:
            trunc_par = asConfig({}, 'trunc_params')
        trunc_err = TruncationError()
        num_swaps = 0
        i = 0
        while i < self.L - 1:
            if perm[i] > perm[i + 1]:
                trunc = self.swap_sites(i, swap_op, trunc_par)
                num_swaps += 1
                x, y = perm[i], perm[i + 1]
                perm[i + 1], perm[i] = x, y
                # restart from very left; but we know it's already sorted up to i-1
                if i > 0:
                    i -= 1
                trunc_err += trunc
            else:
                i += 1
        return trunc_err

    def compute_K(self,
                  perm,
                  swap_op='auto',
                  trunc_par=None,
                  canonicalize=1.e-6,
                  verbose=None,
                  expected_mean_k=0.):
        r"""Compute the momentum quantum numbers of the entanglement spectrum for 2D states.

        Works for an infinite MPS living on a cylinder, infinitely long in `x` direction and with
        periodic boundary conditions in `y` directions.
        If the state is invariant under 'rotations' around the cylinder axis, one can find the
        momentum quantum numbers of it. (The rotation is nothing more than a translation in `y`.)
        This function permutes some sites (on a copy of `self`) to enact the rotation, and then
        finds the dominant eigenvector of the mixed transfer matrix to get the quantum numbers,
        along the lines of :cite:`pollmann2012`, see also (the appendix and Fig. 11 in the arXiv
        version of) :cite:`cincio2013`.

        .. deprecated :: 0.8.0
            Drop / ignore `verbose` argument, never print something.

        Parameters
        ----------
        perm : 1D ndarray | :class:`~tenpy.models.lattice.Lattice`
            Permuation to be applied to the physical indices, see :meth:`permute_sites`.
            If a lattice is given, we use it to read out the lattice structure and shift
            each site by one lattice-vector in y-direction (assuming periodic boundary conditions).
            (If you have a :class:`~tenpy.models.model.CouplingModel`,
            give its `lat` attribute for this argument)
        swap_op : ``None`` | ``'auto', 'autoInv'`` | :class:`~tenpy.linalg.np_conserved.Array`
            The operator used to swap the phyiscal legs of a two-site wave function `theta`,
            see :meth:`swap_sites`.
        trunc_par : dict
            Parameters for truncation, see :cfg:config:`truncation`.
        canonicalize : float
            Check that `self` is in canonical form; call :meth:`canonical_form`
            if :meth:`norm_test` yields ``np.linalg.norm(self.norm_test()) > canonicalize``.
        expected_mean_k : float
            As explained in :cite:`cincio2013`, the returned `W` is extracted as eigenvector of a
            mixed transfer matrix, and hence has an undefined phase. We fix the overall phase
            such that ``sum(s[j]**2 exp(iK[j]) == np.sum(W) = np.exp(1.j*expected_mean_k)``.

        Returns
        -------
        U : :class:`~tenpy.linalg.np_conserved.Array`
            Unitary representation of the applied permutation on left Schmidt states.
        W : ndarray
            1D array of the form ``S**2 exp(i K)``, where `S` are the Schmidt values
            on the left bond. You can use :func:`np.abs` and :func:`np.angle` to extract the
            (squared) Schmidt values `S` and momenta `K` from `W`.
        q : :class:`~tenpy.linalg.charges.LegCharge`
            LegCharge corresponding to `W`.
        ov : complex
            The eigenvalue of the mixed transfer matrix `<psi|T|psi>` per :attr:`L` sites.
            An absolute value different smaller than 1 indicates that the state is not invariant
            under the permutation or that the truncation error `trunc_err` was too large!
        trunc_err : :class:`~tenpy.algorithms.truncation.TruncationError`
            The error of the represented state introduced by the truncation after swaps when
            performing the truncation.
        """
        from ..models.lattice import Lattice  # dynamical import to avoid import loops
        if self.finite:
            raise ValueError("Works only for infinite b.c.")

        if isinstance(perm, Lattice):
            lat = perm
            assert lat.dim >= 2  # ensure that the lattice is at least 2D
            assert lat.N_sites == self.L
            shifted_lat_order = lat.order.copy()
            shifted_lat_order[:, 1] = np.mod(shifted_lat_order[:, 1] + 1, lat.Ls[1])
            perm = lat.lat2mps_idx(shifted_lat_order)
        # preliminary: check canonical form
        self.convert_form('B')
        norm_err = np.linalg.norm(self.norm_test())
        if norm_err > canonicalize:
            warnings.warn("self.norm_test() = {0!s} ==> canonicalize".format(self.norm_test()))
            self.canonical_form()
        # get copy of self
        psi_t = self.copy()
        # apply permutation
        perm = np.asarray(perm)
        trunc_err = psi_t.permute_sites(perm, swap_op, trunc_par)
        # re-check canonical form
        norm_err = np.linalg.norm(psi_t.norm_test())
        if norm_err > canonicalize:
            logger.warning("norm_error=%.10f after permutation: ==> canonicalize", norm_err)
        psi_t.convert_form('B')
        TM = TransferMatrix(self, psi_t, transpose=True, charge_sector=0)
        # Find left dominant eigenvector of this mixed transfer matrix.
        # Because we are in B form and get the left eigenvector,
        # the resulting vector should be sUs up to a scaling.
        ov, sUs = TM.eigenvectors(num_ev=self._transfermatrix_keep)
        if np.abs(ov[0]) < 0.9:
            logger.warning("compute_K: psi is not eigenvector of permutation/translation in y!"
                           f"expected |o| = 1., got |o| = {abs(ov[0]):.3e}\n")

        logger.info("compute_K: overlap %.5f, |o| = 1. - %.e5., trunc_err.eps=%.3e", ov[0],
                    1. - np.abs(ov[0]), trunc_err.eps)
        sUs = sUs[0].split_legs(0)
        _, sUs_blocked = sUs.as_completely_blocked()
        W = npc.eigvals(sUs_blocked, sort='m>')
        # W = s[j]^2 exp(i K[j]) up to overall scaling and phase
        # (as an eigenvector, sUS has arbitrary/unknown prefactor!)
        W = W / np.sum(np.abs(W))  # fix overal scaling by normalization np.sum(S[i]**2) == 1.
        mean_exp_ik = np.sum(W)
        if np.abs(mean_exp_ik) > 1.e-5:
            W *= np.conj(mean_exp_ik) / np.abs(mean_exp_ik)
        # Strip S's from U
        inv_S = 1. / self.get_SL(0)
        U = sUs.scale_axis(inv_S, 0).iscale_axis(inv_S, 1)  # note: U should commute with s
        # U should be unitary - rescale it such that norm(U)**2 = tr(U^dagger U) = chi
        U *= np.sqrt(U.shape[0]) / npc.norm(U)
        return U, W / np.sum(np.abs(W)), sUs_blocked.legs[0], ov[0], trunc_err

    def __str__(self):
        """Some status information about the MPS."""
        res = ["MPS, L={L:d}, bc={bc!r}.".format(L=self.L, bc=self.bc)]
        res.append("chi: " + str(self.chi))
        if self.L > 10:
            res.append("first two sites: " + repr(self.sites[0]) + " " + repr(self.sites[1]))
            res.append("first two forms:" + " ".join([repr(f) for f in self.form[:2]]))
        else:
            res.append("sites: " + " ".join([repr(s) for s in self.sites]))
            res.append("forms: " + " ".join([repr(f) for f in self.form]))
        return "\n".join(res)

    def compress(self, options):
        """Compresss an MPS.

        Options
        -------
        .. cfg:config :: MPS_compress
            :include: VariationalCompression

            compression_method : ``'SVD' | 'variational'``
                Mandatory.
                Selects the method to be used for compression.
                For the `SVD` compression, `trunc_params` is the only other option used.
            trunc_params : dict
                Truncation parameters as described in :cfg:config:`truncation`.
        """
        options = asConfig(options, "MPS_compress")
        method = options['compression_method']
        trunc_params = options.subconfig('trunc_params')
        if method == 'SVD':
            return self.compress_svd(trunc_params)
        elif method == 'variational':
            from ..algorithms.mps_common import VariationalCompression
            return VariationalCompression(self, options).run()
        raise ValueError("Unknown compression method: " + repr(method))

    def compress_svd(self, trunc_par):
        """Compress `self` with a single sweep of SVDs; in place.

        Perform a single right-sweep of QR/SVD without truncation, followed by a left-sweep with
        truncation, very much like :meth:`canonical_form_finite`.

        .. warning ::
            In case of a strong compression, this does not find the optimal, global solution.

        Parameters
        ----------
        trunc_par : dict
            Parameters for truncation, see :cfg:config:`truncation`.
        """
        trunc_err = TruncationError()
        if self.bc == 'finite':
            # Do QR starting from the left
            B = self.get_B(0, form='Th')
            for i in range(self.L - 1):
                B = B.combine_legs(['vL', 'p'])
                q, r = npc.qr(B, inner_labels=['vR', 'vL'])
                B = q.split_legs()
                self.set_B(i, B, form=None)
                B = self.get_B(i + 1, form='B')
                B = npc.tensordot(r, B, axes=('vR', 'vL'))
            # Do SVD from right to left & truncate
            for i in range(self.L - 1, 0, -1):
                B = B.combine_legs(['p', 'vR'])
                U, S, VH, err, norm_new = svd_theta(B, trunc_par)
                trunc_err += err
                self.norm *= norm_new
                VH = VH.split_legs()
                self.set_B(i, VH, form='B')
                B = self.get_B(i - 1, form=None)
                B = npc.tensordot(B, U, axes=('vR', 'vL'))
                B.iscale_axis(S, 'vR')
                self.set_SL(i, S)
            self.set_B(0, B, form='Th')
        elif self.bc == 'infinite':
            for i in range(self.L):
                theta = self.get_theta(i, n=2)
                theta = theta.combine_legs([['vL', 'p0'], ['p1', 'vR']], qconj=[+1, -1])
                self.set_svd_theta(i, theta, update_norm=False)
            for i in range(self.L - 1, -1, -1):
                theta = self.get_theta(i, n=2)
                theta = theta.combine_legs([['vL', 'p0'], ['p1', 'vR']], qconj=[+1, -1])
                trunc_err += self.set_svd_theta(i, theta, trunc_par, update_norm=False)
        else:
            raise NotImplementedError("unsupported boundary conditions " + repr(self.bc))
        return trunc_err

    def _to_valid_index(self, i):
        """Make sure `i` is a valid index (depending on `self.bc`)."""
        if not self.finite:
            return i % self.L
        if i < 0:
            i += self.L
        if i >= self.L or i < 0:
            raise KeyError("i = {0:d} out of bounds for finite MPS".format(i))
        return i

    def _parse_form(self, form):
        """Parse `form` = (list of) {tuple | key of _valid_forms} to list of tuples"""
        if isinstance(form, tuple):
            return [form] * self.L
        form = to_iterable(form)
        if len(form) == 1:
            form = [form[0]] * self.L
        if len(form) != self.L:
            raise ValueError("Wrong len of `form`: " + repr(form))
        return [self._to_valid_form(f) for f in form]

    def _to_valid_form(self, form):
        """Parse `form` = {tuple | key of _valid_forms} to a tuple"""
        if isinstance(form, tuple):
            return form
        return self._valid_forms[form]

    def _scale_axis_B(self, B, S, form_diff, axis_B, cutoff):
        """Scale an axis of B with S to bring it in desired form.

        If S is just 1D (as usual, e.g. during TEBD), this function just performs
        ``B.scale_axis(S**form_diff, axis_B)``.

        However, during the DMRG with mixer, S might acutally be a 2D matrix.
        For ``form_diff = -1``, we need to calculate the inverse of S, more precisely the
        (Moore-Penrose) pseudo inverse, see :func:`~tenpy.linalg.np_conserved.pinv`.
        The cutoff is only used in that case.

        Returns scaled B.
        """
        if form_diff == 0:
            return B  # nothing to do
        if not isinstance(S, npc.Array):
            # the usual case: S is a 1D array with singular values
            if form_diff == -1.:
                S = 1. / S
            elif form_diff != 1.:
                S = S**form_diff
            return B.scale_axis(S, axis_B)
        else:
            # e.g. during DMRG with a DensityMatrixMixer
            if S.rank != 2:
                raise ValueError("Expect 2D npc.Array or 1D numpy ndarray")
            if form_diff == -1:
                S = npc.pinv(S, cutoff)
            elif form_diff != 1.:
                raise ValueError("Can't scale/tensordot a 2D `S` for non-integer `form_diff`")

            # Hack: mpo.MPOEnvironment.full_contraction uses ``axis_B == 'vL*'``
            if axis_B == 'vL' or axis_B == 'vL*':
                B = npc.tensordot(S, B, axes=[1, axis_B]).replace_label(0, axis_B)
            elif axis_B == 'vR' or axis_B == 'vR*':
                B = npc.tensordot(B, S, axes=[axis_B, 0]).replace_label(-1, axis_B)
            else:
                raise ValueError("This should never happen: unexpected leg for scaling with S")
            return B

    def _replace_p_label(self, A, s):
        """Return npc Array `A` with replaced label, ``'p' -> 'p'+s``.

        This is done for each of the 'physical labels' in :attr:`_p_label`. With a clever use of
        this function, the re-implementation of various functions (like get_theta) in derived
        classes with multiple legs per site can be avoided.
        """
        return A.replace_label('p', 'p' + s)
        #  return A.replace_labels(self._p_label, self._get_p_label(s))

    def _get_p_label(self, s):
        """return  self._p_label with additional string `s`."""
        return ['p' + s]
        #  return [lbl + s for lbl in self._p_label]

    def _get_p_labels(self, ks, star=False):
        """Join ``self._get_p_label(str(k)) for k in range(ks)`` to a single list."""
        if star:
            return ['p' + str(k) + '*' for k in range(ks)]
            #  return [lbl + str(k) + '*' for k in range(ks) for lbl in self._p_label]
        else:
            return ['p' + str(k) for k in range(ks)]
            #  return [lbl + str(k) for k in range(ks) for lbl in self._p_label]

    def _expectation_value_args(self, ops, sites, axes):
        """parse the arguments of self.expectation_value()"""
        ops = npc.to_iterable_arrays(ops)
        if any(isinstance(op, str) for op in ops):
            n = 1
        else:
            s = 0 if sites is None else to_iterable(sites)[0]
            n = ops[s % len(ops)].rank // 2  # same as int(rank/2)
        L = self.L
        if sites is None:
            if self.finite:
                sites = range(L - (n - 1))
            else:
                sites = range(L)
        sites = to_iterable(sites)
        if axes is None:
            if n == 1:
                axes = (['p'], ['p*'])
            else:
                axes = (self._get_p_labels(n), self._get_p_labels(n, True))
        # check number of axes
        ax_p, ax_pstar = axes
        if len(ax_p) != n or len(ax_pstar) != n:
            raise ValueError("Len of axes does not match to n-site operator with n=" + str(n))
        return ops, sites, n, axes

    def _correlation_function_args(self, ops1, ops2, sites1, sites2, opstr):
        """get default arguments of self.correlation_function()"""
        if sites1 is None:
            sites1 = range(0, self.L)
        elif isinstance(sites1, int):
            sites1 = range(0, sites1)
        if sites2 is None:
            sites2 = range(0, self.L)
        elif isinstance(sites2, int):
            sites2 = range(0, sites2)
        ops1 = npc.to_iterable_arrays(ops1)
        ops2 = npc.to_iterable_arrays(ops2)
        opstr = npc.to_iterable_arrays(opstr)
        sites1 = np.sort(sites1)
        sites2 = np.sort(sites2)
        return ops1, ops2, sites1, sites2, opstr

    def _corr_up_diag(self, ops1, ops2, i, j_gtr, opstr, str_on_first, apply_opstr_first):
        """correlation function above the diagonal: for fixed i and all j in j_gtr, j > i."""
        op1 = self.get_op(ops1, i)
        opstr1 = self.get_op(opstr, i)
        if opstr1 is not None and str_on_first:
            axes = ['p*', 'p'] if apply_opstr_first else ['p', 'p*']
            op1 = npc.tensordot(op1, opstr1, axes=axes)
        theta = self.get_theta(i, n=1)
        C = npc.tensordot(op1, theta, axes=['p*', 'p0'])
        C = npc.tensordot(theta.conj(), C, axes=[['p0*', 'vL*'], ['p', 'vL']])
        # C has legs 'vR*', 'vR'
        js = list(j_gtr[::-1])  # stack of j, sorted *descending*
        res = []
        for r in range(i + 1, js[0] + 1):  # js[0] is the maximum
            B = self.get_B(r, form='B')
            C = npc.tensordot(C, B, axes=['vR', 'vL'])
            if r == js[-1]:
                Cij = npc.tensordot(self.get_op(ops2, r), C, axes=['p*', 'p'])
                Cij = npc.inner(B.conj(), Cij, axes=[['vL*', 'p*', 'vR*'], ['vR*', 'p', 'vR']])
                res.append(Cij)
                js.pop()
            if len(js) > 0:
                op = self.get_op(opstr, r)
                if op is not None:
                    C = npc.tensordot(op, C, axes=['p*', 'p'])
                C = npc.tensordot(B.conj(), C, axes=[['vL*', 'p*'], ['vR*', 'p']])
        return res

    def _canonical_form_dominant_gram_matrix(self, bond0, transpose, tol_xi, guess=None):
        """Find dominant eigenvector of the transfer matrix starting between sites (bond0-1,bond0).

        Find right (transpose=False) or left (transpose=True) eigenvector of the transfermatrix.
        """
        TM = TransferMatrix(self, self, bond0, bond0, transpose=transpose, charge_sector=0)
        if guess is None:
            diag = self.get_SL(bond0)**2 if transpose else 1.
            guess = TM.initial_guess(diag)
        guess = guess.combine_legs([0, 1], pipes=TM.pipe)
        eta, V = TM.eigenvectors(self._transfermatrix_keep, v0_npc=guess, which='LM')
        self._transfermatrix_keep = len(eta)
        if len(eta) > 1:
            if np.abs(eta[0]) > np.abs(eta[1]):
                xi = -self.L / np.log(np.abs((eta[1] / eta[0])))
            else:
                xi = np.inf
            if xi > tol_xi:
                raise ValueError("Degenerate spectrum of TransferMatrix "
                                 "(corr length xi={xi:.3e})".format(xi=xi))
        eta, G = eta[0], V[0]
        G = G.split_legs()
        # note: the dominant eigenvector should be hermitian and positive
        # removes phase (arbitrary for eigenvectors!) and normalize
        # right eigenvectors should have trace chi, left ones trace 1
        # (since we expect something close to eye(chi) for right and diag(S**2) for left G)
        norm = 1. if transpose else G.shape[0]
        G *= norm / npc.trace(G, 0, 1)
        if self.dtype.kind != 'c':  # psi is real -> G should be real
            eta = np.abs(eta)
            G.iunary_blockwise(np.real)
        return eta, G  # G has legs vL, vL* or vR, vR*

    def _canonical_form_correct_right(self, i1, Gr, eps=2. * np.finfo(np.double).eps):
        """Given the right gram matrix Gr, updated the bond (i0, i1), where i0 = i1 - 1.

        Diagonalise Gr = X^H Wr X and update
        ``B[i0] -> B[i0] X^H / norm``,
        ``B[i1] -> X B[i1] * norm``, where norm = sqrt(chi/sum(Wr)) == sqrt(chi/tr(Gr))
        If `Wr` has (almost) zero entries, reduce the bond dimension at the given bond.
        Then ``Gr -> Wr``.
        Return Wr normalized to ``sum(Wr) = chi``.
        """
        Gr.itranspose(['vL', 'vL*'])
        W, XH = npc.eigh(Gr)  # -> XH has legs vL vL* = vL vR
        if np.sign(W[np.argmax(np.abs(W))]) == -1:  # fix sign
            W = -W  # should actually never happen:  we initially normalize tr(Gr) = chi > 0
        # discard small values on order of machine precision
        proj = (W > eps)
        if np.count_nonzero(proj) < len(W):
            # project into non-degenerate subspace, reducing the bond dimensions!
            if np.count_nonzero(proj) < len(W) * 0.9:
                logger.warning("canonical_form_infinite: project to significantly smaller chi")
            XH.iproject(proj, axes=1)
            W = W[proj]
        norm = len(W) / np.sum(W)
        W *= norm
        norm = np.sqrt(norm)  # (norm doesn't change eigenvalue of TM)
        Kl = XH.iset_leg_labels(['vL', 'vR']) * (1. / norm)
        Kr = XH.transpose().iconj().iset_leg_labels(['vL', 'vR']) * norm
        i0 = i1 - 1
        self.set_B(i0, npc.tensordot(self.get_B(i0), Kl, axes=['vR', 'vL']))
        self.set_B(i1, npc.tensordot(Kr, self.get_B(i1), axes=['vR', 'vL']))
        return W, Kl, Kr

    def _canonical_form_correct_left(self, i1, Gl, Wr, eps=2. * np.finfo(np.double).eps):
        """Bring into canonical form on bond (i0, i1) where i0= i1 - 1.

        Given the left Gram matrix Gl (with legs 'vR*', 'vR')
        and right diag(Wr), compute and diagonalize the density matrix
        ``rho = sqrt(Wr) Gl sqrt(Wr) -> Y^H S^2 Y`` (Y acting on ket, Y^H on bra).
        Then we can update
        B[i0] -> B[i0] sqrt(Wr) Y^H
        B[i1] -> Y 1/sqrt(Wr) B[i1]
        Thus the new dominant left eigenvector is
        Gl -> Y sqrt(Wr) Gl sqrt(Wr) Y^H = S^2
        and dominant right eigenvector is
        diag(Wr) -> Y 1/sqrt(Wr) diag(Wr) 1/sqrt(W) Y^H = diag(1)
        i.e., we brought the bond to canonical form and `S` is the Schmidt spectrum.
        """
        sqrt_Wr = np.sqrt(Wr)
        Gl.itranspose(['vR*', 'vR'])
        rhor = Gl.scale_axis(sqrt_Wr, 0).iscale_axis(sqrt_Wr, 1)
        S2, YH = npc.eigh(rhor, sort='>')  # YH has legs 'vR*', 'vR'
        S2 /= np.sum(S2)  # equivalent to normalizing tr(rhor)=1
        s_norm = 1.
        # discard small values on order of machine precision
        proj = (S2 > eps)
        if np.count_nonzero(proj) < len(S2):
            # project into non-degenerate subspace, reducing the bond dimensions!
            if np.count_nonzero(proj) < len(S2) * 0.9:
                logger.warning("canonical_form_infinite: project to significantly smaller chi")
            YH.iproject(proj, axes=1)
            S2 = S2[proj]
            s_norm = np.sqrt(np.sum(S2))
        S = np.sqrt(S2) / s_norm
        self.set_SL(i1, S)
        Yl = YH.scale_axis(sqrt_Wr / s_norm, 0).iset_leg_labels(['vL', 'vR'])
        Yr = YH.transpose().iconj().scale_axis(1. / sqrt_Wr, 1).iset_leg_labels(['vL', 'vR'])
        i0 = i1 - 1
        self.set_B(i0, npc.tensordot(self.get_B(i0), Yl, axes=['vR', 'vL']))
        self.set_B(i1, npc.tensordot(Yr, self.get_B(i1), axes=['vR', 'vL']))
        Gl = npc.tensordot(Gl, Yl, axes=['vR', 'vL'])
        Gl = npc.tensordot(Yl.conj(), Gl, axes=['vL*', 'vR*'])  # labels 'vR*', 'vR'
        Gl /= npc.trace(Gl)
        # Gl is diag(S**2) up to numerical errors...
        return Gl, Yl, Yr

    def _gauge_compatible_vL_vR(self, other):
        """If necessary, gauge total charge of `other` to match the vL, vR legs of self."""
        if self.chinfo.qnumber == 0:
            return
        from tenpy.tools import optimization
        need_gauge = any(self.get_total_charge() != other.get_total_charge())
        if need_gauge:
            vL, vR = self._outer_virtual_legs()
            other.gauge_total_charge(None, vL, vR)

    def _outer_virtual_legs(self):
        U, V = self.segment_boundaries
        if U is not None:
            vL = U.get_leg('vL')
            vR = V.get_leg('vR')
        else:
            vL = self._B[0].get_leg('vL')
            vR = self._B[-1].get_leg('vR')
        return vL, vR


class MPSEnvironment:
    """Stores partial contractions of :math:`<bra|Op|ket>` for local operators `Op`.

    The network for a contraction :math:`<bra|Op|ket>` of a local operator `Op`, say exemplary
    at sites `i, i+1` looks like::

        |     .-----M[0]--- ... --M[1]---M[2]--- ... ->--.
        |     |     |             |      |               |
        |     |     |             |------|               |
        |     LP[0] |             |  Op  |               RP[-1]
        |     |     |             |------|               |
        |     |     |             |      |               |
        |     .-----N[0]*-- ... --N[1]*--N[2]*-- ... -<--.

    Of course, we can also calculate the overlap `<bra|ket>` by using the special case ``Op = Id``.

    We use the following label convention (where arrows indicate `qconj`)::

        |    .-->- vR           vL ->-.
        |    |                        |
        |    LP                       RP
        |    |                        |
        |    .--<- vR*         vL* -<-.

    To avoid recalculations of the whole network e.g. in the DMRG sweeps,
    we store the contractions up to some site index in this class.
    For ``bc='finite','segment'``, the very left and right part ``LP[0]`` and
    ``RP[L-1]`` are trivial and don't change,
    but for ``bc='infinite'`` they are might be updated
    (by inserting another unit cell to the left/right).

    The MPS `bra` and `ket` have to be in canonical form.
    All the environments are constructed without the singular values on the open bond.
    In other words, we contract left-canonical `A` to the left parts `LP`
    and right-canonical `B` to the right parts `RP`.
    Thus, the special case ``ket=bra`` should yield identity matrices for `LP` and `RP`.

    Parameters
    ----------
    bra : :class:`~tenpy.networks.mps.MPS`
        The MPS to project on. Should be given in usual 'ket' form;
        we call `conj()` on the matrices directly.
        Stored in place, without making copies.
        If necessary to match charges, we call :meth:`~tenpy.networks.mps.MPS.gauge_total_charge`.
    ket : :class:`~tenpy.networks.mpo.MPO` | None
        The MPS on which the local operator acts.
        Stored in place, without making copies.
        If ``None``, use `bra`.
    cache : :class:`~tenpy.tools.cache.DictCache` | None
        Cache in which the tensors should be saved. If ``None``, a new `DictCache` is generated.
    **init_env_data :
        Further keyword arguments with initializaiton data, as returned by
        :meth:`get_initialization_data`.
        See :meth:`initialize_first_LP_last_RP` for details on these parameters.

    Attributes
    ----------
    L : int
        Number of physical sites involved into the Environment, i.e. the least common multiple
        of ``bra.L`` and ``ket.L``.
    bra, ket : :class:`~tenpy.networks.mps.MPS`
        The two MPS for the contraction.
    dtype : type
        The data type.
    _finite : bool
        Whether the boundary conditions of the MPS are finite.
    cache : :class:`~tenpy.tools.cache.DictCache`
        Cache for saving the environment tensors.
    _LP_keys, _RP_keys : list of str
        Map indices to keys for the :attr:`cache`.
    _LP : list of {``None`` | }
        Left parts of the environment, len `L`.
        ``LP[i]`` contains the contraction strictly left of site `i`
        (or ``None``, if we don't have it calculated).
    _RP : list of {``None`` | :class:`~tenpy.linalg.np_conserved.Array`}
        Right parts of the environment, len `L`.
        ``RP[i]`` contains the contraction strictly right of site `i`
        (or ``None``, if we don't have it calculated).
    _LP_age : list of int | ``None``
        Used for book-keeping, how large the DMRG system grew:
        ``_LP_age[i]`` stores the number of physical sites invovled into the contraction
        network which yields ``self.get_LP(i)``.
    _RP_age : list of int | ``None``
        Used for book-keeping, how large the DMRG system grew:
        ``_RP_age[i]`` stores the number of physical sites invovled into the contraction
        network which yields ``self._RP[i]``.
    """
    def __init__(self, bra, ket, cache=None, **init_env_data):
        if ket is None:
            ket = bra
        if ket is not bra:
            ket._gauge_compatible_vL_vR(bra)  # ensure matching charges
        self.bra = bra
        self.ket = ket
        self.dtype = np.find_common_type([bra.dtype, ket.dtype], [])
        self.L = L = lcm(bra.L, ket.L)
        if hasattr(self, 'H'):
            self.L = L = lcm(self.H.L, L)
        self._finite = self.ket.finite  # just for _to_valid_index
        self._LP_keys = ['LP_{0:d}'.format(i) for i in range(L)]
        self._RP_keys = ['RP_{0:d}'.format(i) for i in range(L)]
        self._LP_age = [None] * L
        self._RP_age = [None] * L
        if cache is None:
            cache = DictCache.trivial()
        self.cache = cache
        if not self.cache.long_term_storage.trivial and L < 8:
            warnings.warn("non-trivial cache for short-length environment: "
                          "Much overhead for a little RAM saving. Necessary?")
        self.init_first_LP_last_RP(**init_env_data)
        self.test_sanity()

    def init_first_LP_last_RP(self,
                              init_LP=None,
                              init_RP=None,
                              age_LP=0,
                              age_RP=0,
                              start_env_sites=0):
        """(Re)initialize first LP and last RP from the given data.

        Parameters
        ----------
        init_LP : ``None`` | :class:`~tenpy.linalg.np_conserved.Array`
            Initial very left part ``LP``. If ``None``, build one with :meth`init_LP`.
        init_RP : ``None`` | :class:`~tenpy.linalg.np_conserved.Array`
            Initial very right part ``RP``. If ``None``, build one with :meth:`init_RP`.
        age_LP : int
            The number of physical sites involved into the contraction of `init_LP`.
        age_RP : int
            The number of physical sites involved into the contraction of `init_RP`.
        start_env_sites : int
            If `init_LP` and `init_RP` are not specified, contract each `start_env_sites` for them.
        """
        init_LP, init_RP = self._check_compatible_legs(init_LP, init_RP, start_env_sites)
        ket_U, ket_V = self.ket.segment_boundaries
        bra_U, bra_V = self.bra.segment_boundaries
        if init_LP is None:
            init_LP = self.init_LP(0, start_env_sites)
            age_LP = start_env_sites
        else:
            if ket_U is not None:
                init_LP = npc.tensordot(init_LP, ket_U, axes=['vR', 'vL'])
            if bra_U is not None:
                init_LP = npc.tensordot(bra_U.conj(), init_LP, axes=['vL*', 'vR*'])
        if init_RP is None:
            init_RP = self.init_RP(self.L - 1, start_env_sites)
            age_RP = start_env_sites
        else:
            if ket_V is not None:
                init_RP = npc.tensordot(ket_V, init_RP, axes=['vR', 'vL'])
            if bra_V is not None:
                init_RP = npc.tensordot(init_RP, bra_V.conj(), axes=['vL*', 'vR*'])
        self.set_LP(0, init_LP, age=age_LP)
        self.set_RP(self.L - 1, init_RP, age=age_RP)

    def test_sanity(self):
        """Sanity check, raises ValueErrors, if something is wrong."""
        assert (self.bra.finite == self.ket.finite == self._finite)
        assert any(key in self.cache for key in self._LP_keys)
        assert any(key in self.cache for key in self._RP_keys)

    def _check_compatible_legs(self, init_LP, init_RP, start_env_sites):
        if init_LP is not None or init_RP is not None:
            if start_env_sites == 0:
                vL_ket, vR_ket = self.ket._outer_virtual_legs()
                vL_bra, vR_bra = self.bra._outer_virtual_legs()
            else:
                vL_ket = self.ket.get_B(-start_env_sites, 'A').get_leg('vL')
                vL_bra = self.bra.get_B(-start_env_sites, 'A').get_leg('vL')
                vR_ket = self.ket.get_B(self.L - 1 + start_env_sites, 'B').get_leg('vR')
                vR_bra = self.bra.get_B(self.L - 1 + start_env_sites, 'B').get_leg('vR')
        if init_LP is not None:
            compatible = (init_LP.get_leg('vR') == vL_ket.conj()
                          and init_LP.get_leg('vR*') == vL_bra)
            if not compatible:
                warnings.warn("dropping `init_LP` with incompatible MPS legs")
                init_LP = None
        if init_RP is not None:
            compatible = (init_RP.get_leg('vL') == vR_ket.conj()
                          and init_RP.get_leg('vL*') == vR_bra)
            if not compatible:
                warnings.warn("dropping `init_RP` with incompatible MPS legs")
                init_RP = None
        return init_LP, init_RP

    def init_LP(self, i, start_env_sites=0):
        """Build initial left part ``LP``.

        If `bra` and `ket` are the same and in left canonical form, this is the environment
        you get contracting he overlaps from the left infinity up to bond left of site `i`.

        For segment MPS, the :attr:`~tenpy.networks.mps.MPS.segment_boundaries` are read out
        (if set).

        Parameters
        ----------
        i : int
            Build ``LP`` left of site `i`.
        start_env_sites : int
            How many sites to contract to converge the `init_LP`; the initial `age_LP`.

        Returns
        -------
        init_LP : :class:`~tenpy.linalg.np_conserved.Array`
            Identity contractible with the `vL` leg of ``ket.get_B(i)``, labels ``'vR*', 'vR'``.
        """
        if self.ket.bc == "segment" and self.bra is not self.ket:
            U_bra, V_bra = self.bra.segment_boundaries
            U_ket, V_ket = self.ket.segment_boundaries
            if U_bra is not None or U_ket is not None:
                if U_bra is not None and U_ket is not None:
                    init_LP = npc.tensordot(U_bra.conj(), U_ket, axes=['vL*', 'vL'])
                elif U_bra is not None:
                    init_LP = U_bra.conj().ireplace_label('vL*', 'vR')
                else:
                    init_LP = U_ket.replace_label('vL', 'vR*')
                return init_LP
        leg_ket = self.ket.get_B(i - start_env_sites, None).get_leg('vL')
        leg_bra = self.bra.get_B(i - start_env_sites, None).get_leg('vL')
        leg_ket.test_equal(leg_bra)
        init_LP = npc.diag(1., leg_ket, dtype=self.dtype, labels=['vR*', 'vR'])
        for j in range(i - start_env_sites, i):
            init_LP = self._contract_LP(j, init_LP)
        return init_LP

    def init_RP(self, i, start_env_sites=0):
        """Build initial right part ``RP`` for an MPS/MPOEnvironment.

        If `bra` and `ket` are the same and in right canonical form, this is the environment
        you get contracting from the right infinity up to bond right of site `i`.

        For segment MPS, the :attr:`~tenpy.networks.mps.MPS.segment_boundaries` are read out
        (if set).

        Parameters
        ----------
        i : int
            Build ``RP`` right of site `i`.
        start_env_sites : int
            How many sites to contract to converge the `init_RP`; the initial `age_RP`.

        Returns
        -------
        init_RP : :class:`~tenpy.linalg.np_conserved.Array`
            Identity contractible with the `vR` leg of ``ket.get_B(i)``, labels ``'vL*', 'vL'``.
        """
        if self.ket.bc == "segment" and self.bra is not self.ket:
            U_bra, V_bra = self.bra.segment_boundaries
            U_ket, V_ket = self.ket.segment_boundaries
            if V_bra is not None or V_ket is not None:
                if V_bra is not None and V_ket is not None:
                    init_RP = npc.tensordot(V_bra.conj(), V_ket, axes=['vR*', 'vR'])
                elif V_bra is not None:
                    init_RP = V_bra.conj().ireplace_label('vR*', 'vL')
                else:
                    init_RP = V_ket.replace_label('vR', 'vL*')
                return init_RP
        leg_ket = self.ket.get_B(i + start_env_sites, None).get_leg('vR')
        leg_bra = self.bra.get_B(i + start_env_sites, None).get_leg('vR')
        leg_ket.test_equal(leg_bra)
        init_RP = npc.diag(1., leg_ket, dtype=self.dtype, labels=['vL*', 'vL'])
        for j in range(i + start_env_sites, i, -1):
            init_RP = self._contract_RP(j, init_RP)
        return init_RP

    def get_LP(self, i, store=True):
        """Calculate LP at given site from nearest available one.

        The returned ``LP_i`` corresponds to the following contraction,
        where the M's and the N's are in the 'A' form::

            |     .-------M[0]--- ... --M[i-1]--->-   'vR'
            |     |       |             |
            |     LP[0]   |             |
            |     |       |             |
            |     .-------N[0]*-- ... --N[i-1]*--<-   'vR*'

        Parameters
        ----------
        i : int
            The returned `LP` will contain the contraction *strictly* left of site `i`.
        store : bool
            Wheter to store the calculated `LP` in `self` (``True``) or discard them (``False``).

        Returns
        -------
        LP_i : :class:`~tenpy.linalg.np_conserved.Array`
            Contraction of everything left of site `i`,
            with labels ``'vR*', 'vR'`` for `bra`, `ket`.
        """
        # find nearest available LP to the left.
        for i0 in range(i, i - self.L, -1):
            key = self._LP_keys[self._to_valid_index(i0)]
            LP = self.cache.get(key, None)
            if LP is not None:
                break
            # (for finite, LP[0] should always be set, so we should abort at latest with i0=0)
        else:  # no break called
            raise ValueError("No left part in the system???")
        age = self.get_LP_age(i0)
        for j in range(i0, i):
            LP = self._contract_LP(j, LP)
            age = age + 1
            if store:
                self.set_LP(j + 1, LP, age=age)
        return LP

    def get_RP(self, i, store=True):
        """Calculate RP at given site from nearest available one.

        The returned ``RP_i`` corresponds to the following contraction,
        where the M's and the N's are in the 'B' form::

            |     'vL'  ->---M[i+1]-- ... --M[L-1]----.
            |                |              |         |
            |                |              |         RP[L-1]
            |                |              |         |
            |     'vL*' -<---N[i+1]*- ... --N[L-1]*---.


        Parameters
        ----------
        i : int
            The returned `RP` will contain the contraction *strictly* right of site `i`.
        store : bool
            Wheter to store the calculated `RP` in `self` (``True``) or discard them (``False``).

        Returns
        -------
        RP_i : :class:`~tenpy.linalg.np_conserved.Array`
            Contraction of everything left of site `i`,
            with labels ``'vL', 'vL*'`` for `ket`, `bra`.
        """
        # find nearest available RP to the right.
        for i0 in range(i, i + self.L):
            key = self._RP_keys[self._to_valid_index(i0)]
            RP = self.cache.get(key, None)
            if RP is not None:
                break
            # (for finite, RP[-1] should always be set, so we should abort at latest with i0=L-1)
        else:  # no break called
            raise ValueError("No right part in the system???")
        age = self.get_RP_age(i0)
        for j in range(i0, i, -1):
            RP = self._contract_RP(j, RP)
            age = age + 1
            if store:
                self.set_RP(j - 1, RP, age=age)
        return RP

    def get_LP_age(self, i):
        """Return number of physical sites in the contractions of get_LP(i).

        Might be ``None``.
        """
        return self._LP_age[self._to_valid_index(i)]

    def get_RP_age(self, i):
        """Return number of physical sites in the contractions of get_RP(i).

        Might be ``None``.
        """
        return self._RP_age[self._to_valid_index(i)]

    def set_LP(self, i, LP, age):
        """Store part to the left of site `i`."""
        i = self._to_valid_index(i)
        self.cache[self._LP_keys[i]] = LP
        self._LP_age[i] = age

    def set_RP(self, i, RP, age):
        """Store part to the right of site `i`."""
        i = self._to_valid_index(i)
        self.cache[self._RP_keys[i]] = RP
        self._RP_age[i] = age

    def del_LP(self, i):
        """Delete stored part strictly to the left of site `i`."""
        i = self._to_valid_index(i)
        del self.cache[self._LP_keys[i]]
        self._LP_age[i] = None

    def del_RP(self, i):
        """Delete stored part scrictly to the right of site `i`."""
        i = self._to_valid_index(i)
        del self.cache[self._RP_keys[i]]
        self._RP_age[i] = None

    def clear(self):
        """Delete all partial contractions except the left-most `LP` and right-most `RP`."""
        for key in self._LP_keys[1:] + self._RP_keys[:-1]:
            if key in self.cache:
                del self.cache[key]
        self._LP_age[1:] = [None] * (self.L - 1)
        self._RP_age[:-1] = [None] * (self.L - 1)

    def has_LP(self, i):
        """Return True if `LP` left of site `i` is stored."""
        return self._LP_keys[self._to_valid_index(i)] in self.cache

    def has_RP(self, i):
        """Return True if `RP` right of site `i` is stored."""
        return self._RP_keys[self._to_valid_index(i)] in self.cache

    def cache_optimize(self, short_term_LP=[], short_term_RP=[], preload_LP=None, preload_RP=None):
        """Update `short_term_keys` for the cache and possibly preload tensors.

        Parameters
        ----------
        short_term_LP, short_term_RP : list of int
            `i` indices for :meth:`get_LP` and :meth:`get_RP`, respectively, for which a repeated
            look-up could happen, i.e., for which tensors should be kept in RAM until the next
            call to this function.
        preload_LP, preload_RP : int | None
            If not None, preload the tensors for the corrsponding :meth:`get_LP` and :meth:`get_RP`
            call, respectively, from disk.
        """
        LP_keys = self._LP_keys
        RP_keys = self._RP_keys
        preload = []
        if preload_LP is not None:
            preload.append(LP_keys[self._to_valid_index(preload_LP)])
        if preload_RP is not None:
            preload.append(RP_keys[self._to_valid_index(preload_RP)])
        self.cache.set_short_term_keys(*(LP_keys[self._to_valid_index(i)] for i in short_term_LP),
                                       *(RP_keys[self._to_valid_index(i)] for i in short_term_RP),
                                       *preload)
        self.cache.preload(*preload)

    def get_initialization_data(self, first=0, last=None):
        """Return data for (re-)initialization of the environment.

        Parameters
        ----------
        first, last : int
            The first and last site, to the left and right of which we should return the
            environments.  Defaults to 0 and :attr:`L` - 1.

        Returns
        -------
        init_env_data : dict
            A dictionary with the following entries.

            init_LP, init_RP : :class:`~tenpy.linalg.np_conserved.Array`
                `LP` on the left of site `first` and `RP` on the right of site `last`, which can be
                used as `init_LP` and `init_RP` for the initialization of a new environment.
            age_LP, age_RP : int
                The number of physical sites involved into the contraction yielding `init_LP` and
                `init_RP`, respectively.
        """
        if last is None:
            last = self.L - 1
        data = {'init_LP': self.get_LP(first, True), 'init_RP': self.get_RP(last, True)}
        data['age_LP'] = self.get_LP_age(first)
        data['age_RP'] = self.get_RP_age(last)
        return data

    def full_contraction(self, i0):
        """Calculate the overlap by a full contraction of the network.

        The full contraction of the environments gives the overlap ``<bra|ket>``,
        taking into account :attr:`MPS.norm` of both `bra` and `ket`.
        For this purpose, this function contracts
        ``get_LP(i0+1, store=False)`` and ``get_RP(i0, store=False)`` with appropriate singular
        values in between.

        Parameters
        ----------
        i0 : int
            Site index.
        """
        if self.ket.finite and i0 + 1 == self.L:
            # special case to handle `_to_valid_index` correctly:
            # get_LP(L) is not valid for finite b.c, so we use need to calculate it explicitly.
            LP = self.get_LP(i0, store=False)
            LP = self._contract_LP(i0, LP)
        else:
            LP = self.get_LP(i0 + 1, store=False)
        # multiply with `S` on bra and ket side
        S_bra = self.bra.get_SR(i0).conj()
        if isinstance(S_bra, npc.Array):
            LP = npc.tensordot(S_bra, LP, axes=['vL*', 'vR*'])
        else:
            LP = LP.scale_axis(S_bra, 'vR*')
        S_ket = self.ket.get_SR(i0)
        if isinstance(S_ket, npc.Array):
            LP = npc.tensordot(LP, S_ket, axes=['vR', 'vL'])
        else:
            LP = LP.scale_axis(S_ket, 'vR')
        RP = self.get_RP(i0, store=False)
        contr = npc.inner(LP, RP, axes=[['vR*', 'vR'], ['vL*', 'vL']], do_conj=False)
        return contr * self.bra.norm * self.ket.norm

    def expectation_value(self, ops, sites=None, axes=None):
        """Expectation value ``<bra|ops|ket>`` of (n-site) operator(s).

        Calculates n-site expectation values of operators sandwiched between bra and ket.
        For examples the contraction for a two-site operator on site `i` would look like::

            |          .--S--B[i]--B[i+1]--.
            |          |     |     |       |
            |          |     |-----|       |
            |          LP[i] | op  |       RP[i+1]
            |          |     |-----|       |
            |          |     |     |       |
            |          .--S--B*[i]-B*[i+1]-.

        Here, the `B` are taken from `ket`, the `B*` from `bra`.
        The call structure is the same as for :meth:`MPS.expectation_value`.

        .. warning ::

            In contrast to :meth:`MPS.expectation_value`, this funciton does not normalize,
            thus it also takes into account :attr:`MPS.norm` of both `bra` and `ket`.

        Parameters
        ----------
        ops : (list of) { :class:`~tenpy.linalg.np_conserved.Array` | str }
            The operators, for wich the expectation value should be taken,
            All operators should all have the same number of legs (namely `2 n`).
            If less than ``len(sites)`` operators are given, we repeat them periodically.
            Strings (like ``'Id', 'Sz'``) are translated into single-site operators defined by
            :attr:`sites`.
        sites : list
            List of site indices. Expectation values are evaluated there.
            If ``None`` (default), the entire chain is taken (clipping for finite b.c.)
        axes : None | (list of str, list of str)
            Two lists of each `n` leg labels giving the physical legs of the operator used for
            contraction. The first `n` legs are contracted with conjugated `B`,
            the second `n` legs with the non-conjugated `B`.
            ``None`` defaults to ``(['p'], ['p*'])`` for single site (n=1), or
            ``(['p0', 'p1', ... 'p{n-1}'], ['p0*', 'p1*', .... 'p{n-1}*'])`` for `n` > 1.

        Returns
        -------
        exp_vals : 1D ndarray
            Expectation values, ``exp_vals[i] = <bra|ops[i]|ket>``, where ``ops[i]`` acts on
            site(s) ``j, j+1, ..., j+{n-1}`` with ``j=sites[i]``.
        """
        ops, sites, n, (op_ax_p, op_ax_pstar) = self.ket._expectation_value_args(ops, sites, axes)
        ax_p = ['p' + str(k) for k in range(n)]
        ax_pstar = ['p' + str(k) + '*' for k in range(n)]
        E = []
        for i in sites:
            LP = self.get_LP(i, store=True)
            RP = self.get_RP(i + n - 1, store=True)
            op = self.ket.get_op(ops, i)
            op = op.replace_labels(op_ax_p + op_ax_pstar, ax_p + ax_pstar)
            C = self.ket.get_theta(i, n)
            C = npc.tensordot(op, C, axes=[ax_pstar, ax_p])  # same labels
            C = npc.tensordot(LP, C, axes=['vR', 'vL'])  # axes_p + (vR*, vR)
            C = npc.tensordot(C, RP, axes=['vR', 'vL'])  # axes_p + (vR*, vL*)
            C.ireplace_labels(['vR*', 'vL*'], ['vL', 'vR'])  # back to original theta labels
            theta_bra = self.bra.get_theta(i, n)
            E.append(npc.inner(theta_bra, C, axes='labels', do_conj=True))
        return np.real_if_close(np.array(E)) * self.bra.norm * self.ket.norm

    def _contract_LP(self, i, LP):
        """Contract LP with the tensors on site `i` to form ``self.get_LP(i+1)``"""
        LP = npc.tensordot(LP, self.ket.get_B(i, form='A'), axes=('vR', 'vL'))
        axes = (self.ket._get_p_label('*') + ['vL*'], self.ket._p_label + ['vR*'])
        # for a ususal MPS, axes = (['p*', 'vL*'], ['p', 'vR*'])
        LP = npc.tensordot(self.bra.get_B(i, form='A').conj(), LP, axes=axes)
        return LP  # labels 'vR*', 'vR'

    def _contract_RP(self, i, RP):
        """Contract RP with the tensors on site `i` to form ``self.get_RP(i-1)``"""
        RP = npc.tensordot(self.ket.get_B(i, form='B'), RP, axes=('vR', 'vL'))
        axes = (self.ket._p_label + ['vL*'], self.ket._get_p_label('*') + ['vR*'])
        # for a ususal MPS, axes = (['p', 'vL*'], ['p*', 'vR*'])
        RP = npc.tensordot(RP, self.bra.get_B(i, form='B').conj(), axes=axes)
        return RP  # labels 'vL', 'vL*'

    def _to_valid_index(self, i):
        """Make sure `i` is a valid index (depending on `finite`)."""
        if not self._finite:
            return i % self.L
        if i < 0:
            i += self.L
        if i >= self.L or i < 0:
            raise KeyError("i = {0:d} out of bounds for MPSEnvironment".format(i))
        return i


class TransferMatrix(sparse.NpcLinearOperator):
    r"""Transfer matrix of two MPS (bra & ket).

    For an iMPS in the thermodynamic limit, we often need to find the 'dominant `RP`' (and `LP`).
    This mean nothing else than to take the transfer matrix of the unit cell and find the
    (right/left) eigenvector with the largest (magnitude) eigenvalue, since it will dominate
    :math:`(TM)^n RP` (or :math:`LP (TM)^n`) in the limit :math:`n \rightarrow \infty` - whatever
    the initial `RP` is. This class provides exactly that functionality with :meth:`eigenvectors`.

    Given two MPS, we define the transfer matrix as::

        |    ---M[i]---M[i+1]- ... --M[i+L]---
        |       |      |             |
        |    ---N[j]*--N[j+1]* ... --N[j+L]*--

    Here the `M` denotes the matrices of the bra and `N` the ones of the ket, respectively.
    To view it as a `matrix`, we combine the left and right indices to pipes::

        |  (vL.vL*) ->-TM->- (vR.vR*)   acting on  (vL.vL*) ->-RP

    Note that we keep all M and N as copies.

    .. deprecated :: 0.6.0
        The default for `shift_ket` was the value of `shift_bra`, this will be changed to 0.

    Parameters
    ----------
    bra : MPS
        The MPS which is to be (complex) conjugated.
    ket : MPS
        The MPS which is not (complex) conjugated.
    shift_bra : int
        We start the `N` of the bra at site `shift_bra` (i.e. the `j` in the above network).
    shift_ket : int | None
        We start the `M` of the ket at site `shift_ket` (i.e. the `i` in the above network).
        ``None`` is deprecated, default will be changed to 0 in the future.
    transpose : bool
        Wheter `self.matvec` acts on `RP` (``False``) or `LP` (``True``).
    charge_sector : None | charges | ``0``
        Selects the charge sector of the vector onto which the Linear operator acts.
        ``None`` stands for *all* sectors, ``0`` stands for the zero-charge sector.
        Defaults to ``0``, i.e., **assumes** the dominant eigenvector is in charge sector 0.
    form : ``'B' | 'A' | 'C' | 'G' | 'Th' | None`` | tuple(float, float)
        In which canonical form we take the `M` and `N` matrices.


    Attributes
    ----------
    L : int
        Number of physical sites involved in the transfer matrix, i.e. the least common multiple
        of `bra.L` and `ket.L`.
    shift_bra : int
        We start the `N` of the bra at site `shift_bra`.
    shift_ket : int | None
        We start the `M` of the ket at site `shift_ket`. ``None`` defaults to `shift_bra`.
    transpose : bool
        Wheter `self.matvec` acts on `RP` (``True``) or `LP` (``False``).
    qtotal : charges
        Total charge of the transfer matrix (which is gauged away in matvec).
    form : tuple(float, float) | None
        In which canonical form (all of) the `M` and `N` matrices are.
    flat_linop : :class:`~tenpy.linalg.sparse.FlatLinearOperator`
        Class lifting :meth:`matvec` to ndarrays in order to use :func:`~tenpy.tools.math.speigs`.
    pipe : :class:`~tenpy.linalg.charges.LegPipe`
        Pipe corresponding to ``'(vL.vL*)'`` for ``transpose=False``
        or to ``'(vR.vR*)'`` for ``transpose=True``.
    label_split :
        ``['vL', 'vL*']`` if ``tranpose=False`` or ``['vR', 'vR*']`` if ``transpose=True``.
    _bra_N : list of npc.Array
        Complex conjugated matrices of the bra, transposed for fast `matvec`.
    _ket_M : list of npc.Array
        The matrices of the ket, transposed for fast `matvec`.
    """
    def __init__(self,
                 bra,
                 ket,
                 shift_bra=0,
                 shift_ket=None,
                 transpose=False,
                 charge_sector=0,
                 form='B'):
        L = self.L = lcm(bra.L, ket.L)
        if shift_ket is None:
            if shift_bra != 0:
                warnings.warn("default for shift_ket will change to 0. Specify both explicitly!",
                              FutureWarning, 2)
            shift_ket = shift_bra
        self.shift_bra = shift_bra
        self.shift_ket = shift_ket
        self.transpose = transpose
        if ket.chinfo != bra.chinfo:
            raise ValueError("incompatible charges")
        form = ket._to_valid_form(form)
        self._p_label = p = ket._p_label  # for ususal MPS just ['p']
        assert p == bra._p_label
        self._pstar_label = pstar = ket._get_p_label('*')  # ['p*']
        if not transpose:  # right to left
            label = '(vL.vL*)'  # what we act on
            label_split = ['vL', 'vL*']
            M = self._ket_M = [
                ket.get_B(i, form=form).itranspose(['vL'] + p + ['vR'])
                for i in reversed(range(shift_ket, shift_ket + L))
            ]
            N = self._bra_N = [
                bra.get_B(i, form=form).conj().itranspose(pstar + ['vR*', 'vL*'])
                for i in reversed(range(shift_bra, shift_bra + L))
            ]
            pipe = npc.LegPipe([M[0].get_leg('vR'), N[0].get_leg('vR*')], qconj=-1).conj()
        else:  # left to right
            label = '(vR*.vR)'  # mathematically more natural
            label_split = ['vR*', 'vR']
            M = self._ket_M = [
                ket.get_B(i, form=form).itranspose(['vL'] + p + ['vR'])
                for i in range(shift_ket, shift_ket + L)
            ]
            N = self._bra_N = [
                bra.get_B(i, form=form).conj().itranspose(['vR*', 'vL*'] + pstar)
                for i in range(shift_bra, shift_bra + L)
            ]
            pipe = npc.LegPipe([N[0].get_leg('vL*'), M[0].get_leg('vL')], qconj=+1).conj()
        dtype = np.promote_types(bra.dtype, ket.dtype)
        self.pipe = pipe
        self.label_split = label_split
        self.flat_linop = sparse.FlatLinearOperator(self.matvec, pipe, dtype, charge_sector, label)
        self.qtotal = bra.chinfo.make_valid(np.sum([B.qtotal for B in M + N], axis=0))
        if not ket.finite and np.any(self.qtotal != 0):
            # for non-zero U(1) qtotal, we can immediately say that `self` is nilpotent.
            # In contrast, nonzero Z_N qtotal does not imply that, since the transfer-matrix
            # doesn't have to be hermitian: it could be circulant, with arbitrary eigenvalues!
            # The eigenvectors will *not* conserve the charge in this case!
            enlarge_factors = []
            for i in np.nonzero(self.qtotal)[0]:
                if ket.chinfo.mod[i] == 1:  # U(1) qtotal
                    raise ValueError("TransferMatrix is nil-potent due to charges")
                enlarge_factors.append(ket.chinfo.mod[i])  # get N of Z_N charge

            raise ValueError("TransferMatrix has non-zero qtotal for Z_N charges. "
                             "It can have valid eigenvectors, but they will break the Z_N charge. "
                             "To avoid that, you can enlarge the unit cell of the MPS "
                             "by a factor of " + str(lcm(enlarge_factors)))

    def matvec(self, vec):
        """Given `vec` as an npc.Array, apply the transfer matrix.

        Parameters
        ----------
        vec : :class:`~tenpy.linalg.np_conserved.Array`
            Vector to act on with the transfermatrix.
            If not `transposed`, `vec` is the right part `RP` of an environment,
            with legs ``'(vL.vL*)'`` in a pipe or splitted.
            If `transposed`, the left part `LP` of an environment with legs ``'(vR*.vR)'``.

        Returns
        -------
        mat_vec : :class:`~tenpy.linalg.np_conserved.Array`
            The tranfer matrix acted on `vec`, in the same form as given.
        """
        pipe = None
        if self.label_split[0] not in vec._labels:
            vec = vec.split_legs(0)
            pipe = self.pipe
        # vec.itranspose(self.label_split)  # ['vL', 'vL*'] or ['vR*', 'vR']
        qtotal = vec.qtotal
        legs = vec.legs
        # the actual work
        if not self.transpose:  # right to left
            contract = [self._p_label + ['vL*'], self._pstar_label + ['vR*']]
            for N, M in zip(self._bra_N, self._ket_M):
                vec = npc.tensordot(M, vec, axes=['vR', 'vL'])
                vec = npc.tensordot(vec, N, axes=contract)  # [['p', 'vL*'], ['p*', 'vR*']]
        else:  # left to right
            contract = [['vL*'] + self._pstar_label, ['vR*'] + self._p_label]
            for N, M in zip(self._bra_N, self._ket_M):
                vec = npc.tensordot(vec, M, axes=['vR', 'vL'])
                vec = npc.tensordot(N, vec, axes=contract)  # [['vL*', 'p*'], ['vR*', 'p']])
        if pipe is not None:
            vec = vec.combine_legs(self.label_split, pipes=pipe)
        return vec

    def initial_guess(self, diag=1.):
        """Return a diagonal matrix as initial guess for the eigenvector.

        Parameters
        ----------
        diag : float | 1D ndarray
            Should be ``1.`` for the identity or some singular values squared.

        Returns
        -------
        mat : :class:`~tenpy.linalg.np_conserved.Array`
            A 2D array with `diag` on the diagonal such that :meth:`matvec` can act on it.
        """
        return npc.diag(diag, self.pipe.legs[0], labels=self.label_split)

    def eigenvectors(self, *args, **kwargs):
        """Find (dominant) eigenvector(s) of self using :mod:`scipy.sparse`.

        For arguments see :meth:`~tenpy.linalg.sparse.FlatLinearOperator.eigenvectors`.

        If no :attr:`charge_sector` was selected, we look in *all* charge sectors.
        The returned eigenvectors have combined legs ``'(vL.vL*)'`` or ``(vR*.vR)``.
        """
        return self.flat_linop.eigenvectors(*args, **kwargs)


class InitialStateBuilder:
    """Class to simplify providing common sets of intial states.

    This class is used by the :class:`~tenpy.simulations.simulation.Simulation` to initialize MPS.
    The idea is that you can fully specify the initial state by a few parameters and possibly
    a custom function (by inheriting from this class), as demonstrated in the examples below.

    Parameters
    ----------
    lattice : :class:`~tenpy.models.lattice.Lattice`
        The lattice class defining the geometry and sites.
    options : dict
        Parameter dictionary which specifies the initial state to be generated.
    model_dtype : :class:`numpy.dtype`
        Data type of the model hamiltonian, default for the dtype of the resulting `psi`.

    Options
    -------
    .. cfg:config :: InitialStateBuilder

        method : str
            Selects the category of the initial state, and in particular the
            name of the method called by :meth:`build` to generate the state, for example you can
            set ``method='lat_product_state'`` to use :meth:`lat_product_state`.
            The available other parameters depend on the chosen method.

    Examples
    --------
    If you use the :class:`~tenpy.simulations.simulation.Simulation` setup, it will extract
    the `lattice` and `model_dtype` from the :attr:`~tenpy.models.model.Model.lat` and MPO dtype.
    Let's assume that we have a lattice `lat` with :class:`~tenpy.networks.site.SpinHalfSite`
    on a square lattices. Then you could initialize a Neel state like this:

    .. testsetup :: InitialStateBuilder

        from tenpy.networks.mps import InitialStateBuilder
        import tenpy
        spin_half = tenpy.networks.site.SpinHalfSite(conserve=None)
        lat = tenpy.models.lattice.Square(4, 4, spin_half)

    .. doctest :: InitialStateBuilder

        >>> options = {'method': 'lat_product_state',
        ...            'product_state' : [[['up'], ['down']],
        ...                               [['down'], ['up']]],
        ...            }
        >>> psi = InitialStateBuilder(lat, options).run()

    Note that the `method` options is mandatory, it selects which other method :meth:`run` calls.
    This allows to define custum initial states with the same interface by defining your own
    subclass, e.g. to get a short-hand for the Neel state:

    .. doctest :: InitialStateBuilder

        >>> class MyInitialStateBuilderForSquare(InitialStateBuilder):
        ...     def neel(self):
        ...         # my_option = self.options.get('opt_name', default) # not necessary here
        ...         product_state = [[['up'], ['down']], [['down'], ['up']]]
        ...         return self.lat_product_state(product_state)
        >>> options = {'method': 'neel'}
        >>> psi = MyInitialStateBuilderForSquare(lat, options).run()

    If you define such a custom class, you can activate it in simulations by explicitly setting
    the :cfg:option:`Simulation.initial_state_builder_class` to the name of it.
    """

    #: logger : An instance of a logger; see :doc:`/intro/logging`. NB: class attribute.
    logger = logging.getLogger(__name__ + '.InitialStateBuilder')

    def __init__(self, lattice, options, model_dtype=np.float64):
        self.lattice = lattice
        self.options = asConfig(options, self.__class__.__name__)
        self.model_dtype = model_dtype

    def run(self):
        """Build an initial state from a specified method.

        Reads out the option :cfg:option:`InitialStateBuilder.method` to chooce a method of the
        class which generates the state.

        Returns
        -------
        psi : :class:`MPS`
            The generated MPS.
        """
        method_name = self.options['method']
        method = getattr(self, method_name, None)
        if method is None:
            raise ValueError(f"initial state 'method'={method_name!r} not recognized in " +
                             self.__class__.__name__)
        self.logger.info("calling %s.%s()", self.__class__.__name__, method_name)
        psi = method()
        self.check_total_charge(psi)
        return psi

    def check_total_charge(self, psi):
        """Assert that the given state has the expected charge.

        Parameters
        ----------
        psi : :class:`MPS`
            The final, generated state.

        .. cfg:configoptions :: InitialStateBuilder

            check_total_charge : tuple of int
                Check that the :meth:`MPS.get_total_charge` returns these values.
        """
        check_charge = self.options.get('check_global_charge', None)
        if check_charge is None:
            return
        check_charge = tuple(check_charge)
        has_charge = tuple(psi.get_total_charge(psi.bc == 'finite'))
        assert check_charge == has_charge

    def from_file(self):
        """Load the initial state from an exisiting file.

        Options
        -------
        .. cfg:configoptions :: InitialStateBuilder

            filename : str
                The filename from which to load the state
            data_key : str
                Key within the file to be used for loading the data.
                Can be recursive (separted by '/'), see :func:`tenpy.tools.misc.get_recursive`.
        """
        filename = self.options['filename']
        data_key = self.options.get('data_key', "psi")
        self.logger.info("loading initial state from %r", filename)
        if filename.endswith('.h5') or filename.endswith('.hdf5'):
            with hdf5_io.h5py.File(filename, 'r') as f:
                psi = hdf5_io.load_from_hdf5(f, data_key)
        else:
            data = hdf5_io.load(filename)
            psi = get_recursive(data, data_key)
        psi.test_sanity()
        return psi

    def lat_product_state(self, p_state=None):
        """Initialize from a lattice product state.

        See :meth:`MPS.from_lat_product_state` for details.

        Options
        -------
        .. cfg:configoptions :: InitialStateBuilder

            product_state : array of str
                The `p_state` passed on to :meth:`MPS.from_lat_product_state`.
        """
        if p_state is None:
            p_state = self.options['product_state']
        self.check_filling(p_state)
        dtype = self.options.get('dtype', self.model_dtype)
        psi = MPS.from_lat_product_state(self.lattice, p_state, dtype=dtype)
        return psi

    def mps_product_state(self, p_state=None):
        """Initialize from a product state.

        See :meth:`MPS.from_product_state` for details.

        Options
        -------
        .. cfg:configoptions :: InitialStateBuilder

            product_state : list of str
                The `p_state` passed on to :meth:`MPS.from_product_state`.
        """
        if p_state is None:
            p_state = self.options['product_state']
        self.check_filling(p_state)
        dtype = self.options.get('dtype', self.model_dtype)
        lat = self.lattice
        psi = MPS.from_product_state(lat.mps_sites(), p_state, bc=lat.bc_MPS, dtype=dtype)
        return psi

    def check_filling(self, p_state):
        """Ensure that the filling of the product state matches `check_filling` parameter.

        Options
        -------
        .. cfg:configoptions :: InitialStateBuilder

            check_filling : None | (int, int) or float
                The desired filling as float ``p/q``, or by a tuple ``(p, q)``.
                ``None`` (default) disables the check.
            full_empty : (str, str)
                Definition of which local states are "full" and "emtpy".
                For example you can use ``full_empty=('up', 'down')`` if you have spin sites.
        """
        check_filling = self.options.get("check_filling", None)
        if check_filling is None:
            return
        full, empty = self.options.get("full_empty", ('full', 'empty'))
        p_state = np.asarray(p_state, dtype=object)
        N_filled = np.sum(p_state == full)
        N_total = p_state.size
        try:
            p, q = check_filling
            check_filling = p / q
        except:
            p, q = int(round(check_filling * N_total)), N_total
        if abs(p - check_filling * N_total) > 1.e-13:
            raise ValueError(
                "check_filling={0:.5f} doesn't fit as integer in p_state.size = {1:d}".format(
                    check_filling, N_total))
        if N_filled * q != N_total * p:  # int-version of N_filled/N_total != p/q
            raise ValueError("unexpected filling {0:.5f} != check_filling = {1:.5f}".format(
                N_filled / N_total, check_filling))
        # done

    def fill_where(self):
        """Allow to specify a condition where sites should be filled.

        .. warning ::
            The `fill_where` string is parsed with the python function `eval`;
            this is unsafe if the variable comes from an untrusted source (e.g. the internet).

        Options
        -------
        .. cfg:configoptions :: InitialStateBuilder

            fill_where : str
                A string specifying the condition where to fill sites.
                See examples in :meth:`fill_where`.
            full_empty : (str, str)
                Definition of which local states are "full" and "emtpy".
                For example you can use ``full_empty=('up', 'down')`` if you have spin sites.

        Examples
        --------
        Possible variables for conditions are specified in :meth:`fill_where__get_variables`,
        for example `x_ind, y_ind, u_ind`.

        You can do arbitrary logical expressions. Example conditions:
        - ``"x_ind == 0"``
        - ``"AND(x_ind == 0, y_ind == 0)"``
        - ``"AND(x_ind == 0, IN(y_ind, [0, 2])"``
        - ``"WITHIN(x_ind, 0.25*Lx, 0.75*Lx )"`` # 0.25*Lx <= x <= 0.75&Lx
        """
        variables = self.fill_where__get_variables()
        shape = variables['x_ind'].shape
        condition = self.options["fill_where"]
        full, empty = self.options.get('full_empty', ('full', 'empty'))
        try:
            fill_array = eval(condition, variables)
        except:
            print("Error in InitialStateBuilder.fill_where condition")
            print(">>> condition:")
            print(condition)
            print(">>> available variables:")
            print(sorted(variables.keys()))
            raise  # re-throw the error, we just print usefull debugging info
        p_state = np.where(fill_array, to_array([full], shape=shape, dtype=object),
                           to_array([empty], shape=shape, dtype=object))
        return self.lat_product_state(p_state)

    def fill_where__get_variables(self):
        """Define the variables which can be used in the condition of :meth:`fill_where`.

        Here, the following variables get defined.

        x_ind, y_ind, u_ind : int
            Lattice indices of the
        eps : float
            Cutoff ``1.e-12`` that can get used when it is necessary to compare floats
        np :
            Numpy module, allowing to use any function from numpy.
        AND, OR, XOR, NOT, ANY, ALL, CLOSE, EQUAL, IN, WITHIN
            Function aliases to allow logical combinations.
            Use for example as ``AND(x_ind == 0, IN(y_ind, [0, 2, 5]))``.
        """
        lattice = self.lattice
        if lattice.dim == 1:
            Lx, Lu = lattice.shape
            x, u = np.mgrid[0:Lx, 0:Lu]
            variables = {'x_ind': x, 'u_ind': u, 'Lx': Lx, 'L': Lx, 'Lu': Lu}
        elif lattice.dim == 2:
            Lx, Ly, Lu = lattice.shape
            x, y, u = np.mgrid[0:Lx, 0:Ly, 0:Lu]
            variables = {'x_ind': x, 'y_ind': y, 'u_ind': u, 'Lx': Lx, 'Ly': Ly, 'Lu': Lu}
        else:
            raise NotImplementedError("3D lattice not handled...")

        def any_(*cond):
            return np.any(cond, axis=0)

        def all_(*cond):
            return np.all(cond, axis=0)

        def close(var, value, *, eps=1.e-12):
            return np.abs(var - value) < eps

        def oneof(var, values, *, eps=1.e-12):
            if eps:
                return np.any([close(var, val, eps=eps) for val in values], axis=0)
            return np.any([var == val for val in values], axis=0)

        def within(var, lower, upper, *, eps=1.e-12):
            if eps:
                lower = lower - eps
                upper = upper + eps
            return np.logical_and(np.less_equal(lower, var), np.less_equal(var, upper))

        variables.update({
            'np': np,  # numpy
            'AND': np.logical_and,
            'OR': np.logical_or,
            'XOR': np.logical_xor,
            'NOT': np.logical_not,
            'ANY': any_,
            'ALL': all_,
            'CLOSE': close,
            'EQUAL': np.equal,
            'IN': oneof,
            'WITHIN': within,
            'eps': 1.e-12,
        })
        return variables

    def randomized(self):
        """Initialize a state with another method and then apply a RandomUnitaryEvolution.

        Options
        -------
        .. cfg:configoptions :: InitialStateBuilder

            randomized_from_method : str
                Selects another method to initialize the starting state to be randomized,
                e.g., ``'lat_product_state'`` for :meth:`lat_product_state`.
            randomize_params : dict-like
                Parameters given to the :class:`~tenpy.algorithms.tebd.RandomUnitaryEvolution`.
                In particular, you might want to set the `N_steps` and `trunc_params['chi_max']`.
                The default is {'N_steps': 10, 'trunc_params': {'chi_max': 100}}``.
            randomize_canonicalize : bool
                Whether to call :meth:`MPS.canonical_form` before returning the state.
        """
        method_name = self.options['randomized_from_method']
        method = getattr(self, method_name)
        psi = method()
        close_1 = self.options.get('randomize_close_1', False)
        canonicalize = self.options.get('randomize_canonicalize', not close_1)
        params = {'N_steps': 10, 'trunc_params': {'chi_max': 100}}
        params = self.options.subconfig('randomize_params', {})
        psi.perturb(params, close_1=close_1, canonicalize=canonicalize)
        return psi


def build_initial_state(size, states, filling, mode='random', seed=None):
    """Build an "initial state" list.

    Uses two iterables ('states' and 'filling') to determine how to fill the
    state. The two lists should have the same length as every element in 'filling' gives the filling
    fraction for the corresponding state in 'states'.

    Example:
        size = 6, states = [0, 1, 2], filling = [1./3, 2./3, 0.]
        n_states = size * filling = [2, 4, 0]
        ==> Two sites will get state 0, 4 sites will get state 1, 0 sites will
        get state 2.

    .. todo ::
        Make more general: it should be possible to specify states as strings.

    Parameters
    ----------
    size : int
        length of state
    states : iterable
        Containing the possible local states
    filling : iterable
        Fraction of the total number of sites to get a certain state. If
        infinite fractions (e.g. 1/3) are needed, one should supply a
        fraction (1./3.)
    mode : str | None
        State filling pattern. Only 'random' is implemented
    seed : int | None
        Seed for random number generators

    Returns
    -------
    initial_state (list) : the initial state

    Raises
    ------
    ValueError
        If fractonal fillings are incommensurate with system size.
    AssertionError
        If the total filling is not equal to 1, or the length of `filling`
        does not equal the length of `states`.
    """

    random.seed(seed)

    # Do some safety checks
    assert sum(filling) == 1
    assert len(states) == len(filling)

    # Get number of sites for each local state
    n_states = np.array(filling) * size
    for num in n_states:
        if ((num - round(num)) < 1e-12):
            num = int(round(num))
        if type(num) != int and not num.is_integer():
            raise ValueError("Cannot create model of length {} with filling {}".format(
                size, filling))

    # Randomly assign local states
    initial_state = [0] * size
    all_sites = list(range(size))  # To avoid having two types on same site.
    for state, fill in zip(states, filling):
        sites = random.sample(set(all_sites),
                              int(fill * size))  # pick fill*size sites to put state
        for site in sites:
            initial_state[site] = state
            all_sites.remove(site)

    return initial_state
