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 Sequence
from string import ascii_letters
import numpy as np
from pennylane import math
from pennylane.exceptions import MeasurementShapeError
from pennylane.operation import Operator
from pennylane.ops import RZ, Hadamard, I, X, Y, Z
from pennylane.queuing import QueuingManager
from pennylane.wires import Wires, WiresLike
from .measurements import MeasurementTransform
[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
"""
_shortname = "shadow"
def __init__(
self,
wires: WiresLike | None = None,
seed: int | None = None,
id: str | None = 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.devices.LegacyDevice): 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 = [X, Y, 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 math.cast(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 dimension 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. ``T`` is the number of shots,
and ``n`` is the number of qubits.
"""
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(
[
X.compute_matrix(),
Y.compute_matrix(),
Z.compute_matrix(),
]
)
# the diagonalizing matrices corresponding to the Pauli observables above
diag_list = np.stack(
[
Hadamard.compute_matrix(),
Hadamard.compute_matrix() @ RZ.compute_matrix(-np.pi / 2),
I.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 = ascii_letters[num_remaining_qubits]
stacked_dim = ascii_letters[num_remaining_qubits + 1]
state_str = f"{stacked_dim}{ascii_letters[:num_remaining_qubits]}"
conj_state_str = (
f"{stacked_dim}{conj_state_first_qubit}{ascii_letters[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)
[docs]
def process_density_matrix_with_shots(
self, state: Sequence[complex], wire_order: Wires, shots: int, rng=None
):
"""Process the given quantum state (density matrix) with the given number of shots
Args:
state (Sequence[complex]): quantum density matrix given as a rank-N tensor, where
each dim has size 2 and N is twice 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) // 2
# 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.array(
[
[[0, 1], [1, 0]], # X
[[0, -1j], [1j, 0]], # Y
[[1, 0], [0, -1]], # Z
]
)
# the diagonalizing matrices corresponding to the Pauli observables above
diag_list = np.array(
[
[[1 / np.sqrt(2), 1 / np.sqrt(2)], [1 / np.sqrt(2), -1 / np.sqrt(2)]],
[[0.5 + 0.5j, 0.5 - 0.5j], [0.5 + 0.5j, -0.5 + 0.5j]],
[[1, 0], [0, 1]],
]
)
obs = obs_list[recipes]
diagonalizers = diag_list[recipes]
# 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
+ [w + num_dev_qubits for w in mapped_wires]
+ [w + num_dev_qubits for w in 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 = ascii_letters[num_remaining_qubits]
stacked_dim = ascii_letters[num_remaining_qubits + 1]
remaining_dim = ascii_letters[:num_remaining_qubits]
traced_dim = remaining_dim[1:]
state_str = f"{stacked_dim}{remaining_dim}"
conj_state_str = f"{conj_state_first_qubit}{traced_dim}"
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,
)
# 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
U = diagonalizers[:, active_qubit]
UT = np.stack([math.conjugate(math.transpose(m)) for m in U])
# index labeling:
# (s, vL, a) (s, a, ..., b, ...) (s, b, vR) -> (s, vL, ..., vR, ...)
v_dim_left = ascii_letters[num_remaining_qubits + 2]
v_dim_right = ascii_letters[num_remaining_qubits + 3]
a_dagger_dim = ascii_letters[num_remaining_qubits + 4]
U_str = f"{stacked_dim}{v_dim_left}a"
rho_str = f"{stacked_dim}a{traced_dim}{a_dagger_dim}..."
UT_str = f"{stacked_dim}{a_dagger_dim}{v_dim_right}"
new_str = f"{stacked_dim}{v_dim_left}{traced_dim}{v_dim_right}..."
rotated_state = np.einsum(
f"{U_str}, {rho_str}, {UT_str}->{new_str}", U, stacked_state, UT
)
sampled_index = samples.astype(np.int8)
stacked_state = rotated_state[np.arange(shots), sampled_index]
stacked_state = np.stack(
[
np.take(stacked_state[i], sampled_index[i], axis=num_remaining_qubits - 1)
for i in range(shots)
]
)
# re-normalize the collapsed state
norms = np.einsum(
f"{stacked_dim}{traced_dim}{traced_dim}->{stacked_dim}", stacked_state
)
norms = norms.reshape(norms.shape + (1,) * (2 * num_remaining_qubits - 2))
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
@classmethod
def _abstract_eval(
cls,
n_wires: int | None = None,
has_eigvals=False,
shots: int | None = None,
num_device_wires: int = 0,
) -> tuple:
return (2, shots, n_wires), np.int8
[docs]
def shape(self, shots: int | None = 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
"""
_shortname = "shadowexpval"
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: Operator | Sequence[Operator],
seed: int | None = None,
k: int = 1,
id: str | None = 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: Operator | Sequence,
seed: int | None = 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):
from pennylane.shadows import ( # pylint: disable=import-outside-toplevel # tach-ignore
ClassicalShadow,
)
bits, recipes = classical_shadow(wires=self.wires, seed=self.seed).process(tape, device)
shadow = 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 = classical_shadow(wires=self.wires, seed=self.seed).process_state_with_shots(
state, wire_order, shots, rng=rng
)
# tach-ignore
from pennylane.shadows import ClassicalShadow # pylint:disable=import-outside-toplevel
shadow = ClassicalShadow(bits, recipes, wire_map=self.wires.tolist())
return shadow.expval(self.H, self.k)
[docs]
def process_density_matrix_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 = classical_shadow(
wires=self.wires, seed=self.seed
).process_density_matrix_with_shots(state, wire_order, shots, rng=rng)
# tach-ignore
from pennylane.shadows import ( # tach-ignore pylint: disable=import-outside-toplevel
ClassicalShadow,
)
shadow = 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
[docs]
def shape(self, shots: int | None = 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, Sequence):
return Wires.all_wires([h.wires for h in self.H])
return self.H.wires
[docs]
def queue(self, context=QueuingManager):
"""Append the measurement process to an annotated queue, making sure
the observable is not queued"""
# A CompositeOp is also an Sequence, but we should consider it to be a single observable
Hs = (
self.H
if isinstance(self.H, Sequence) and not isinstance(self.H, Operator)
else [self.H]
)
for H in Hs:
context.remove(H)
context.append(self)
return self
def __copy__(self):
# A CompositeOp is also an Sequence, we do not want to copy each operand of the op.
H_copy = (
[copy.copy(H) for H in self.H]
if isinstance(self.H, Sequence) and not isinstance(self.H, Operator)
else copy.copy(self.H)
)
return self.__class__(
H=H_copy,
k=self.k,
seed=self.seed,
)
[docs]
def shadow_expval(
H: Operator | Sequence[Operator], k: int = 1, seed: int | None = None
) -> ShadowExpvalMP:
r"""Estimate expectation values using Classical Shadows with full differentiability support.
The Classical Shadows protocol provide a way to estimate a large number of expectation values
(even non-commuting ones) using a single set of random Pauli measurements.
See `arXiv:2002.08953 <https://arxiv.org/abs/2002.08953>`_ for the original proposal and theoretical details.
Args:
H (Sequence[Operator] | Operator): Obserable(s) whose expectation values are to be estimated.
Provide a single observable or a sequence to estimate the expectation values of multiple
observables from the same classical shadows data.
k (int): Number of equal parts for which to split the shadow's measurements in order to compute the median of means.
The default is ``k=1``, which simply computes the mean of all measurements.
``k>1`` provides no expected advantage for Pauli measurements and Pauli observables.
seed (int | None): Optional seed for the random Pauli measurement basis in the
classical shadows protocol. This controls which bases (X, Y or Z) each qubit is measured
in per shot. If ``None``, a random seed will be generated.
.. note::
The ``seed`` argument only controls the measurement basis choice.
The ``seed`` of a simulator device separately controls the sampling outcomes.
For fully reproducible results, you must seed both the device and the measurement.
.. code-block:: python
dev = qml.device("default.qubit", seed=42, shots=100)
@qml.qnode(dev)
def circuit():
qml.H(0)
return qml.shadow_expval(qml.Z(0), seed=99)
Returns:
ShadowExpvalMP: Measurement process instance
.. seealso::
This measurement internally relies on the measurement :func:`~.pennylane.classical_shadow` and the class
:class:`~.pennylane.ClassicalShadow` for post-processing in order to compute expectation values.
**Example**
With the standard :func:`~.pennylane.expval` measurement, each group of non-commuting
observables requires its own separate circuit execution. However, with ``shadow_expval``
we can reuse the shadow data generated from the circuit executions to estimate all expectation values simultaneously.
Let's say we want to estimate the expectation values of all three (non-commuting) single qubit Paulis
(:class:`~.X`, :class:`~.Y`, :class:`~.Z`) on a :math:`| + \rangle` state.
Theoretically, we would expect that :math:`\langle X \rangle = 1`, :math:`\langle Y \rangle = \langle Z \rangle = 0`.
.. code-block:: python
device = qml.device("default.qubit", seed=42)
@qml.set_shots(1_000)
@qml.qnode(device)
def circuit():
qml.H(0) # Create |+> state
return qml.shadow_expval((qml.X(0), qml.Y(0), qml.Z(0)), seed=99)
>>> print(circuit())
[0.984 0. 0.03 ]
This is very close to their expected values!
.. details::
:title: Differentiability
Consider the following observable,
>>> H = qml.Hamiltonian([1., 1.], [qml.Z(0) @ qml.Z(1), qml.X(0) @ qml.X(1)])
We can estimate its expectation value with the classical shadows protocol:
.. code-block:: python
dev = qml.device("default.qubit", seed=42, wires=range(2))
@qml.set_shots(shots=10_000)
@qml.qnode(dev)
def circuit(x, obs):
qml.Hadamard(0)
qml.CNOT((0,1))
qml.RX(x, wires=0)
return qml.shadow_expval(obs, seed=99)
x = pnp.array(0.5, requires_grad=True)
>>> print(circuit(x, H))
1.8891
>>> print(qml.grad(circuit)(x, H))
-0.4653...
In ``shadow_expval``, we can also pass a list of observables to estimate them
all from the same shadow data.
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)]
>>> print(circuit(x, Hs))
[ 1.8783 0.0096 -0.0174 0.0138]
>>> print(qml.jacobian(circuit)(x, Hs))
[-0.4851 -0.0063 -0.0099 0.0006]
"""
seed = seed or np.random.randint(2**30)
return ShadowExpvalMP(H=H, seed=seed, k=k)
[docs]
def classical_shadow(wires: WiresLike, seed=None) -> ClassicalShadowMP:
"""
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:: python
dev = qml.device("default.qubit", seed=42, wires=2)
@qml.set_shots(shots=5)
@qml.qnode(dev)
def circuit():
qml.Hadamard(wires=0)
qml.CNOT(wires=[0, 1])
return qml.classical_shadow(wires=[0, 1], seed=42)
Executing this QNode produces the sampled bits and the Pauli measurements used:
>>> bits, recipes = circuit()
>>> bits
array([[1, 1],
[0, 0],
[1, 1],
[1, 0],
[0, 0]], dtype=int8)
>>> recipes
array([[2, 0],
[2, 2],
[0, 0],
[2, 1],
[2, 2]], dtype=int8)
.. 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
array([[0, 0],
[1, 1],
[1, 1],
[1, 1],
[0, 0]], dtype=int8)
>>> recipes
array([[2, 0],
[2, 2],
[0, 0],
[2, 1],
[2, 2]], dtype=int8)
To use the same Pauli recipes for different executions, the :class:`~.tape.QuantumTape`
interface should be used instead:
.. code-block:: python
dev = qml.device("default.qubit", wires=2)
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, diff_method=None)[0]
>>> bits2, recipes2 = qml.execute([tape], device=dev, diff_method=None)[0]
>>> print(np.all(recipes1 == recipes2))
True
>>> print(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:: python
dev = qml.device("default.qubit", wires=2)
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, diff_method=None)[0]
>>> bits2, recipes2 = qml.execute([tape2], device=dev, diff_method=None)[0]
>>> print(np.all(recipes1 == recipes2))
False
>>> print(np.all(bits1 == bits2))
False
"""
wires = Wires(wires)
seed = seed or np.random.randint(2**30)
return ClassicalShadowMP(wires=wires, seed=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)
_modules/pennylane/measurements/classical_shadow
Download Python script
Download Notebook
View on GitHub