Skip to content

Robust shadows

Robust shadows 2 were built upon the classical shadow scheme but have the particularity to be noise-resilient. It involves an experimental simple calibration procedure based on the preparation of a all-zero state with very high fidelity. We then perform noisy randomized measurements to learn about the averaged, as known as twirled, effect of the noise, obtaining calibration coefficients for shadows. One can efficiently characterize and mitigate noises in the shadow estimation scheme, given only minimal assumptions on the experimental conditions. Such a procedure has been used in 4 to estimate the Quantum Fisher information out of a quantum system. Note that robust shadows are equivalent to classical shadows in non-noisy settings by setting the calibration coefficients to \(\frac{1}{3}\) for each qubit, as shown below:

from qadence_measurement.calibration import zero_state_calibration

# Classical shadows are defined up to some accuracy and confidence.
from qadence_measurement.utils.data_acquisition import number_of_samples

shadow_options = {"accuracy": 0.1, "confidence": 0.1}
N, K = number_of_samples(observable, shadow_options["accuracy"], shadow_options["confidence"])

# Calibration coefficients are by default 1/3
calibration = zero_state_calibration(N, n_qubits=2, n_shots=100, backend=model.backend, noise=None)

robust_shadow_options = {"shadow_size": N, "shadow_medians": K, "calibration": calibration}
robust_shadow_measurement = Measurements(protocol=MeasurementProtocol.ROBUST_SHADOW, options=robust_shadow_options)
estimated_values_robust_shadow = robust_shadow_measurement(model=model)
Estimated expectation value shadow = tensor([[-2.0000]])

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_measurement.utils.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_measurement.protocol import Measurements
from qadence_measurement.utils.types import 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.7202, 0.6589])

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.

Calibration for 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_measurement.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.7606, 0.6915])
Purities with classical shadows = tensor([0.7202, 0.6589])