# This code is part of qtealeaves.
#
# 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.
"""
Local observable
"""
import logging
from typing import TYPE_CHECKING, Any, Iterator, Self, cast
import numpy as np
from qtealeaves.emulator import ATTN, LPTN, MPS, TTN, TTO, StateVector
from qtealeaves.mpos import ITPO, DenseMPO, DenseMPOList, MPOSite
from qtealeaves.tooling import QTeaLeavesError
from .tnobase import _TNObsBase
if TYPE_CHECKING:
from qtealeaves.abstracttns.abstract_tn import _AbstractTN
from qtealeaves.mpos.disentangler import DELayer
from qtealeaves.operators import TNOperators
from qtealeaves.tensors import TensorBackend
else:
_AbstractTN = Any
DELayer = Any
TNOperators = Any
TensorBackend = Any
__all__ = ["Local"]
logger = logging.getLogger(__name__)
[docs]
class Local(_TNObsBase):
"""
The local observable will measure an operator defined on a single site
across the complete system. This means that, if the single site has
a dimension :math:`d` we expect the local observable to be a matrix
of dimension :math:`d\\times d`. For example, if we are working with
qubits, i.e. :math:`d=2`, a local observable can be the Pauli matrix
:math:`\\sigma_z`.
The local observable will be measured on *each* site of the tensor
network. As such, the output of this measurement will be a dictionary
where:
- the key is the `name` of the observable
- the value is a list of length :math:`n`, with :math:`n` the number
of sites of the tensor network. The :math:`i`-th element of the
list will contain the expectation value of the local observable
on the :math:`i`-th site.
**Arguments**
name : str
Define a label under which we can find the observable in the
result dictionary.
operator : str
Identifier/string for the operator to be measured.
"""
# mypy triggered because of StateVector
_measurable_ansaetze = (MPS, TTN, TTO, ATTN, LPTN, StateVector) # type: ignore[assignment]
def __init__(self, name: str, operator: str):
super().__init__(name)
self.operator = [operator]
[docs]
@classmethod
def empty(cls) -> Self:
"""
Documentation see :func:`_TNObsBase.empty`.
"""
obj = cls("x", "x")
obj.name = []
obj.operator = []
return obj
def __len__(self) -> int:
"""
Provide appropriate length method.
"""
return len(self.name)
def __iadd__(self, other: Any) -> Self:
"""
Documentation see :func:`_TNObsBase.__iadd__`.
"""
if isinstance(other, Local):
self.name += other.name
self.operator += other.operator
else:
raise QTeaLeavesError(
f"__iadd__ not defined for types {type(self)} and {type(other)}."
)
return self
[docs]
def measure(
self, state: _AbstractTN, operators: TNOperators, **kwargs: Any
) -> dict[str, Any]:
"""
Run the measurement of the local observable for a given state and save it
in the results buffer.
Args:
state : instance of class from :py:mod:`qtealeaves.emulator`
The state to measure the observable on.
"""
if len(self.name) == 0:
return self.results_buffer
if not self.check_measurable(state.__class__):
logger.warning(
"Observable %s not measurable for TN type %s",
self.name,
str(state),
)
return self.results_buffer
params = kwargs.get("params", None)
# Copy the effective projectors and remove them from the state.
# They are reinstated after the measurements.
# 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
if state.has_de:
# help the type checker
state = cast(ATTN, state)
if params is None:
raise QTeaLeavesError(
"Parameters are needed for disentangling layers, but not provided."
)
# if aTTN, then local entries on sites with disentanglers are not
# local anymore
state.iso_towards(state.default_iso_pos, keep_singvals=True, trunc=True)
hmpo = state.eff_op
# get the iTPO of these non-local terms and contract with DE layer
itpo = self.to_itpo(
operators,
state.tensor_backend,
state.num_sites,
de_layer=state.de_layer,
)
itpo = state.de_layer.contract_de_layer(itpo, state.tensor_backend, params)
# measure iTPO
itpo.set_meas_status(do_measurement=True)
state.eff_op = None # type: ignore[assignment]
itpo.setup_as_eff_ops(state, measurement_mode=True)
dict_by_tpo_id = itpo.collect_measurements()
idx = -1
# restore back the original hamiltonian as effective operator
state.eff_op = hmpo
for jj, name_jj in enumerate(self.name):
local_meas = state.meas_local(
[operators[(kk, self.operator[jj])] for kk in range(state.num_sites)]
)
# if aTTN, rewrite the values for sites with disentanglers
if state.has_de:
# help the type checker
state = cast(ATTN, state)
for kk in range(state.num_sites):
if kk in state.de_layer.de_sites:
# disentangler on a site
idx += 1
local_meas[kk] = np.real(dict_by_tpo_id[idx])
self.results_buffer[name_jj] = local_meas
# 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 collect_operators(self) -> Iterator[tuple[str, None]]:
"""
Documentation see :func:`_TNObsBase.collect_operators`.
"""
for elem in self.operator:
yield (elem, None)
# pylint: disable-next=too-many-locals
[docs]
def to_itpo(
self,
operators: TNOperators,
tensor_backend: TensorBackend,
num_sites: int,
de_layer: DELayer,
) -> ITPO:
"""
For measurements of aTTN when local entries on sites with disentanglers
are not local anymore, returns an ITPO with these non-local terms for
measurement. The resulting terms are stored in order of looping
over observable names and number of sites.
Parameters
----------
operators: TNOperators
The operator class
tensor_backend: instance of `TensorBackend`
Tensor backend of the simulation
num_sites: int
Number of sites of the state
de_layer : DELayer
Disentangler layer for which the iTPO layer is created
Returns
-------
ITPO: The `ITPO` class
iTPO with the non-local terms for measurement.
"""
dense_mpo_list = DenseMPOList()
for kk, _ in enumerate(self.name):
key_loc = self.operator[kk]
key_id = None
# get local operator and corresponding identity
for oset_name in operators.set_names:
op = operators[(oset_name, key_loc)]
# the matching identity (is it not always in operators?)
op_identity = op.eye_like(op.links[1])
op_identity.attach_dummy_link(0, is_outgoing=False)
op_identity.attach_dummy_link(3, is_outgoing=True)
key_id = str(id(op_identity)) if key_id is None else key_id
operators[(oset_name, key_id)] = op_identity
# fill the itpo with correct operators on correct sites
for ii in range(num_sites):
if ii in de_layer.de_sites:
# appropriate identity operator needs
# to be added on the other site of the disentangler
where = np.where(de_layer.de_sites == ii)
# we assume max 1 disentangler per site
ind = [where[0][0], where[1][0]]
position_identity = de_layer.de_sites[ind[0], abs(ind[1] - 1)]
site_a = MPOSite(
ii, key_loc, 1.0, 1.0, operators=operators, params={}
)
site_b = MPOSite(
position_identity,
key_id,
1.0,
1.0,
operators=operators,
params={},
)
# Checked manually, must be a linter-problem with double-inheritance
# pylint: disable-next=abstract-class-instantiated
dense_mpo = DenseMPO(
[site_a, site_b], tensor_backend=tensor_backend
)
dense_mpo_list.append(dense_mpo)
# Sites within dense mpos are not ordered and we have to make links match anyways
dense_mpo_list = dense_mpo_list.sort_sites()
itpo = ITPO(num_sites)
itpo.add_dense_mpo_list(dense_mpo_list)
itpo.set_meas_status(do_measurement=True)
return itpo