# 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