Source code for mitiq.shadows.shadows_utils
# 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.
"""Defines utility functions for classical shadows protocol."""
from typing import Generator, List, Optional, Tuple
import numpy as np
import numpy.typing as npt
from scipy.linalg import sqrtm
import mitiq
[docs]
def create_string(str_len: int, loc_list: List[int]) -> str:
"""
This function returns a string of length ``str_len`` with 1s at the
locations specified by ``loc_list`` and 0s elsewhere.
Args:
str_len: The length of the string.
loc_list: A list of integers indices specifying the locations of 1s in
the string.
Returns:
A bitstring constructed as above.
Example:
A basic example::
create_string(5, [1, 3])
>>> "01010"
"""
return "".join(
map(lambda i: "1" if i in set(loc_list) else "0", range(str_len))
)
[docs]
def valid_bitstrings(
num_qubits: int, max_hamming_weight: Optional[int] = None
) -> set[str]:
"""
Description.
Args:
num_qubits:
max_hamming_weight:
Returns:
The set of all valid bitstrings on ``num_qubits`` bits, with a maximum
hamming weight.
Raises:
Value error when ``max_hamming_weight`` is not greater than 0.
"""
if max_hamming_weight and max_hamming_weight < 1:
raise ValueError(
"max_hamming_weight must be greater than 0. "
f"Got {max_hamming_weight}."
)
bitstrings = {
bin(i)[2:].zfill(num_qubits)
for i in range(2**num_qubits)
if bin(i).count("1") <= (max_hamming_weight or num_qubits)
}
return bitstrings
[docs]
def fidelity(
sigma: npt.NDArray[np.complex64], rho: npt.NDArray[np.complex64]
) -> float:
"""
Calculate the fidelity between two states.
Args:
sigma: A state in terms of square matrix or vector.
rho: A state in terms square matrix or vector.
Returns:
Scalar corresponding to the fidelity.
"""
if sigma.ndim == 1 and rho.ndim == 1:
val = np.abs(np.dot(sigma.conj(), rho)) ** 2.0
elif sigma.ndim == 1 and rho.ndim == 2:
val = np.abs(sigma.conj().T @ rho @ sigma)
elif sigma.ndim == 2 and rho.ndim == 1:
val = np.abs(rho.conj().T @ sigma @ rho)
elif sigma.ndim == 2 and rho.ndim == 2:
val = np.abs(np.trace(sqrtm(sigma) @ rho @ sqrtm(sigma)))
else:
raise ValueError("Invalid input dimensions")
return float(val)
[docs]
def batch_calibration_data(
data: Tuple[List[str], List[str]], num_batches: int
) -> Generator[Tuple[List[str], List[str]], None, None]:
"""Batch calibration into chunks of size batch_size.
Args:
data: The random Pauli measurement outcomes.
batch_size: Size of each batch that will be processed.
Yields:
Tuples of bit strings and pauli strings.
"""
bits, paulis = data
batch_size = len(bits) // num_batches
for i in range(0, len(bits), batch_size):
yield bits[i : i + batch_size], paulis[i : i + batch_size]
[docs]
def n_measurements_tomography_bound(epsilon: float, num_qubits: int) -> int:
"""
This function returns the minimum number of classical shadows required
for state reconstruction for achieving the desired accuracy.
Args:
epsilon: The error on the estimator.
num_qubits: The number of qubits in the system.
Returns:
An integer that gives the number of snapshots required to satisfy the
shadow bound.
"""
return int(34 * (4**num_qubits) * epsilon ** (-2))
[docs]
def local_clifford_shadow_norm(obs: mitiq.PauliString) -> float:
"""
Calculate shadow norm of an operator with random unitary sampled from local
Clifford group.
Args:
obs: A self-adjoint operator, i.e. mitiq.PauliString with real coffe.
Returns:
Shadow norm when unitary ensemble is local Clifford group.
"""
opt = obs.matrix()
norm = (
np.linalg.norm(
opt - np.trace(opt) / 2 ** int(np.log2(opt.shape[0])),
ord=np.inf,
)
** 2
)
return float(norm)
[docs]
def n_measurements_opts_expectation_bound(
error: float,
observables: List[mitiq.PauliString],
failure_rate: float,
) -> Tuple[int, int]:
"""
This function returns the minimum number of classical shadows required and
the number of groups "k" into which we need to split the shadows for
achieving the desired accuracy and failure rate in operator expectation
value estimation.
Args:
error: The error on the estimator.
observables: List of mitiq.PauliString corresponding to the
observables we intend to measure.
failure_rate: Rate of failure for the bound to hold.
Returns:
Integers quantifying the number of snapshots required to satisfy
the shadow bound and the chunk size required to attain the specified
failure rate.
"""
M = len(observables)
K = 2 * np.log(2 * M / failure_rate)
N = 34 * max(local_clifford_shadow_norm(o) for o in observables) / error**2
return int(np.ceil(N * K)), int(K)