Skip to content

Robust shadow tomography

In this tutorial, we will estimate a physical property out of a quantum system, namely the purity of the partial traces, in the presence of measurement noise. To do so, we will use the formalism of classical shadows1, and especially their robust version2. This tutorial is inspired from a notebook example of robust shadow tomography from the randomized measurements toolbox5 in Julia3.

Setting the model

First, we will set the noise model and a circuit from which we will estimate the purity.

Noise model

We will use a depolarizing noise model with a different error probability per qubit.

import torch
from qadence import NoiseHandler, NoiseProtocol

torch.manual_seed(0)
n_qubits = 2
error_probs = torch.clamp(0.1 + 0.02 * torch.randn(n_qubits), min=0, max=1)

noise = NoiseHandler(protocol=NoiseProtocol.DIGITAL.DEPOLARIZING, options={"error_probability": error_probs[0], "target": 0})

for i, proba in enumerate(error_probs[1:]):
    noise.digital_depolarizing(options={"error_probability": proba, "target": i+1})
Error probabilities = tensor([0.1308, 0.0941])

Noiseless circuit and model

Let us set the circuit without noise and calculating the expected purities:

from qadence import *

theta1 = Parameter("theta1", trainable=False)
theta2 = Parameter("theta2", trainable=False)
theta3 = Parameter("theta3", trainable=False)
theta4 = Parameter("theta4", trainable=False)

blocks = chain(
    kron(RX(0, theta1), RY(1, theta2)),
    kron(RX(0, theta3), RY(1, theta4,),),
)

circuit = QuantumCircuit(2, blocks)

values = {
    "theta1": torch.tensor([0.5]),
    "theta2": torch.tensor([1.5]),
    "theta3": torch.tensor([2.0]),
    "theta4": torch.tensor([2.5]),
}
# no observable needed here
model = QuantumModel(
    circuit=circuit,
)

For calculating purities, we can use the utility functions partial_trace and purity:

from qadence_protocols.utils_trace import partial_trace, purity

def partial_purities(density_mat):
    purities = []
    for i in range(n_qubits):
        partial_trace_i = partial_trace(density_mat, [i]).squeeze()
        purities.append(purity(partial_trace_i))

    return torch.tensor(purities)

expected_purities = partial_purities(model.run(values))
Expected purities = tensor([0.8209, 0.7136])

Add noise to circuit

The circuit is defined as follows where we set the previous noise model in the last operations as measurement noise.

noisy_blocks = chain(
    kron(RX(0, theta1), RY(1, theta2)),
    kron(RX(0, theta3, NoiseHandler(protocol=NoiseProtocol.DIGITAL.DEPOLARIZING, options={"error_probability": error_probs[0], "target": 0})),
        RY(1, theta4, NoiseHandler(protocol=NoiseProtocol.DIGITAL.DEPOLARIZING, options={"error_probability": error_probs[1], "target": 1})),
        ),
)

noisy_circuit = QuantumCircuit(2, noisy_blocks)
noisy_model = QuantumModel(
    circuit=noisy_circuit,
)

Shadow estimations

Vanilla classical shadows

We will first run vanilla shadows to reconstruct the density matrix representation of the circuit, from which we can estimate the purities.

from qadence_protocols import Measurements, MeasurementProtocol

shadow_options = {"shadow_size": 10200, "shadow_medians": 6, "n_shots":1000}
shadow_measurements = Measurements(protocol=MeasurementProtocol.SHADOW, options=shadow_options)
shadow_measurements.measure(noisy_model, param_values=values)
vanilla_purities = partial_purities(shadow_measurements.reconstruct_state())
Purities with classical shadows = tensor([0.7295, 0.6638])

As we can see, the estimated purities diverge from the expected ones due to the presence of noise. Next, we will use robust shadows to mitigate the noise effect.

Robust shadows

We now use an efficient calibration method based on the experimental demonstration of classical shadows4. A first set of measurements are used to determine calibration coefficients. The latter are used within robust shadows to mitigate measurement errors. Indeed, we witness below the estimated purities being closer to the analytical ones.

from qadence_protocols.measurements.calibration import zero_state_calibration

calibration = zero_state_calibration(n_unitaries=2000, n_qubits=circuit.n_qubits, n_shots=10000, noise=noise)
robust_options = {"shadow_size": 10200, "shadow_medians": 6, "n_shots":1000, "calibration": calibration}
robust_shadow_measurements = Measurements(protocol=MeasurementProtocol.ROBUST_SHADOW, options=robust_options)
robust_shadow_measurements.measure(noisy_model, param_values=values)
robust_purities = partial_purities(robust_shadow_measurements.reconstruct_state())
Expected purities = tensor([0.8209, 0.7136])
Purities with robust shadows = tensor([0.7575, 0.6907])
Purities with classical shadows = tensor([0.7295, 0.6638])