# 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.
"""
Observable to perform the final projective measurements on the system
"""
import logging
from collections.abc import Iterator
from typing import TYPE_CHECKING, Any, 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__ = ["Projective"]
logger = logging.getLogger(__name__)
[docs]
class Projective(_TNObsBase):
"""
Observable to enable the final projective measurements after the evolution.
This observable is meant to give single-shot measurements: the system is
**projectively** measured a number of times equal to `num_shots`, such that
the user can observe a statistic of the distribution of the state.
The result of the observable will be a dictionary where:
- the keys are the measured state on a given basis
- the values are the number of occurrences of the keys in the `num_shots`
single shots measurements
As an example, if we work with qubits, we measure on the computational base
and we end up with the state :math:`\\frac{1}{\\sqrt{2}}(|00\\rangle+|11\\rangle)`,
requesting 1000 `num_shots` we will end up with the following result:
:code:`{'00' : 505, '11' : 495}`. Take into account that the measurement is probabilistic
and such is will only be an approximation of the true probability distribution,
that in the example case would be :math:`p_{00}=p_{11}=0.5`.
Parameters
----------
num_shots : int
Number of projective measurements
qiskit_convention : bool, optional
If you should use the qiskit convention when measuring, i.e. least significant qubit
on the right. Default to False.
"""
_measurable_ansaetze = (MPS, TTN, TTO, ATTN)
def __init__(self, num_shots: int, qiskit_convention: bool = False):
self.num_shots = num_shots
self.qiskit_convention = [qiskit_convention]
self._measures: dict[str, int] = {}
_TNObsBase.__init__(self, "projective_measurements")
def __iadd__(self, other: Any) -> Self:
"""
Documentation see :func:`_TNObsBase.__iadd__`.
"""
if isinstance(other, Projective):
self.num_shots += other.num_shots
self.qiskit_convention += other.qiskit_convention
if len(set(self.qiskit_convention)) > 1:
logger.warning(
"Merging Projective observables with different qiskit_convention values."
"Only the first will be used."
)
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(0)
obj.qiskit_convention = []
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", 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
for name, qk_conv in zip(self.name, self.qiskit_convention):
self.results_buffer[name] = state.meas_projective(
nmeas=self.num_shots, qiskit_convention=qk_conv
)
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 add_trajectories(self, all_results: dict, new: dict[str, Any]) -> dict:
"""
Documentation see :func:`_TNObsBase.add_trajectories`.
"""
for name in self.name:
if name not in all_results:
all_results[name] = new[name]
else:
if len(new[name]) > 0:
for key in new[name]:
if key not in all_results[name]:
all_results[name][key] = new[name][key]
else:
all_results[name][key] += new[name][key]
return all_results
[docs]
def avg_trajectories(self, all_results: dict, num_trajectories: int) -> dict:
"""
Documentation see :func:`_TNObsBase.avg_trajectories`.
"""
# the average is not performed for projective measurements
return all_results
[docs]
def read(self, fh: h5py.File, **kwargs: Any) -> Iterator[tuple[str, dict]]:
"""
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)
name = self.name[0]
if not is_measured or name not in fg:
yield name, {}
return
data = fg[name]
keys = data.attrs["keys"]
yield name, {key: data[ii] for ii, key in enumerate(keys)}
[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))
fg.attrs["is_measured"] = is_measured
if is_measured and len(self.results_buffer.values()) > 0:
# note: this observable has only one name "projective_measurements",
# with no guarantee for the result buffer to have it
name = self.name[0]
result_dict = self.results_buffer[name]
fg.create_dataset(name, data=result_dict.values())
fg.attrs["keys"] = result_dict.keys()