Source code for mitiq.shadows.classical_postprocessing

# Copyright (C) Unitary Fund
# Portions of this code have been adapted from PennyLane's tutorial
# on Classical Shadows.
# Original authors: PennyLane developers: Brian Doolittle, Roeland Wiersema
# Tutorial link: https://pennylane.ai/qml/demos/tutorial_classical_shadows
#
# This source code is licensed under the GPL license (v3) found in the
# LICENSE file in the root directory of this source tree.
"""Classical post-processing process of classical shadows."""

from collections import defaultdict
from functools import reduce
from itertools import compress
from operator import mul
from statistics import median
from typing import Any, Dict, List, Optional, Tuple

import cirq
import numpy as np
import numpy.typing as npt

import mitiq
from mitiq.shadows.shadows_utils import (
    batch_calibration_data,
    create_string,
    valid_bitstrings,
)
from mitiq.utils import matrix_kronecker_product, operator_ptm_vector_rep

# Local unitaries to measure Pauli operators in the Z basis
PAULI_MAP = {
    "X": cirq.unitary(cirq.H),
    "Y": cirq.unitary(cirq.H) @ cirq.unitary(cirq.S).conj(),
    "Z": cirq.unitary(cirq.I),
}

ZERO_STATE = np.array([[1, 0], [0, 0]])
ONE_STATE = np.array([[0, 0], [0, 1]])


[docs] def get_single_shot_pauli_fidelity( bitstring: str, paulistring: str, locality: Optional[int] = None ) -> Dict[str, float]: r""" Calculate Pauli fidelity :math:`f_b` for a single shot measurement of the calibration circuit for b= bit_string. In the notation of arXiv:2011.09636, this function estimates the coefficient :math:`f_b`, which characterizes the (noisy) classical shadow channel. The locality is realized on the assumption that the noisy channel :math:`\Lambda` is local :math:`\Lambda \equiv \bigotimes_i^n\Lambda_i`. Args: bit_string: The bitstring corresponding to a computational basis state. E.g., '01...0':math:`:=|0\rangle|1\rangle...|0\rangle`. pauli_string: The local Pauli measurement performed on each qubit. e.g.'XY...Z' means perform local X-basis measurement on the 1st qubit, local Y-basis measurement the 2ed qubit, local Z-basis measurement the last qubit in the circuit. locality: The locality of the operator, whose expectation value is going to be estimated by the classical shadow. E.g., if the operator is the Ising model Hamiltonian with nearest neighbor interactions, then locality = 2. Returns: A dictionary of Pauli fidelity bit_string: :math:`\{{f}_b\}`. If the locality is :math:`w < n`, then derive the output's keys from the bit_string. Ensure that the number of 1s in the keys is less than or equal to w. The corresponding Pauli fidelity is the product of local Pauli fidelity where the associated locus in the keys are '1'. """ pauli_fidelity = {"Z0": 1.0, "Z1": -1.0} local_fidelities = [ pauli_fidelity.get(p + b, 0.0) for b, p in zip(bitstring, paulistring) ] num_qubits = len(bitstring) bitstrings = valid_bitstrings(num_qubits, max_hamming_weight=locality) fidelities = {} for bitstring in bitstrings: subset_fidelities = compress(local_fidelities, map(int, bitstring)) fidelities[bitstring] = reduce(mul, subset_fidelities, 1.0) return fidelities
[docs] def get_pauli_fidelities( calibration_outcomes: Tuple[List[str], List[str]], num_batches: int, locality: Optional[int] = None, ) -> Dict[str, complex]: r""" Calculate Pauli fidelities for the calibration circuit. In the notation of arXiv:2011.09636, this function estimates the coefficients :math:`f_b`, which characterize the (noisy) classical shadow channel :math:`\mathcal{M}=\sum_b f_b \Pi_b`. Args: calibration_measurement_outcomes: The `random_Pauli_measurement` outcomes for the state :math:`|0\rangle^{\otimes n}`}` . num_batches: The number of batches in the median of means estimator. locality: The locality of the operator, whose expectation value is going to be estimated by the classical shadow. E.g., if the operator is the Ising model Hamiltonian with nearest neighbor interactions, then locality = 2. Returns: A :math:`2^n`-dimensional dictionary of Pauli fidelities :math:`f_b` for :math:`b = \{0,1\}^{n}` """ means = defaultdict(list) for bitstrings, paulistrings in batch_calibration_data( calibration_outcomes, num_batches ): all_fidelities = defaultdict(list) for bitstring, paulistring in zip(bitstrings, paulistrings): fidelities = get_single_shot_pauli_fidelity( bitstring, paulistring, locality=locality ) for b, f in fidelities.items(): all_fidelities[b].append(f) for bitstring, fids in all_fidelities.items(): means[bitstring].append(sum(fids) / num_batches) return { bitstring: median(averages) for bitstring, averages in means.items() }
[docs] def classical_snapshot( bitstring: str, paulistring: str, fidelities: Optional[Dict[str, float]] = None, ) -> npt.NDArray[Any]: r""" Implement a single snapshot state reconstruction with calibration of the noisy quantum channel. Args: bitstring: A bitstring corresponding to the outcome ... TODO paulistring: String of the applied Pauli measurement on each qubit. f_est: The estimated Pauli fidelities to use for calibration if available. Returns: Reconstructed classical snapshot in terms of nparray. """ # calibrate the noisy quantum channel, output in PTM rep. # ptm rep of identity I_ptm = operator_ptm_vector_rep(np.eye(2) / np.sqrt(2)) pi_zero = np.outer(I_ptm, I_ptm) pi_one = np.eye(4) - pi_zero pi_zero = np.diag(pi_zero) pi_one = np.diag(pi_one) if fidelities: elements = [] for bits, fidelity in fidelities.items(): pi_snapshot_vector = [] for b1, b2, pauli in zip(bits, bitstring, paulistring): # get pi for each qubit based on calibration measurement pi = pi_zero if b1 == "0" else pi_one # get state for each qubit based on shadow measurement state = ZERO_STATE if b2 == "0" else ONE_STATE # get U for each qubit based on shadow measurement U = PAULI_MAP[pauli] pi_snapshot_vector.append( pi * operator_ptm_vector_rep(U.conj().T @ state @ U) ) elements.append( 1 / fidelity * matrix_kronecker_product(pi_snapshot_vector) ) rho_snapshot = np.sum(elements, axis=0) else: local_rhos = [] for bit, pauli in zip(bitstring, paulistring): state = ZERO_STATE if bit == "0" else ONE_STATE U = PAULI_MAP[pauli] # apply inverse of the quantum channel,get PTM vector rep local_rho = 3.0 * (U.conj().T @ state @ U) - cirq.unitary(cirq.I) local_rhos.append(local_rho) rho_snapshot = matrix_kronecker_product(local_rhos) return rho_snapshot
[docs] def shadow_state_reconstruction( shadow_measurement_outcomes: Tuple[List[str], List[str]], fidelities: Optional[Dict[str, float]] = None, ) -> npt.NDArray[Any]: """Reconstruct a state approximation as an average over all snapshots. Args: shadow_measurement_outcomes: Measurement result and the basis performing the measurement obtained from `random_pauli_measurement` for classical shadow protocol. f_est: The estimated Pauli fidelities to use for calibration if available. Returns: The state reconstructed from classical shadow protocol """ bitstrings, paulistrings = shadow_measurement_outcomes return np.mean( [ classical_snapshot(bitstring, paulistring, fidelities) for bitstring, paulistring in zip(bitstrings, paulistrings) ], axis=0, )
[docs] def expectation_estimation_shadow( measurement_outcomes: Tuple[List[str], List[str]], pauli: mitiq.PauliString, num_batches: int, fidelities: Optional[Dict[str, float]] = None, ) -> float: """Calculate the expectation value of an observable from classical shadows. Use median of means to ameliorate the effects of outliers. Args: measurement_outcomes: A shadow tuple obtained from `z_basis_measurement`. pauli_str: Single mitiq observable consisting of Pauli operators. num_batches: Number of batches to process measurement outcomes in. f_est: The estimated Pauli fidelities to use for calibration if available. Returns: Float corresponding to the estimate of the observable expectation value. """ bitstrings, paulistrings = measurement_outcomes num_qubits = len(bitstrings[0]) qubits = sorted(pauli.support()) filtered_bitstrings = [ "".join([bitstring[q] for q in qubits]) for bitstring in bitstrings ] filtered_paulis = [ "".join([pauli[q] for q in qubits]) for pauli in paulistrings ] filtered_data = (filtered_bitstrings, filtered_paulis) means = [] for bits, paulis in batch_calibration_data(filtered_data, num_batches): matching_indices = [i for i, p in enumerate(paulis) if p == pauli.spec] if matching_indices: matching_bits = (bits[i] for i in matching_indices) product = sum((-1) ** bit.count("1") for bit in matching_bits) if fidelities: b = create_string(num_qubits, qubits) product /= fidelities.get(b, np.inf) else: product *= 3 ** len(qubits) else: product = 0.0 means.append(product / len(bits)) return np.real(np.median(means) * pauli.coeff)