Source code for povm_toolbox.quantum_info.multi_qubit_frame

# (C) Copyright IBM 2024.
#
# 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.

"""MultiQubitFrame."""

from __future__ import annotations

import sys

if sys.version_info < (3, 11):
    from typing_extensions import Self
else:
    from typing import Self  # pragma: no cover

if sys.version_info < (3, 12):
    from typing_extensions import override
else:
    from typing import override  # pragma: no cover

import numpy as np
from qiskit.exceptions import QiskitError
from qiskit.quantum_info import Operator, SparsePauliOp

from povm_toolbox.utilities import matrix_to_double_ket

from .base import BaseFrame


[docs] class MultiQubitFrame(BaseFrame[int]): """Class that collects all information that any frame of multiple qubits should specify. This is a representation of an operator-valued vector space frame. The effects are specified as a list of :class:`~qiskit.quantum_info.Operator`. .. note:: This is a base class which collects functionality common to various subclasses. As an end-user you would not use this class directly. Check out :mod:`povm_toolbox.quantum_info` for more general information. """ def __init__(self, list_operators: list[Operator]) -> None: """Initialize from explicit operators. Args: list_operators: list that contains the explicit frame operators. The length of the list is the number of operators of the frame. Raises: ValueError: if the frame operators do not have a correct shape. They should all be hermitian and of the same dimension. """ self._num_operators: int self._dimension: int self._operators: list[Operator] self._pauli_operators: list[dict[str, complex]] | None self._array: np.ndarray self._informationally_complete: bool self.operators = list_operators def __repr__(self) -> str: """Return the string representation of a :class:`.MultiQubitFrame` instance.""" f_subsystems = f"(num_qubits={self.num_subsystems})" if self.num_subsystems > 1 else "" return f"{self.__class__.__name__}{f_subsystems}<{self.num_operators}> at {hex(id(self))}" @property def informationally_complete(self) -> bool: """If the frame spans the entire Hilbert space.""" return self._informationally_complete @property def dimension(self) -> int: """The dimension of the Hilbert space on which the effects act.""" return self._dimension @property def num_operators(self) -> int: """The number of effects of the frame.""" return self._num_operators @property def operators(self) -> list[Operator]: """Return the list of frame operators.""" return self._operators @operators.setter def operators(self, new_operators: list[Operator]): """Set the frame operators.""" self._num_operators = len(new_operators) self._dimension = new_operators[0].dim[0] for frame_op in new_operators: if not (self._dimension == frame_op.dim[0] and self._dimension == frame_op.dim[1]): raise ValueError( f"Frame operators need to be square ({frame_op.dim[0]},{frame_op.dim[1]}) and " "all of the same dimension." ) self._operators = new_operators self._pauli_operators = None self._array = np.ndarray((self.dimension**2, self.num_operators), dtype=complex) for k, frame_op in enumerate(new_operators): self._array[:, k] = matrix_to_double_ket(frame_op.data) self._informationally_complete = bool( np.linalg.matrix_rank(self._array) == self.dimension**2 ) self._check_validity() @property def pauli_operators(self) -> list[dict[str, complex]]: """Convert the internal frame operators to Pauli form. .. warning:: The conversion to Pauli form can be computationally intensive. Returns: The frame operators in Pauli form. Each frame operator is returned as a dictionary mapping Pauli labels to coefficients. Raises: QiskitError: when the frame operators could not be converted to Pauli form (e.g. when they are not N-qubit operators). """ if self._pauli_operators is None: try: self._pauli_operators = [ dict(SparsePauliOp.from_operator(op).label_iter()) for op in self.operators ] except QiskitError as exc: raise QiskitError( f"Failed to convert frame operators to Pauli form: {exc.message}" ) from exc return self._pauli_operators def _check_validity(self) -> None: """Check if frame axioms are fulfilled. Raises: ValueError: if any one of the frame operators is not hermitian. """ for k, op in enumerate(self.operators): if not np.allclose(op, op.adjoint(), atol=1e-5): raise ValueError(f"The {k}-the frame operator is not hermitian.") def __getitem__(self, index: slice) -> Operator | list[Operator]: """Return a frame operator or a list of frame operators. Args: index: indicate the operator(s) to be returned. Returns: The operator or list of operators corresponding to the index. """ return self.operators[index] def __len__(self) -> int: """Return the number of operators of the frame.""" return self.num_operators def __array__(self) -> np.ndarray: """Return the array representation of the frame. The array has a shape :math:`(``self.dimension``**2, ``self.num_operators``)`. """ return self._array
[docs] @override def analysis( self, hermitian_op: SparsePauliOp | Operator, frame_op_idx: int | set[int] | None = None, ) -> float | dict[int, float] | np.ndarray: if isinstance(hermitian_op, SparsePauliOp): hermitian_op = hermitian_op.to_operator() op_vectorized = np.conj(matrix_to_double_ket(hermitian_op.data)) if isinstance(frame_op_idx, int): return float(np.dot(op_vectorized, self._array[:, frame_op_idx]).real) if isinstance(frame_op_idx, set): return { idx: float(np.dot(op_vectorized, self._array[:, idx]).real) for idx in frame_op_idx } if frame_op_idx is None: return np.array(np.dot(op_vectorized, self._array).real) raise TypeError( "The optional `frame_op_idx` can either be a single or set of integers, not a " f"{type(frame_op_idx)}." )
[docs] @classmethod def from_vectors(cls, frame_vectors: np.ndarray) -> Self: r"""Initialize a frame from non-normalized bloch vectors. The non-normalized Bloch vectors are given by :math:`|\tilde{\psi}_k \rangle = \sqrt{\gamma_k} |\psi_k \rangle`. The resulting frame operators are :math:`F_k = \gamma_k |\psi_k \rangle \langle \psi_k |` where :math:`\gamma_k` is the trace of the :math:`k`'th frame operator. Args: frame_vectors: list of vectors :math:`|\tilde{\psi_k} \rangle`. The length of the list corresponds to the number of operators of the frame. Each vector is of shape :math:`(\mathrm{dim},)` where :math:`\mathrm{dim}` is the :attr:`.dimension` of the Hilbert space on which the frame acts. Returns: The frame corresponding to the vectors. """ return cls([Operator(np.outer(vec, vec.conj())) for vec in frame_vectors])