Source code for mitiq.shadows.quantum_processing
# Copyright (C) Unitary Fund
#
# This source code is licensed under the GPL license (v3) found in the
# LICENSE file in the root directory of this source tree.
"""Quantum processing functions for classical shadows."""
from typing import Callable, List, Optional, Sequence, Tuple
import cirq
import numpy as np
try:
from tqdm import tqdm
except ImportError:
tqdm = None
from mitiq import MeasurementResult
[docs]
def generate_random_pauli_strings(
num_qubits: int, num_strings: int
) -> List[str]:
"""Generate a list of random Pauli strings.
Args:
num_qubits: The number of qubits in the Pauli strings.
num_strings: The number of Pauli strings to generate.
Returns:
A list of random Pauli strings.
"""
# Sample random Pauli operators uniformly from ("X", "Y", "Z")
unitary_ensemble = ["X", "Y", "Z"]
paulis = np.random.choice(unitary_ensemble, (num_strings, num_qubits))
return ["".join(pauli) for pauli in paulis]
[docs]
def get_rotated_circuits(
circuit: cirq.Circuit,
pauli_strings: List[str],
add_measurements: bool = True,
qubits: Optional[Sequence[cirq.Qid]] = None,
) -> List[cirq.Circuit]:
"""Returns a list of circuits that are identical to the input circuit,
except that each one has single-qubit Clifford gates followed by
measurement gates that are designed to measure the input
Pauli strings in the Z basis.
Args:
circuit: The circuit to measure.
pauli_strings: The Pauli strings to measure in each output circuit.
add_measurements: Whether to add measurement gates to the circuit.
qubits: The qubits to measure. If None, all qubits in the circuit.
Returns:
The list of circuits with rotation and measurement gates appended.
"""
qubits = sorted(list(circuit.all_qubits())) if qubits is None else qubits
rotated_circuits = []
for pauli_string in pauli_strings:
rotated_circuit = circuit.copy()
for qubit, pauli in zip(qubits, pauli_string):
# Pauli X measurement is equivalent to H plus a Z measurement
if pauli == "X":
rotated_circuit.append(cirq.H(qubit))
# Pauli X measurement is equivalent to S^-1*H plus a Z measurement
elif pauli == "Y":
rotated_circuit.append(cirq.S(qubit) ** -1)
rotated_circuit.append(cirq.H(qubit))
# Pauli Z measurement
else:
assert (
pauli == "Z"
), f"Pauli must be X, Y, Z. Got {pauli} instead."
if add_measurements:
rotated_circuit.append(cirq.measure(*qubits))
rotated_circuits.append(rotated_circuit)
return rotated_circuits
[docs]
def random_pauli_measurement(
circuit: cirq.Circuit,
n_total_measurements: int,
executor: Callable[[cirq.Circuit], MeasurementResult],
qubits: Optional[List[cirq.Qid]] = None,
) -> Tuple[List[str], List[str]]:
r"""This function performs random Pauli measurements on a given circuit and
returns the outcomes. These outcomes are represented as a tuple of two
lists of strings.
Args:
circuit: A Cirq circuit.
n_total_measurements: The number of snapshots.
executor: A callable that runs a circuit and returns a single
bitstring.
qubits: The qubits in the circuit to be measured. If None,
all qubits in the circuit will be measured.
Warning:
The ``executor`` must return a ``MeasurementResult`` for a single shot,
i.e., a single bitstring.
Returns:
Tuple containing two lists of strings, each of length equal to
``n_total_measurements``. Strings in the first list are sequences of
0's and 1's, which represent qubit measurements outcomes in the
computational basis (e.g. "01001"). Strings in the second list are
sequences of Pauli-measurement performed on each qubit (e.g. "XZZYY").
"""
qubits = sorted(list(circuit.all_qubits())) if qubits is None else qubits
num_qubits = len(qubits)
pauli_strings = generate_random_pauli_strings(
num_qubits, n_total_measurements
)
# Rotate and attach measurement gates to the circuit
rotated_circuits = get_rotated_circuits(
circuit=circuit,
pauli_strings=pauli_strings,
add_measurements=True,
qubits=qubits,
)
if tqdm is not None:
rotated_circuits = tqdm(
rotated_circuits,
desc="Measurement",
leave=False,
)
results = [
executor(rotated_circuit) for rotated_circuit in rotated_circuits
]
shadow_outcomes = []
for result in results:
bitstring = list(result.get_counts().keys())[0]
if len(result.get_counts().keys()) > 1:
raise ValueError(
"The `executor` must return a `MeasurementResult` "
"for a single shot"
)
shadow_outcomes.append(bitstring)
return shadow_outcomes, pauli_strings