Source code for povm_toolbox.quantum_info.single_qubit_povm

# (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.

"""SingleQubitPOVM."""

from __future__ import annotations

import matplotlib as mpl
import numpy as np
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from qiskit.visualization.bloch import Bloch
from qiskit.visualization.utils import matplotlib_close_if_inline

from .multi_qubit_povm import MultiQubitPOVM


[docs] class SingleQubitPOVM(MultiQubitPOVM): """A convenience class to represent a single-qubit :class:`.MultiQubitPOVM` instance. Below is a simple example showing how you define a symmetric and informationally-complete POVM (SIC-POVM): >>> import cmath >>> import numpy as np >>> from povm_toolbox.quantum_info import SingleQubitPOVM >>> vecs = np.sqrt(1.0 / 2.0) * np.array( ... [ ... [1, 0], ... [np.sqrt(1.0 / 3.0), np.sqrt(2.0 / 3.0)], ... [np.sqrt(1.0 / 3.0), np.sqrt(2.0 / 3.0) * cmath.exp(2.0j * np.pi / 3)], ... [np.sqrt(1.0 / 3.0), np.sqrt(2.0 / 3.0) * cmath.exp(4.0j * np.pi / 3)], ... ] ... ) >>> sic_povm = SingleQubitPOVM.from_vectors(vecs) >>> print(sic_povm) SingleQubitPOVM<4> at 0x... """ def _check_validity(self) -> None: """Check if POVM axioms are fulfilled. In addition to the checks performed by the super-class, the following errors may be raised. Raises: ValueError: if the dimension does not equal 2 (i.e. the POVM acts on more than 1 qubit). """ if not self.dimension == 2: raise ValueError( "Dimension of Single Qubit POVM operator space should be 2," f" not {self.dimension}." ) super()._check_validity()
[docs] def get_bloch_vectors(self) -> np.ndarray: r"""Compute the Bloch vector of each effect of the POVM. For a rank-1 POVM, each effect :math:`M_k` can be written as .. math:: M_k = \gamma_k |\psi_k \rangle \langle \psi_k | = \gamma_k \frac{1}{2} \left( \mathbb{I} + \vec{a}_k \cdot \vec{\sigma} \right) where :math:`\vec{\sigma}` is the usual Pauli vector and :math:`||\vec{a}_k||^2=1`. We then define the Bloch vector of a rank-1 effect as :math:`\vec{r}_k = \gamma_k \vec{a}_k`, which uniquely defines the rank-1 effect. Example: >>> import cmath >>> import numpy as np >>> from povm_toolbox.quantum_info import SingleQubitPOVM >>> vecs = np.sqrt(1.0 / 2.0) * np.array( ... [ ... [1, 0], ... [np.sqrt(1.0 / 3.0), np.sqrt(2.0 / 3.0)], ... [np.sqrt(1.0 / 3.0), np.sqrt(2.0 / 3.0) * cmath.exp(2.0j * np.pi / 3)], ... [np.sqrt(1.0 / 3.0), np.sqrt(2.0 / 3.0) * cmath.exp(4.0j * np.pi / 3)], ... ] ... ) >>> sic_povm = SingleQubitPOVM.from_vectors(vecs) >>> bloch_vectors = sic_povm.get_bloch_vectors() >>> print(bloch_vectors) # doctest: +FLOAT_CMP [[ 0. 0. 0.5 ] [ 0.47140452 0. -0.16666667] [-0.23570226 0.40824829 -0.16666667] [-0.23570226 -0.40824829 -0.16666667]] Returns: The Bloch vector of all POVM effects. Raises: ValueError: if any effect of this POVM has a rank greater than 1. """ r = np.empty((self.num_outcomes, 3)) for i, pauli_op in enumerate(self.pauli_operators): # Check that the povm effect is rank-1: if np.linalg.matrix_rank(self.operators[i]) > 1: raise ValueError( "Bloch vector is only well-defined for single-qubit rank-1" f" POVMs. However, the effect number {i} of this POVM has" f" rank {np.linalg.matrix_rank(self.operators[i])}." ) r[i, 0] = 2 * np.real_if_close(pauli_op.get("X", 0)) r[i, 1] = 2 * np.real_if_close(pauli_op.get("Y", 0)) r[i, 2] = 2 * np.real_if_close(pauli_op.get("Z", 0)) return r
[docs] def draw_bloch( self, *, title: str = "", figure: Figure | None = None, axes: Axes | list[Axes] | None = None, figsize: tuple[float, float] | None = None, font_size: float | None = None, colorbar: bool = False, ) -> Figure: """Plot the Bloch vector of each effect of the POVM. .. plot:: :include-source: >>> import cmath >>> import numpy as np >>> from povm_toolbox.quantum_info import SingleQubitPOVM >>> vecs = np.sqrt(1.0 / 2.0) * np.array( ... [ ... [1, 0], ... [np.sqrt(1.0 / 3.0), np.sqrt(2.0 / 3.0)], ... [np.sqrt(1.0 / 3.0), np.sqrt(2.0 / 3.0) * cmath.exp(2.0j * np.pi / 3)], ... [np.sqrt(1.0 / 3.0), np.sqrt(2.0 / 3.0) * cmath.exp(4.0j * np.pi / 3)], ... ] ... ) >>> sic_povm = SingleQubitPOVM.from_vectors(vecs) >>> sic_povm.draw_bloch() <Figure size 500x500 with 1 Axes> Args: title: A string that represents the plot title. figure: User supplied Matplotlib Figure instance for plotting Bloch sphere. axes: User supplied Matplotlib axes to render the bloch sphere. figsize: Figure size in inches. Has no effect if passing ``ax``. font_size: Size of font used for Bloch sphere labels. colorbar: If ``True``, normalize the vectors on the Bloch sphere and add a colormap to keep track of the norm of the vectors. It can help to visualize the vector if they have a small norm. Returns: The resulting figure. """ if figsize is None: figsize = (5, 4) if colorbar else (5, 5) # Initialize Bloch sphere B = Bloch(fig=figure, axes=axes, font_size=font_size) # Compute Bloch vector vectors = self.get_bloch_vectors() if colorbar: # Keep track of vector norms through colorbar cmap = mpl.colormaps["viridis"] B.vector_color = [cmap(np.linalg.norm(vec)) for vec in vectors] # Normalize for i in range(len(vectors)): vectors[i] /= np.linalg.norm(vectors[i]) B.add_vectors(vectors) B.render(title=title) if figure is None: figure = B.fig axes = B.axes figure.set_size_inches(figsize[0], figsize[1]) matplotlib_close_if_inline(figure) if colorbar: figure.colorbar(mpl.cm.ScalarMappable(cmap=cmap), ax=axes, label="weight") return figure