Note:

The documentation for Classical Shadows in Mitiq is still under construction. This users guide will change in the future.

How Do I Use Classical Shadows Estimation?#

The mitiq.shadows module facilitates the application of the classical shadows protocol on quantum circuits, designed for tasks like quantum state tomography or expectation value estimation. In addition this module integrates a robust shadow estimation protocol that’s tailored to counteract noise. The primary objective of the classical shadow protocol is to extract information from a quantum state using repeated measurements.

The procedure can be broken down as follows:

  1. shadow_quantum_processing:

    • Purpose: Execute quantum processing on the provided quantum circuit.

    • Outcome: Measurement results from the processed circuit.

  2. classical_post_processing:

    • Purpose: Handle classical processing of the measurement results.

    • Outcome: Estimation based on user-defined inputs.

For users aiming to employ the robust shadow estimation protocol, an initial step is needed which entails characterizing the noisy quantum channel. This is done by:

  1. pauli_twirling_calibration

    • Purpose: Characterize the noisy quantum channel.

    • Outcome: A dictionary of calibration_results.

  2. shadow_quantum_processing: same as above.

  3. classical_post_processing

    • Args: calibration_results = output of pauli_twirling_calibration

    • Outcome: Error mitigated estimation based on user-defined inputs.

Notes:

  • The calibration process is specifically designed to mitigate noise encountered during the classical shadow protocol, such as rotation and computational basis measurements. It does not address noise that occurs during state preparation.

  • Do not need to redo the calibration stage (0. pauli_twirling_calibration) if:

    1. The input circuit has a consistent number of qubits.

    2. The estimated observables have the same or fewer qubit support.

Protocol Overview#

The classical shadow protocol aims to create an approximate classical representation of a quantum state using minimal measurements. This approach not only characterizes and mitigates noise effectively but also retains sample efficiency and demonstrates noise resilience. For more details, see the section (What is the theory behind Classical Shadow Estimation?).

One can use the `mitiq.shadows’ module as follows.

User-defined inputs#

Define a quantum circuit, e.g., a circuit which prepares a GHZ state with \(n\) = 3 qubits,

import numpy as np
#fix random seed
np.random.seed(1)
import cirq

qubits = cirq.LineQubit.range(3)
num_qubits = len(qubits)
circuit = cirq.Circuit(
    cirq.H(qubits[0]),
    cirq.CNOT(qubits[0], qubits[1]),
    cirq.CNOT(qubits[1], qubits[2]),
)
print(circuit)
0: ───H───@───────
          │
1: ───────X───@───
              │
2: ───────────X───

Define an executor to run the circuit on a quantum computer or a noisy simulator. Note that the robust shadow estimation technique can only calibrate and mitigate the noise acting on the operations associated to the classical shadow protocol. So, in order to test the technique, we assume that the state preparation part of the circuit is noiseless. In particular, we define an executor in which:

  1. A noise channel is added to circuit right before the measurements. I.e. \(U_{\Lambda_U}(M_z)_{\Lambda_{\mathcal{M}_Z}}\equiv U\Lambda\mathcal{M}_Z\).

  2. A single measurement shot is taken for each circuit, as required by classical shadow protocol.

from mitiq import MeasurementResult


def cirq_executor(
    circuit: cirq.Circuit,
    noise_model_function=cirq.depolarize,
    noise_level=(0.2,),
    sampler=cirq.Simulator(),
) -> MeasurementResult:
    """
    This function returns the measurement outcomes of a circuit with noisy channel added before measurements.
    Args:
        circuit: The circuit to execute.
    Returns:
        A one shot MeasurementResult object containing the measurement outcomes.
    """
    circuit = circuit.copy()
    qubits = sorted(list(circuit.all_qubits()))
    if noise_level[0] > 0:
        noisy_circuit = cirq.Circuit()
        operations = list(circuit)
        n_ops = len(operations)
        for i, op in enumerate(operations):
            if i == n_ops - 1:
                noisy_circuit.append(
                    cirq.Moment(
                        *noise_model_function(*noise_level).on_each(*qubits)
                    )
                )
            noisy_circuit.append(op)
        circuit = noisy_circuit
    executor = cirq_sample_bitstrings(
        circuit,
        noise_model_function=None,
        noise_level=(0,),
        shots=1,
        sampler=sampler,
    )
    return executor

Given the above general executor, we define a specific example of a noisy executor, assuming a bit flip channel with a probability of `0.1’

from functools import partial

noisy_executor = partial(
    cirq_executor,
    noise_level=(0.1,),
    noise_model_function=cirq.bit_flip,
)

0. Calibration Stage#

One can simply skip this stage if one just wants to perform the classical shadow protocol (without calibration). This step can also be skipped if calibration data is already available from previous runs.

By setting the total calibration rounds \(R\) = num_total_measurements_calibration and the number of groups for the “median of means” estimation used for calibration \(K\) = k_calibration, we can characterize the noisy quantum channel (see this tutorial for more details) by running the following code:

import sys
sys.modules["tqdm"] = None # disable tqdm for cleaner notebook rendering
from mitiq.shadows import *
from mitiq.interface.mitiq_cirq.cirq_utils import (
    sample_bitstrings as cirq_sample_bitstrings,
)

f_est = pauli_twirling_calibrate(
    k_calibration=1,
    locality=2,
    qubits=qubits,
    executor=noisy_executor,
    num_total_measurements_calibration=5000,
)
f_est
{'000': 5000.0,
 '001': 1341.0,
 '110': 373.0,
 '010': 1346.0,
 '100': 1367.0,
 '101': 396.0,
 '011': 341.0}

the varible locality is the maximum number of qubits on which our operators of interest are acting on. E.g. if our operator is a sequence of two point correlation terms \(\{\langle Z_iZ_{i+1}\rangle\}_{0\leq i\leq n-1}\), then locality = 2. We note that one could also split the calibration process into two stages:

  1. shadow_quantum_processing

  • Outcome: Get quantum measurement result of the calibration circuit \(|0\rangle^{\otimes n}\) zero_state_shadow_outcomes.

  1. pauli_twirling_calibration

  • Outcome: A dictionary of calibration_results. For more details, please refer to this tutorial

1. Quantum Processing#

In this step, we obtain classical shadow snapshots from the input state (before applying the invert channel).

1.1 Add Rotation Gate and Meausure the Rotated State in Computational Basis#

At present, the implementation supports random Pauli measurement. This is equivalent to randomly sampling \(U\) from the local Clifford group \(Cl_2^n\), followed by a \(Z\)-basis measurement (see this tutorial for a clear explanation).

1.2 Get the Classical Shadows#

One can obtain the list of measurement results of local Pauli measurements in terms of bitstrings, and the related Pauli-basis measured in terms of strings as follows.

You have two choices: run the quantum measurement or directly use the results from the previous run.

  • If True, the measurement will be run again.

  • If False, the results from the previous run will be used.

import zipfile, pickle, io, requests

run_quantum_processing = False
run_pauli_twirling_calibration = False

file_directory = "../examples/resources"

if not run_quantum_processing:
    saved_data_name = "shadows-1-intro-output1"
    with open(f"{file_directory}/{saved_data_name}.pkl", "rb") as file:
        shadow_measurement_output = pickle.load(file)

else:
    shadow_measurement_output = shadow_quantum_processing(
        circuit,
        noisy_executor,
        num_total_measurements_shadow=5000,
    )

As an example, we print out one of those measurement outcomes and the associated measured operator:

print("one snapshot measurement result = ", shadow_measurement_output[0][0])
print("one snapshot measurement basis = ", shadow_measurement_output[1][0])
one snapshot measurement result =  001
one snapshot measurement basis =  XXZ

2. Classical Post-Processing#

In this step, we estimate our object of interest (expectation value or density matrix) by post-processing the (previously obtained) measurement outcomes.

2.1 Example: Operator Expectation Value Esitimation#

For example, if we want to estimate the two point correlation function \(\{\langle Z_iZ_{i+1}\rangle\}_{0\leq i\leq n-1}\), we will define the corresponding Puali strings:

from mitiq import PauliString

two_pt_correlations = [
    PauliString("ZZ", support=(i, i + 1), coeff=1)
    for i in range(0, num_qubits - 1)
]
for i in range(0, num_qubits - 1):
    print(two_pt_correlations[i]._pauli)
Z(q(0))*Z(q(1))
Z(q(1))*Z(q(2))

The corresponding expectation values can be estimated (with and without calibration) as shown in the next code cell.

est_corrs = classical_post_processing(
    shadow_outcomes=shadow_measurement_output,
    observables=two_pt_correlations,
    k_shadows=1,
)
cal_est_corrs = classical_post_processing(
    shadow_outcomes=shadow_measurement_output,
    calibration_results=f_est,
    observables=two_pt_correlations,
    k_shadows=1,
)

Let’s compare the results with the exact theoretical values:

expval_exact = []
state_vector = circuit.final_state_vector()
for i, pauli_string in enumerate(two_pt_correlations):
    exp = pauli_string._pauli.expectation_from_state_vector(
        state_vector, qubit_map={q: i for i, q in enumerate(qubits)}
    )
    expval_exact.append(exp.real)
print("Classical shadow estimation:", est_corrs)
print("Robust shadow estimation   :", cal_est_corrs)
print(
    "Exact expectation values:",
    "'Z(q(0))*Z(q(1))':",
    expval_exact[0],
    "'Z(q(1))*Z(q(2))':",
    expval_exact[1],
)
Classical shadow estimation: {'Z(q(0))*Z(q(1))': 0.6948, 'Z(q(1))*Z(q(2))': 0.7128}
Robust shadow estimation   : {'Z(q(0))*Z(q(1))': 0.000206970509383378, 'Z(q(1))*Z(q(2))': 0.00023225806451612904}
Exact expectation values: 'Z(q(0))*Z(q(1))': 1.0000000000000002 'Z(q(1))*Z(q(2))': 1.0000000000000002

2.2 Example: GHZ State Reconstruction#

In addition to the estimation of expectation values, the mitiq.shadow module can also be used to reconstruct an approximated version of the density matrix. As an example, we use the 3-qubit GHZ circuit, previously defined. As a first step, we calculate the Pauli fidelities \(f_b\) characterizing the noisy quantum channel \(\mathcal{M}=\sum_{b\in\{0,1\}^n}f_b\Pi_b\):

noisy_executor = partial(
    cirq_executor,
    noise_level=(0.2,),
    noise_model_function=cirq.bit_flip,
)

if not run_pauli_twirling_calibration:
    saved_data_name = "shadows-1-intro-PTC-50000"
    with open(f"{file_directory}/{saved_data_name}.pkl", "rb") as file:
        f_est = pickle.load(file)

else:
    f_est = pauli_twirling_calibrate(
        k_calibration=1,
        qubits=qubits,
        executor=noisy_executor,
        num_total_measurements_calibration=50000
    )

f_est
{'000': (1+0j),
 '100': (0.20008+0j),
 '010': (0.20252+0j),
 '001': (0.19812+0j),
 '110': (0.03964+0j),
 '101': (0.03968+0j),
 '011': (0.03862+0j),
 '111': (0.00712+0j)}

Similar to the previous case (estimation of expectation values), the quantum processing for estimating the density matrix is done as follows.

if not run_quantum_processing:
    saved_data_name = "shadows-1-intro-output2"
    with open(f"{file_directory}/{saved_data_name}.pkl", "rb") as file:
        shadow_measurement_output = pickle.load(file)

else:
    shadow_measurement_output = shadow_quantum_processing(
        circuit,
        noisy_executor,
        num_total_measurements_shadow=50000,
    )
est_corrs = classical_post_processing(
    shadow_outcomes=shadow_measurement_output,
    state_reconstruction=True,
)
cal_est_corrs = classical_post_processing(
    shadow_outcomes=shadow_measurement_output,
    calibration_results=f_est,
    state_reconstruction=True,
)

Let’s compare the fidelity between the reconstructed state and the ideal state.

from mitiq.utils import operator_ptm_vector_rep

ghz_state = circuit.final_state_vector().reshape(-1, 1)
ghz_true = ghz_state @ ghz_state.conj().T
ptm_ghz_state = operator_ptm_vector_rep(ghz_true)
from mitiq.shadows.shadows_utils import fidelity

fidelity_shadow = fidelity(ghz_true, est_corrs["reconstructed_state"])
fidelity_shadow_calibrated = fidelity(
    ptm_ghz_state, cal_est_corrs["reconstructed_state"]
)
print(
    f"fidelity between true state and shadow reconstruced state {fidelity_shadow}"
)
print(
    f"fidelity between true state and rshadow reconstruced state {fidelity_shadow_calibrated}"
)
fidelity between true state and shadow reconstruced state 0.37301749999999956
fidelity between true state and rshadow reconstruced state 1.198425487974679