Source code for qtealeaves.observables.bond_entropy

# This code is part of tn_py_fronted.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
Observable to measure the bond entropy of the system
"""
import logging
from typing import TYPE_CHECKING, Any, Iterator, Self

import h5py

from qtealeaves.emulator import ATTN, MPS, TTN, TTO
from qtealeaves.tooling import QTeaLeavesError

from .tnobase import _TNObsBase

if TYPE_CHECKING:
    from qtealeaves.abstracttns.abstract_tn import _AbstractTN
else:
    _AbstractTN = Any

__all__ = ["BondEntropy"]
logger = logging.getLogger(__name__)


[docs] class BondEntropy(_TNObsBase): """ Observable to enable the measurement of the Von Neumann bond entropy at the end of a circuit. If the state is pure, than this measurement is equal to the entanglement. Otherwise it is not well defined. Given a quantum state of :math:`n` sites :math:`|\\psi\\rangle` the Von Neumann entropy of the bipartition :math:`A(B)` with :math:`n_{A(B)}` sites is defined as: .. math:: S_V(\\rho_A) = -\\mathrm{Tr} \\rho_A\\ln(\\rho_A), \\quad \\rho_A=\\mathrm{Tr}_B\\left( |\\psi\\rangle\\langle\\psi|\\right) This value should be computed using the natural logarithm, i.e. the logarithm in base :math:`e`, but you can also pass a different base using the `base` parameter. The output of the measurement will be a dictionary, where: - As keys, we report on continous range of site which form the first bipartition. For example, the continuous range (4, 5) has the bipartitions [4, 5] vs [0, 1, 2, 3, 6, 7]. An MPS will define the first bipartitions in the different cuts as (0, 0), (0, 1), (0, 2), (0, 3), etc. - Indices are python indices starting at zero in the keys. - As value the result of :math:`S_V`. Natural log is used for both MPS and TTN. The expression above for the computation of the bond entropy is quite complex. Using tensor network we can strongly simplify it, using the singular values :math:`s_i` "stored" in the link. This is equivalent to look at the eigenvalues :math:`\\lambda_i` of the reduced density matrix :math:`\\rho_A` for a pure system like MPS and TTN: .. math:: S_V(\\rho_A) = -\\sum_{i} \\lambda_i \\ln(\\lambda_i) = -\\sum_{i} s_i^2 \\ln(s_i^2) """ _measurable_ansaetze = (MPS, TTN, TTO, ATTN) def __init__( self, base: int | None = None, mode: str = "N", kwargs: dict | None = None ): _TNObsBase.__init__(self, "bond_entropy") self.base = [base] mode = (mode or "").upper() allowed = {"N", "L", "R", "H"} if not mode or any(ch not in allowed for ch in mode): raise QTeaLeavesError( "Mode must only contain letters from 'N', 'L', 'R', 'H'." ) self.mode = [mode] self.kwargs = [kwargs] def __iadd__(self, other: Any) -> Self: """ Documentation see :func:`_TNObsBase.__iadd__`. If you add another bond entropy observable, the observable that performs measurements more often is kept. """ if isinstance(other, BondEntropy): # this is useless as name is always the same # but keep it for future changes, maybe allowing for # measuring bond_entropy with different bases names = set(self.name + other.name) # use set because np.unique doesn't handle None basis = set(self.base + other.base) mode = set(self.mode + other.mode) is_one_name = len(names) == 1 is_one_base = len(basis) == 1 is_one_mode = len(mode) == 1 if is_one_base and is_one_name and is_one_mode: self.base = list(basis) self.name = list(names) self.mode = list(mode) # Keep kwargs from other if self has none. # I cannot repeat the same logic as above with sets # because kwargs is a dict, therefore not hashable. self.kwargs = other.kwargs if len(self.kwargs) == 0 else self.kwargs else: logger.warning( "Measurement of more than one BondEntropy is not allowed, " "skipping inplace addition.\nWarning triggered because:%s%s%s", ( "" if is_one_name else "\n\tTried to add BondEntropy with different names" ), ( "" if is_one_base else "\n\tTried to add BondEntropy with different bases" ), ( "" if is_one_mode else "\n\tTried to add BondEntropy with different modes" ), ) else: raise QTeaLeavesError( f"__iadd__ not defined for types {type(self)} and {type(other)}." ) return self
[docs] @classmethod def empty(cls) -> Self: """ Documentation see :func:`_TNObsBase.empty`. """ obj = cls() obj.name = [] obj.base = [] obj.mode = [] obj.kwargs = [] return obj
[docs] def measure( self, state: _AbstractTN, operators: Any = None, **kwargs: Any, ) -> dict[str, Any]: """ Documentation see :func:`_TNObsBase.measure`. """ if len(self.name) == 0: return self.results_buffer if not self.check_measurable(state.__class__): logger.warning( "Observable %s not measurable for %s ansatz.", self.name, str(state) ) return self.results_buffer # Why only the effective projectors, but not the effective operators. While # isometrizing in meas_tensor_product, we still propagate them through? Why? tmp_eff_proj = state.eff_proj state.eff_proj = [] ini_iso_pos = state.iso_center # Get the bond entropies assert len(self.name) == 1, "Only one bond entropy measurement supported." bond_entropy = state.meas_bond_entropy( # type: ignore[attr-defined] base=self.base[0], mode=self.mode[0], **(self.kwargs[0] if self.kwargs[0] is not None else {}), ) self.results_buffer[self.name[0]] = bond_entropy # Restore the effective projectors if ini_iso_pos is not None: state.iso_towards(ini_iso_pos) state.eff_proj = tmp_eff_proj return self.results_buffer
[docs] def read(self, fh: h5py.File, **kwargs: Any) -> Iterator[tuple[str, Any]]: """ Read the measurements of the projective measurement observable from HDF5 file. Parameters ---------- fh : h5py.File Read the information about the measurements from this HDF5 file. """ # check that group exists if str(self) not in fh: raise QTeaLeavesError("Observable group not found in file.") fg = fh[str(self)] is_measured = fg.attrs.get("is_measured", False) if is_measured: num_bond_entropy = len(fg.keys()) for ii in range(num_bond_entropy): res_key = self.name[0] + str(ii) data = fg[res_key] keys = data.attrs["keys"] self.results_buffer[res_key] = { tuple(key): data[ii] for ii, key in enumerate(keys) } yield res_key, self.results_buffer[res_key]
[docs] def write_results( self, fh: h5py.File, state_ansatz: type[_AbstractTN], **kwargs: Any ) -> None: """ See :func:`_TNObsBase.write_results`. """ is_measured = self.check_measurable(state_ansatz) fg = fh.create_group(str(self), track_order=True) fg.attrs["is_measured"] = is_measured assert len(self.name) < 2, "Only one bond entropy measurement supported." if is_measured: num_bond_entropies = 0 for value in self.results_buffer.values(): if value is None: continue keys, values = [], [] for subkey, subvalue in value.items(): if subvalue is None: continue keys.append(subkey) values.append(subvalue) if len(keys) > 0: fgd = fg.create_dataset( self.name[0] + f"{num_bond_entropies}", data=values ) fgd.attrs["keys"] = keys num_bond_entropies += 1