Visualization of POVMs

How to visualize single-qubit POVMs (and products of them) in a simple way.

For a single-qubit rank-1 POVM, each effect \(M_k\) can be written as

\[M_k = \gamma_k |\psi_k \rangle \langle \psi_k | = \gamma_k \frac{1}{2} ( \mathbb{I} + \vec{a}_k \cdot \vec{\sigma} ) \, ,\]

where \(\vec{\sigma}\) is the usual Pauli vector and \(||\vec{a}_k||^2=1\).

We then define the Bloch vector of a rank-1 effect as

\[\vec{r}_k = \gamma_k \vec{a}_k \, ,\]

which uniquely defines the rank-1 effect.

Let us first look at a classical shadows measurement.

[1]:
from povm_toolbox.library import ClassicalShadows

# Define the classical shadows measurement. It creates a trivial product POVM
# with only one single-qubit measurement.
cs = ClassicalShadows(1)
# Extract the single-qubit POVM from the product POVM.
sqpovm1 = cs.definition()[(0,)]
# Draw the Bloch vectors of the POVM.
sqpovm1.draw_bloch(title="Classical Shadows")
[1]:
../_images/how_tos_visualization_3_0.png

Two different representations

Let us look as an example to showcase two possible representation of the Bloch vectors

[2]:
import matplotlib.pyplot as plt
from povm_toolbox.quantum_info import SingleQubitPOVM
from qiskit.quantum_info import Operator

# Directly define the single-qubit POVM
sqpovm2 = SingleQubitPOVM(
    [
        0.8 * Operator.from_label("0"),
        0.8 * Operator.from_label("1"),
        0.2 * Operator.from_label("+"),
        0.2 * Operator.from_label("-"),
    ]
)

# Draw the usual Bloch vectors. Since the last two effects have a small norm,
# it is difficult to visualize their direction.
sqpovm2.draw_bloch(title="Raw vectors")
[2]:
../_images/how_tos_visualization_5_0.png
[3]:
# Instead, we can normalize the vector and plot their norm as a colormap.
sqpovm2.draw_bloch(colorbar=True, title="Normalized vectors with colormap")
[3]:
../_images/how_tos_visualization_6_0.png

Another example with a symmetric and informationally-complete POVM (SIC-POVM)

[4]:
import cmath

import numpy as np

# Define a symmetric and informationally-complete POVM (SIC-POVM)
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(title="SIC-POVM", colorbar=True)
[4]:
../_images/how_tos_visualization_8_0.png

Drawing product POVM

[5]:
from povm_toolbox.quantum_info import ProductPOVM

prod_povm = ProductPOVM.from_list([sqpovm1, sqpovm2])
prod_povm.draw_bloch()
[5]:
../_images/how_tos_visualization_10_0.png
[6]:
prod_povm = ProductPOVM(
    {(1,): sqpovm2, (2,): sqpovm1, (4,): sic_povm, (6,): sic_povm, (3,): sqpovm2, (0,): sic_povm}
)
prod_povm.draw_bloch()
[6]:
../_images/how_tos_visualization_11_0.png

Plotting on a user-defined figure.

The user can define the figure and axes to be used to plot the Bloch vectors.

[7]:
fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(2, 2, 1, projection="3d")
sqpovm2.draw_bloch(figure=fig, axes=ax, title="4-outcome POVM")

ax = fig.add_subplot(2, 2, 2, projection="3d")
sic_povm.draw_bloch(figure=fig, axes=ax, title="SIC-POVM")
ax = fig.add_subplot(2, 2, 3, projection="3d")
sqpovm1.draw_bloch(figure=fig, axes=ax, title="Classical Shadows")

plt.show()
../_images/how_tos_visualization_13_0.png

Further examples with increasing number of qubits

We look at classical shadows, locally-biased classical shadows and randomized projective measurements.

[8]:
num_qubits = 8

cs_povm = ClassicalShadows(num_qubits).definition()
cs_povm.draw_bloch(title=f"{num_qubits}-qubit Classical Shadows")
[8]:
../_images/how_tos_visualization_15_0.png
[9]:
from povm_toolbox.library import LocallyBiasedClassicalShadows

num_qubits = 19

# Define the bias
bias = np.array(
    [
        [0.36094312, 0.50796196, 0.13109492],
        [0.62943668, 0.2935754, 0.07698793],
        [0.4560272, 0.51934089, 0.02463191],
        [0.05196344, 0.92902357, 0.01901299],
        [0.12965604, 0.23763234, 0.63271162],
        [0.61626697, 0.02046334, 0.36326969],
        [0.41067641, 0.24565853, 0.34366506],
        [0.16973604, 0.43348055, 0.39678341],
        [0.18148969, 0.34008117, 0.47842915],
        [0.47717857, 0.30436939, 0.21845204],
        [0.00546897, 0.4053871, 0.58914393],
        [0.381513, 0.41750024, 0.20098675],
        [0.34945753, 0.43306577, 0.21747669],
        [0.32524666, 0.34202124, 0.3327321],
        [0.24619264, 0.51835773, 0.23544963],
        [0.04301446, 0.52050851, 0.43647703],
        [0.3361605, 0.0079976, 0.6558419],
        [0.15042373, 0.35211066, 0.49746561],
        [0.17035462, 0.33837793, 0.49126745],
    ]
)

cs_povm = LocallyBiasedClassicalShadows(num_qubits, bias=bias).definition()
cs_povm.draw_bloch(title=f"{num_qubits}-qubit Locally-Biased Classical Shadows")
[9]:
../_images/how_tos_visualization_16_0.png
[10]:
from numpy.random import default_rng
from povm_toolbox.library import RandomizedProjectiveMeasurements

rng = default_rng(96)

num_qubits = 32

# Choose the angles at random
phi = 2 * np.pi * rng.uniform(0, 1, size=3 * num_qubits).reshape((num_qubits, 3))
theta = np.arccos(2 * rng.uniform(0, 1, size=3 * num_qubits).reshape((num_qubits, 3)) - 1.0)
angles = np.concatenate((theta, phi), axis=1)

# Also choose the bias at random
bias = rng.uniform(0, 1, size=3 * num_qubits).reshape((num_qubits, 3))
bias /= bias.sum(axis=1)[:, np.newaxis]

cs_povm = RandomizedProjectiveMeasurements(num_qubits, bias=bias, angles=angles).definition()
cs_povm.draw_bloch(title=f"{num_qubits}-qubit Randomized Projective Measurements")
[10]:
../_images/how_tos_visualization_17_0.png
[11]:
cs_povm.draw_bloch(colorbar=True, title=f"{num_qubits}-qubit Randomized Projective Measurements")
[11]:
../_images/how_tos_visualization_18_0.png