Source code for pennylane.measurements.classical_shadow

# Copyright 2018-2021 Xanadu Quantum Technologies Inc.

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
This module contains the qml.classical_shadow measurement.
"""
import copy
from collections.abc import Iterable, Sequence
from string import ascii_letters as ABC
from typing import Optional, Union

import numpy as np

import pennylane as qml
from pennylane.operation import Operator
from pennylane.wires import Wires

from .measurements import MeasurementShapeError, MeasurementTransform, Shadow, ShadowExpval


[docs]def shadow_expval(H, k=1, seed=None): r"""Compute expectation values using classical shadows in a differentiable manner. The canonical way of computing expectation values is to simply average the expectation values for each local snapshot, :math:`\langle O \rangle = \sum_t \text{tr}(\rho^{(t)}O) / T`. This corresponds to the case ``k=1``. In the original work, `2002.08953 <https://arxiv.org/abs/2002.08953>`_, it has been proposed to split the ``T`` measurements into ``k`` equal parts to compute the median of means. For the case of Pauli measurements and Pauli observables, there is no advantage expected from setting ``k>1``. Args: H (Union[Iterable[Operator], Operator]): Observable or iterable of observables to compute the expectation value over. k (int): Number of equal parts to split the shadow's measurements to compute the median of means. ``k=1`` (default) corresponds to simply taking the mean over all measurements. seed (Union[None, int]): Seed used to randomly sample Pauli measurements during the classical shadows protocol. If None, a random seed will be generated. If a tape with a ``shadow_expval`` measurement is copied, the seed will also be copied. Different seeds are still generated for different constructed tapes. Returns: ShadowExpvalMP: Measurement process instance .. note:: This measurement uses the measurement :func:`~.pennylane.classical_shadow` and the class :class:`~.pennylane.ClassicalShadow` for post-processing internally to compute expectation values. In order to compute correct gradients using PennyLane's automatic differentiation, you need to use this measurement. **Example** .. code-block:: python3 H = qml.Hamiltonian([1., 1.], [qml.Z(0) @ qml.Z(1), qml.X(0) @ qml.X(1)]) dev = qml.device("default.qubit", wires=range(2), shots=10000) @qml.qnode(dev) def circuit(x, obs): qml.Hadamard(0) qml.CNOT((0,1)) qml.RX(x, wires=0) return qml.shadow_expval(obs) x = np.array(0.5, requires_grad=True) We can compute the expectation value of H as well as its gradient in the usual way. >>> circuit(x, H) array(1.8774) >>> qml.grad(circuit)(x, H) -0.44999999999999984 In ``shadow_expval``, we can pass a list of observables. Note that each qnode execution internally performs one quantum measurement, so be sure to include all observables that you want to estimate from a single measurement in the same execution. >>> Hs = [H, qml.X(0), qml.Y(0), qml.Z(0)] >>> circuit(x, Hs) array([ 1.881 , -0.0312, -0.0027, -0.0087]) >>> qml.jacobian(circuit)(x, Hs) array([-0.4518, 0.0174, -0.0216, -0.0063]) """ seed = seed or np.random.randint(2**30) return ShadowExpvalMP(H=H, seed=seed, k=k)
[docs]def classical_shadow(wires, seed=None): """ The classical shadow measurement protocol. The protocol is described in detail in the paper `Predicting Many Properties of a Quantum System from Very Few Measurements <https://arxiv.org/abs/2002.08953>`_. This measurement process returns the randomized Pauli measurements (the ``recipes``) that are performed for each qubit and snapshot as an integer: - 0 for Pauli X, - 1 for Pauli Y, and - 2 for Pauli Z. It also returns the measurement results (the ``bits``); 0 if the 1 eigenvalue is sampled, and 1 if the -1 eigenvalue is sampled. The device shots are used to specify the number of snapshots. If ``T`` is the number of shots and ``n`` is the number of qubits, then both the measured bits and the Pauli measurements have shape ``(T, n)``. Args: wires (Sequence[int]): the wires to perform Pauli measurements on seed (Union[None, int]): Seed used to randomly sample Pauli measurements during the classical shadows protocol. If None, a random seed will be generated. If a tape with a ``classical_shadow`` measurement is copied, the seed will also be copied. Different seeds are still generated for different constructed tapes. Returns: ClassicalShadowMP: measurement process instance **Example** Consider the following QNode that prepares a Bell state and performs a classical shadow measurement: .. code-block:: python3 dev = qml.device("default.qubit", wires=2, shots=5) @qml.qnode(dev) def circuit(): qml.Hadamard(wires=0) qml.CNOT(wires=[0, 1]) return qml.classical_shadow(wires=[0, 1]) Executing this QNode produces the sampled bits and the Pauli measurements used: >>> bits, recipes = circuit() >>> bits tensor([[0, 0], [1, 0], [1, 0], [0, 0], [0, 1]], dtype=uint8, requires_grad=True) >>> recipes tensor([[2, 2], [0, 2], [1, 0], [0, 2], [0, 2]], dtype=uint8, requires_grad=True) .. details:: :title: Usage Details Consider again the QNode in the above example. Since the Pauli observables are randomly sampled, executing this QNode again would produce different bits and Pauli recipes: >>> bits, recipes = circuit() >>> bits tensor([[0, 1], [0, 1], [0, 0], [0, 1], [1, 1]], dtype=uint8, requires_grad=True) >>> recipes tensor([[1, 0], [2, 1], [2, 2], [1, 0], [0, 0]], dtype=uint8, requires_grad=True) To use the same Pauli recipes for different executions, the :class:`~.tape.QuantumTape` interface should be used instead: .. code-block:: python3 dev = qml.device("default.qubit", wires=2, shots=5) ops = [qml.Hadamard(wires=0), qml.CNOT(wires=(0,1))] measurements = [qml.classical_shadow(wires=(0,1))] tape = qml.tape.QuantumTape(ops, measurements, shots=5) >>> bits1, recipes1 = qml.execute([tape], device=dev, gradient_fn=None)[0] >>> bits2, recipes2 = qml.execute([tape], device=dev, gradient_fn=None)[0] >>> np.all(recipes1 == recipes2) True >>> np.all(bits1 == bits2) False If using different Pauli recipes is desired for the :class:`~.tape.QuantumTape` interface, different seeds should be used for the classical shadow: .. code-block:: python3 dev = qml.device("default.qubit", wires=2, shots=5) measurements1 = [qml.classical_shadow(wires=(0,1), seed=10)] tape1 = qml.tape.QuantumTape(ops, measurements1, shots=5) measurements2 = [qml.classical_shadow(wires=(0,1), seed=15)] tape2 = qml.tape.QuantumTape(ops, measurements2, shots=5) >>> bits1, recipes1 = qml.execute([tape1], device=dev, gradient_fn=None)[0] >>> bits2, recipes2 = qml.execute([tape2], device=dev, gradient_fn=None)[0] >>> np.all(recipes1 == recipes2) False >>> np.all(bits1 == bits2) False """ wires = Wires(wires) seed = seed or np.random.randint(2**30) return ClassicalShadowMP(wires=wires, seed=seed)
[docs]class ClassicalShadowMP(MeasurementTransform): """Represents a classical shadow measurement process occurring at the end of a quantum variational circuit. Please refer to :func:`pennylane.classical_shadow` for detailed documentation. Args: wires (.Wires): The wires the measurement process applies to. seed (Union[int, None]): The seed used to generate the random measurements id (str): custom label given to a measurement instance, can be useful for some applications where the instance has to be identified """ def __init__( self, wires: Optional[Wires] = None, seed: Optional[int] = None, id: Optional[str] = None ): self.seed = seed super().__init__(wires=wires, id=id) def _flatten(self): metadata = (("wires", self.wires), ("seed", self.seed)) return (None, None), metadata @property def hash(self): """int: returns an integer hash uniquely representing the measurement process""" fingerprint = ( self.__class__.__name__, self.seed, tuple(self.wires.tolist()), ) return hash(fingerprint)
[docs] def process(self, tape, device): """ Returns the measured bits and recipes in the classical shadow protocol. The protocol is described in detail in the `classical shadows paper <https://arxiv.org/abs/2002.08953>`_. This measurement process returns the randomized Pauli measurements (the ``recipes``) that are performed for each qubit and snapshot as an integer: - 0 for Pauli X, - 1 for Pauli Y, and - 2 for Pauli Z. It also returns the measurement results (the ``bits``); 0 if the 1 eigenvalue is sampled, and 1 if the -1 eigenvalue is sampled. The device shots are used to specify the number of snapshots. If ``T`` is the number of shots and ``n`` is the number of qubits, then both the measured bits and the Pauli measurements have shape ``(T, n)``. This implementation is device-agnostic and works by executing single-shot quantum tapes containing randomized Pauli observables. Devices should override this if they can offer cleaner or faster implementations. .. seealso:: :func:`~pennylane.classical_shadow` Args: tape (QuantumTape): the quantum tape to be processed device (pennylane.Device): the device used to process the quantum tape Returns: tensor_like[int]: A tensor with shape ``(2, T, n)``, where the first row represents the measured bits and the second represents the recipes used. """ wires = self.wires n_snapshots = device.shots seed = self.seed original_shots = device.shots original_shot_vector = device._shot_vector # pylint: disable=protected-access try: device.shots = 1 # slow implementation but works for all devices n_qubits = len(wires) mapped_wires = np.array(device.map_wires(wires)) # seed the random measurement generation so that recipes # are the same for different executions with the same seed rng = np.random.RandomState(seed) recipes = rng.randint(0, 3, size=(n_snapshots, n_qubits)) obs_list = [qml.X, qml.Y, qml.Z] outcomes = np.zeros((n_snapshots, n_qubits)) for t in range(n_snapshots): # compute rotations for the Pauli measurements rotations = [ rot for wire_idx, wire in enumerate(wires) for rot in obs_list[recipes[t][wire_idx]].compute_diagonalizing_gates( wires=wire ) ] device.reset() device.apply(tape.operations, rotations=tape.diagonalizing_gates + rotations) outcomes[t] = device.generate_samples()[0][mapped_wires] finally: device.shots = original_shots device._shot_vector = original_shot_vector # pylint: disable=protected-access return qml.math.cast(qml.math.stack([outcomes, recipes]), dtype=np.int8)
[docs] def process_state_with_shots( self, state: Sequence[complex], wire_order: Wires, shots: int, rng=None ): """Process the given quantum state with the given number of shots Args: state (Sequence[complex]): quantum state vector given as a rank-N tensor, where each dim has size 2 and N is the number of wires. wire_order (Wires): wires determining the subspace that ``state`` acts on; a matrix of dimension :math:`2^n` acts on a subspace of :math:`n` wires shots (int): The number of shots rng (Union[None, int, array_like[int], SeedSequence, BitGenerator, Generator]): A seed-like parameter matching that of ``seed`` for ``numpy.random.default_rng``. If no value is provided, a default RNG will be used. The random measurement outcomes in the form of bits will be generated from this argument, while the random recipes will be created from the ``seed`` argument provided to ``.ClassicalShadowsMP``. Returns: tensor_like[int]: A tensor with shape ``(2, T, n)``, where the first row represents the measured bits and the second represents the recipes used. """ wire_map = {w: i for i, w in enumerate(wire_order)} mapped_wires = [wire_map[w] for w in self.wires] n_qubits = len(mapped_wires) num_dev_qubits = len(state.shape) # seed the random measurement generation so that recipes # are the same for different executions with the same seed recipe_rng = np.random.RandomState(self.seed) recipes = recipe_rng.randint(0, 3, size=(shots, n_qubits)) bit_rng = np.random.default_rng(rng) obs_list = np.stack( [ qml.X.compute_matrix(), qml.Y.compute_matrix(), qml.Z.compute_matrix(), ] ) # the diagonalizing matrices corresponding to the Pauli observables above diag_list = np.stack( [ qml.Hadamard.compute_matrix(), qml.Hadamard.compute_matrix() @ qml.RZ.compute_matrix(-np.pi / 2), qml.Identity.compute_matrix(), ] ) obs = obs_list[recipes] diagonalizers = diag_list[recipes] # There's a significant speedup if we use the following iterative # process to perform the randomized Pauli measurements: # 1. Randomly generate Pauli observables for all snapshots for # a single qubit (e.g. the first qubit). # 2. Compute the expectation of each Pauli observable on the first # qubit by tracing out all other qubits. # 3. Sample the first qubit based on each Pauli expectation. # 4. For all snapshots, determine the collapsed state of the remaining # qubits based on the sample result. # 4. Repeat iteratively until no qubits are remaining. # # Observe that after the first iteration, the second qubit will become the # "first" qubit in the process. The advantage to this approach as opposed to # simulataneously computing the Pauli expectations for each qubit is that # the partial traces are computed over iteratively smaller subsystems, leading # to a significant speed-up. # transpose the state so that the measured wires appear first unmeasured_wires = [i for i in range(num_dev_qubits) if i not in mapped_wires] transposed_state = np.transpose(state, axes=mapped_wires + unmeasured_wires) outcomes = np.zeros((shots, n_qubits)) stacked_state = np.repeat(transposed_state[np.newaxis, ...], shots, axis=0) for active_qubit in range(n_qubits): # stacked_state loses a dimension each loop # trace out every qubit except the first num_remaining_qubits = num_dev_qubits - active_qubit conj_state_first_qubit = ABC[num_remaining_qubits] stacked_dim = ABC[num_remaining_qubits + 1] state_str = f"{stacked_dim}{ABC[:num_remaining_qubits]}" conj_state_str = f"{stacked_dim}{conj_state_first_qubit}{ABC[1:num_remaining_qubits]}" target_str = f"{stacked_dim}a{conj_state_first_qubit}" first_qubit_state = np.einsum( f"{state_str},{conj_state_str}->{target_str}", stacked_state, np.conj(stacked_state), ) # sample the observables on the first qubit probs = (np.einsum("abc,acb->a", first_qubit_state, obs[:, active_qubit]) + 1) / 2 samples = bit_rng.random(size=probs.shape) > probs outcomes[:, active_qubit] = samples # collapse the state of the remaining qubits; the next qubit in line # becomes the first qubit for the next iteration rotated_state = np.einsum( "ab...,acb->ac...", stacked_state, diagonalizers[:, active_qubit] ) stacked_state = rotated_state[np.arange(shots), samples.astype(np.int8)] # re-normalize the collapsed state sum_indices = tuple(range(1, num_remaining_qubits)) state_squared = np.abs(stacked_state) ** 2 norms = np.sqrt(np.sum(state_squared, sum_indices, keepdims=True)) stacked_state /= norms return np.stack([outcomes, recipes]).astype(np.int8)
@property def samples_computational_basis(self): return False @property def numeric_type(self): return int @property def return_type(self): return Shadow @classmethod def _abstract_eval( cls, n_wires: Optional[int] = None, has_eigvals=False, shots: Optional[int] = None, num_device_wires: int = 0, ) -> tuple: return (2, shots, n_wires), np.int8
[docs] def shape(self, shots: Optional[int] = None, num_device_wires: int = 0) -> tuple[int, int, int]: # otherwise, the return type requires a device if shots is None: raise MeasurementShapeError( "Shots must be specified to obtain the shape of a classical " "shadow measurement process." ) # the first entry of the tensor represents the measured bits, # and the second indicate the indices of the unitaries used return (2, shots, len(self.wires))
def __copy__(self): return self.__class__( seed=self.seed, wires=self._wires, )
[docs]class ShadowExpvalMP(MeasurementTransform): """Measures the expectation value of an operator using the classical shadow measurement process. Please refer to :func:`~pennylane.shadow_expval` for detailed documentation. Args: H (Operator, Sequence[Operator]): Operator or list of Operators to compute the expectation value over. seed (Union[int, None]): The seed used to generate the random measurements k (int): Number of equal parts to split the shadow's measurements to compute the median of means. ``k=1`` corresponds to simply taking the mean over all measurements. id (str): custom label given to a measurement instance, can be useful for some applications where the instance has to be identified """ def _flatten(self): metadata = ( ("seed", self.seed), ("k", self.k), ) return (self.H,), metadata @classmethod def _unflatten(cls, data, metadata): return cls(data[0], **dict(metadata)) def __init__( self, H: Union[Operator, Sequence], seed: Optional[int] = None, k: int = 1, id: Optional[str] = None, ): self.seed = seed self.H = H self.k = k super().__init__(id=id) # pylint: disable=arguments-differ @classmethod def _primitive_bind_call( cls, H: Union[Operator, Sequence], seed: Optional[int] = None, k: int = 1, **kwargs, ): if cls._obs_primitive is None: # pragma: no cover return type.__call__(cls, H=H, seed=seed, k=k, **kwargs) # pragma: no cover return cls._obs_primitive.bind(H, seed=seed, k=k, **kwargs)
[docs] def process(self, tape, device): bits, recipes = qml.classical_shadow(wires=self.wires, seed=self.seed).process(tape, device) shadow = qml.shadows.ClassicalShadow(bits, recipes, wire_map=self.wires.tolist()) return shadow.expval(self.H, self.k)
[docs] def process_state_with_shots( self, state: Sequence[complex], wire_order: Wires, shots: int, rng=None ): """Process the given quantum state with the given number of shots Args: state (Sequence[complex]): quantum state wire_order (Wires): wires determining the subspace that ``state`` acts on; a matrix of dimension :math:`2^n` acts on a subspace of :math:`n` wires shots (int): The number of shots rng (Union[None, int, array_like[int], SeedSequence, BitGenerator, Generator]): A seed-like parameter matching that of ``seed`` for ``numpy.random.default_rng``. If no value is provided, a default RNG will be used. Returns: float: The estimate of the expectation value. """ bits, recipes = qml.classical_shadow( wires=self.wires, seed=self.seed ).process_state_with_shots(state, wire_order, shots, rng=rng) shadow = qml.shadows.ClassicalShadow(bits, recipes, wire_map=self.wires.tolist()) return shadow.expval(self.H, self.k)
@property def samples_computational_basis(self): return False @property def numeric_type(self): return float @property def return_type(self): return ShadowExpval
[docs] def shape(self, shots: Optional[int] = None, num_device_wires: int = 0) -> tuple: return () if isinstance(self.H, Operator) else (len(self.H),)
@property def wires(self): r"""The wires the measurement process acts on. This is the union of all the Wires objects of the measurement. """ if isinstance(self.H, Iterable): return Wires.all_wires([h.wires for h in self.H]) return self.H.wires
[docs] def queue(self, context=qml.QueuingManager): """Append the measurement process to an annotated queue, making sure the observable is not queued""" Hs = self.H if isinstance(self.H, Iterable) else [self.H] for H in Hs: context.remove(H) context.append(self) return self
def __copy__(self): H_copy = ( [copy.copy(H) for H in self.H] if isinstance(self.H, Iterable) else copy.copy(self.H) ) return self.__class__( H=H_copy, k=self.k, seed=self.seed, )
if ShadowExpvalMP._obs_primitive is not None: # pylint: disable=protected-access @ShadowExpvalMP._obs_primitive.def_impl # pylint: disable=protected-access def _(H, **kwargs): return type.__call__(ShadowExpvalMP, H, **kwargs)