From 5a65371cd43730ca105845d7b771cfca5c514611 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 09:18:01 +0100 Subject: [PATCH 01/37] Add process abstract method to MeasurementProcess class --- pennylane/measurements/measurements.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index c5abdf517db..7b2870bc15f 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -21,7 +21,9 @@ import contextlib import copy import functools +from abc import abstractmethod from enum import Enum +from typing import Sequence, Tuple import numpy as np @@ -692,3 +694,18 @@ def map_wires(self, wire_map: dict): if self.obs is not None: new_measurement.obs = self.obs.map_wires(wire_map=wire_map) return new_measurement + + @abstractmethod + def process( + self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None + ): + """_summary_ + + Args: + samples (Sequence[complex]): computational basis samples generated for all wires + shot_range (tuple[int]): 2-tuple of integers specifying the range of samples + to use. If not specified, all samples are used. + bin_size (int): Divides the shot range into bins of size ``bin_size``, and + returns the measurement statistic separately over each bin. If not + provided, the entire shot range is treated as a single bin. + """ From 22a20f6a4ca24422887480f481b945a9f46269e3 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 09:19:44 +0100 Subject: [PATCH 02/37] Add TODO comment. --- pennylane/measurements/measurements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index 7b2870bc15f..b1c4812ab76 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -108,7 +108,7 @@ class MeasurementShapeError(ValueError): quantum tape.""" -class MeasurementProcess: +class MeasurementProcess: # TODO: Inherit from ABC """Represents a measurement process occurring at the end of a quantum variational circuit. From 5ab2e4f2469f2c7ceaf7395439e79a052032e072 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 09:22:04 +0100 Subject: [PATCH 03/37] Add docstring --- pennylane/measurements/measurements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index b1c4812ab76..6e2a12f562c 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -699,7 +699,7 @@ def map_wires(self, wire_map: dict): def process( self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None ): - """_summary_ + """Process the given samples. Args: samples (Sequence[complex]): computational basis samples generated for all wires From f449a946339e7184f0ae75be4a19348580e759e7 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 10:17:18 +0100 Subject: [PATCH 04/37] Add _Sample class --- pennylane/measurements/measurements.py | 6 +-- pennylane/measurements/sample.py | 64 +++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index 6e2a12f562c..13ab8252702 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -23,12 +23,12 @@ import functools from abc import abstractmethod from enum import Enum -from typing import Sequence, Tuple +from typing import Sequence, Tuple, Union import numpy as np import pennylane as qml -from pennylane.operation import Operator +from pennylane.operation import Observable from pennylane.wires import Wires # ============================================================================= @@ -133,7 +133,7 @@ class MeasurementProcess: # TODO: Inherit from ABC def __init__( self, return_type: ObservableReturnTypes, - obs: Operator = None, + obs: Union[Observable, None] = None, wires=None, eigvals=None, id=None, diff --git a/pennylane/measurements/sample.py b/pennylane/measurements/sample.py index f609f5a718a..f37dd6baec5 100644 --- a/pennylane/measurements/sample.py +++ b/pennylane/measurements/sample.py @@ -16,13 +16,18 @@ This module contains the qml.sample measurement. """ import warnings +from typing import Sequence, Tuple, Union +import numpy as np + +import pennylane as qml +from pennylane.operation import Observable from pennylane.wires import Wires from .measurements import MeasurementProcess, Sample -def sample(op=None, wires=None): +def sample(op: Union[Observable, None] = None, wires=None): r"""Sample from the supplied observable, with the number of shots determined from the ``dev.shots`` attribute of the corresponding device, returning raw samples. If no observable is provided then basis state samples are returned @@ -106,4 +111,59 @@ def circuit(x): ) wires = Wires(wires) - return MeasurementProcess(Sample, obs=op, wires=wires) + return _Sample(Sample, obs=op, wires=wires) + + +class _Sample(MeasurementProcess): + """Measurement process that returns the samples of a given observable.""" + + def process( + self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None + ): + name = getattr(self.obs, "name", None) + # Select the samples from samples that correspond to ``shot_range`` if provided + if shot_range is None: + sub_samples = samples + else: + # Indexing corresponds to: (potential broadcasting, shots, wires). Note that the last + # colon (:) is required because shots is the second-to-last axis and the + # Ellipsis (...) otherwise would take up broadcasting and shots axes. + sub_samples = samples[..., slice(*shot_range), :] + + if self.obs is None: + # if no observable was provided then return the raw samples + if len(self.wires) != 0: + # if wires are provided, then we only return samples from those wires + samples = sub_samples[..., np.array(self.wires)] + else: + samples = sub_samples + + elif isinstance(name, str) and name in {"PauliX", "PauliY", "PauliZ", "Hadamard"}: + # Process samples for observables with eigenvalues {1, -1} + samples = 1 - 2 * sub_samples[..., self.wires[0]] + + else: + + # Replace the basis state in the computational basis with the correct eigenvalue. + # Extract only the columns of the basis samples required based on ``wires``. + samples = sub_samples[..., np.array(self.wires)] # Add np.array here for Jax support. + powers_of_two = 2 ** np.arange(samples.shape[-1])[::-1] + indices = samples @ powers_of_two + indices = np.array(indices) # Add np.array here for Jax support. + try: + samples = self.obs.eigvals()[indices] + except qml.operation.EigvalsUndefinedError as e: + # if observable has no info on eigenvalues, we cannot return this measurement + raise qml.operation.EigvalsUndefinedError( + f"Cannot compute samples of {self.obs.name}." + ) from e + + num_wires = len(self.wires) if len(self.wires) > 0 else Ellipsis + if bin_size is None: + return samples + + return ( + samples.reshape((num_wires, bin_size, -1)) + if self.obs is None + else samples.reshape((bin_size, -1)) + ) From 5c1f35595b7060b9a160b9e2a8de9c453c211d09 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 10:20:34 +0100 Subject: [PATCH 05/37] Add process method to ClassicalShadow class --- pennylane/measurements/classical_shadow.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pennylane/measurements/classical_shadow.py b/pennylane/measurements/classical_shadow.py index f0bd1c09bc6..a9d4dee853c 100644 --- a/pennylane/measurements/classical_shadow.py +++ b/pennylane/measurements/classical_shadow.py @@ -16,6 +16,7 @@ This module contains the qml.classical_shadow measurement. """ from collections.abc import Iterable +from typing import Sequence, Tuple import numpy as np @@ -293,3 +294,8 @@ def __copy__(self): obj.H = self.H obj.k = self.k return obj + + def process( + self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None + ): + pass From 92d6dccf38743b2ed7df447952dafddba7daa3f1 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 12:51:22 +0100 Subject: [PATCH 06/37] Add tests --- pennylane/measurements/sample.py | 20 +++---- tests/measurements/test_sample.py | 89 ++++++++++++++++++++----------- 2 files changed, 65 insertions(+), 44 deletions(-) diff --git a/pennylane/measurements/sample.py b/pennylane/measurements/sample.py index f37dd6baec5..027fd6aabab 100644 --- a/pennylane/measurements/sample.py +++ b/pennylane/measurements/sample.py @@ -16,7 +16,7 @@ This module contains the qml.sample measurement. """ import warnings -from typing import Sequence, Tuple, Union +from typing import Tuple, Union import numpy as np @@ -117,9 +117,7 @@ def circuit(x): class _Sample(MeasurementProcess): """Measurement process that returns the samples of a given observable.""" - def process( - self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None - ): + def process(self, samples: np.ndarray, shot_range: Tuple[int] = None, bin_size: int = None): name = getattr(self.obs, "name", None) # Select the samples from samples that correspond to ``shot_range`` if provided if shot_range is None: @@ -137,8 +135,10 @@ def process( samples = sub_samples[..., np.array(self.wires)] else: samples = sub_samples + num_wires = qml.math.shape(samples)[-1] + return samples if bin_size is None else samples.reshape(num_wires, bin_size, -1) - elif isinstance(name, str) and name in {"PauliX", "PauliY", "PauliZ", "Hadamard"}: + if isinstance(name, str) and name in {"PauliX", "PauliY", "PauliZ", "Hadamard"}: # Process samples for observables with eigenvalues {1, -1} samples = 1 - 2 * sub_samples[..., self.wires[0]] @@ -158,12 +158,4 @@ def process( f"Cannot compute samples of {self.obs.name}." ) from e - num_wires = len(self.wires) if len(self.wires) > 0 else Ellipsis - if bin_size is None: - return samples - - return ( - samples.reshape((num_wires, bin_size, -1)) - if self.obs is None - else samples.reshape((bin_size, -1)) - ) + return samples if bin_size is None else samples.reshape((bin_size, -1)) diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index 0bacd921be7..5f0eda1a803 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -18,18 +18,38 @@ import pennylane as qml from pennylane.measurements import Sample +from pennylane.operation import Observable class TestSample: """Tests for the sample function""" - def test_sample_dimension(self): + # TODO: Remove this when new CustomMP are the default + def teardown_method(self): + """Method called at the end of every test. It loops over all the calls to + QubitDevice.sample and compares its output with the new _Sample.process method.""" + if not getattr(self, "circuit", False): + return + samples = self.dev._samples + for call_args in self.spy.call_args_list: + meas = call_args.args[0] + shot_range, bin_size = (call_args.kwargs["shot_range"], call_args.kwargs["bin_size"]) + if isinstance(meas, Observable): + meas = qml.sample(op=meas) + assert qml.math.allequal( + self.dev.sample(*call_args.args, **call_args.kwargs), + meas.process(samples=samples, shot_range=shot_range, bin_size=bin_size), + ) + + def test_sample_dimension(self, mocker): """Test that the sample function outputs samples of the right size""" n_sample = 10 - dev = qml.device("default.qubit", wires=2, shots=n_sample) + self.dev = qml.device("default.qubit", wires=2, shots=n_sample) - @qml.qnode(dev) + self.spy = mocker.spy(self.dev, "sample") + + @qml.qnode(self.dev) def circuit(): qml.RX(0.54, wires=0) return qml.sample(qml.PauliZ(0)), qml.sample(qml.PauliX(1)) @@ -39,13 +59,14 @@ def circuit(): assert np.array_equal(sample.shape, (2, n_sample)) @pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences") - def test_sample_combination(self): + def test_sample_combination(self, mocker): """Test the output of combining expval, var and sample""" n_sample = 10 - dev = qml.device("default.qubit", wires=3, shots=n_sample) + self.dev = qml.device("default.qubit", wires=3, shots=n_sample) + self.spy = mocker.spy(self.dev, "sample") - @qml.qnode(dev, diff_method="parameter-shift") + @qml.qnode(self.dev, diff_method="parameter-shift") def circuit(): qml.RX(0.54, wires=0) @@ -58,13 +79,14 @@ def circuit(): assert isinstance(result[1], np.ndarray) assert isinstance(result[2], np.ndarray) - def test_single_wire_sample(self): + def test_single_wire_sample(self, mocker): """Test the return type and shape of sampling a single wire""" n_sample = 10 - dev = qml.device("default.qubit", wires=1, shots=n_sample) + self.dev = qml.device("default.qubit", wires=1, shots=n_sample) + self.spy = mocker.spy(self.dev, "sample") - @qml.qnode(dev) + @qml.qnode(self.dev) def circuit(): qml.RX(0.54, wires=0) @@ -75,14 +97,15 @@ def circuit(): assert isinstance(result, np.ndarray) assert np.array_equal(result.shape, (n_sample,)) - def test_multi_wire_sample_regular_shape(self): + def test_multi_wire_sample_regular_shape(self, mocker): """Test the return type and shape of sampling multiple wires where a rectangular array is expected""" n_sample = 10 - dev = qml.device("default.qubit", wires=3, shots=n_sample) + self.dev = qml.device("default.qubit", wires=3, shots=n_sample) + self.spy = mocker.spy(self.dev, "sample") - @qml.qnode(dev) + @qml.qnode(self.dev) def circuit(): return qml.sample(qml.PauliZ(0)), qml.sample(qml.PauliZ(1)), qml.sample(qml.PauliZ(2)) @@ -94,14 +117,15 @@ def circuit(): assert result.dtype == np.dtype("int") @pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences") - def test_sample_output_type_in_combination(self): + def test_sample_output_type_in_combination(self, mocker): """Test the return type and shape of sampling multiple works in combination with expvals and vars""" n_sample = 10 - dev = qml.device("default.qubit", wires=3, shots=n_sample) + self.dev = qml.device("default.qubit", wires=3, shots=n_sample) + self.spy = mocker.spy(self.dev, "sample") - @qml.qnode(dev, diff_method="parameter-shift") + @qml.qnode(self.dev, diff_method="parameter-shift") def circuit(): return qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(1)), qml.sample(qml.PauliZ(2)) @@ -114,12 +138,13 @@ def circuit(): assert result[2].dtype == np.dtype("int") assert np.array_equal(result[2].shape, (n_sample,)) - def test_not_an_observable(self): + def test_not_an_observable(self, mocker): """Test that a UserWarning is raised if the provided argument might not be hermitian.""" - dev = qml.device("default.qubit", wires=2, shots=10) + self.dev = qml.device("default.qubit", wires=2, shots=10) + self.spy = mocker.spy(self.dev, "sample") - @qml.qnode(dev) + @qml.qnode(self.dev) def circuit(): qml.RX(0.52, wires=0) return qml.sample(qml.prod(qml.PauliX(0), qml.PauliZ(0))) @@ -127,12 +152,13 @@ def circuit(): with pytest.warns(UserWarning, match="Prod might not be hermitian."): _ = circuit() - def test_observable_return_type_is_sample(self): + def test_observable_return_type_is_sample(self, mocker): """Test that the return type of the observable is :attr:`ObservableReturnTypes.Sample`""" n_shots = 10 - dev = qml.device("default.qubit", wires=1, shots=n_shots) + self.dev = qml.device("default.qubit", wires=1, shots=n_shots) + self.spy = mocker.spy(self.dev, "sample") - @qml.qnode(dev) + @qml.qnode(self.dev) def circuit(): res = qml.sample(qml.PauliZ(0)) assert res.return_type is Sample @@ -156,11 +182,12 @@ def circuit(): ): _ = circuit() - def test_providing_no_observable_and_no_wires(self): + def test_providing_no_observable_and_no_wires(self, mocker): """Test that we can provide no observable and no wires to sample function""" - dev = qml.device("default.qubit", wires=2, shots=1000) + self.dev = qml.device("default.qubit", wires=2, shots=1000) + self.spy = mocker.spy(self.dev, "sample") - @qml.qnode(dev) + @qml.qnode(self.dev) def circuit(): qml.Hadamard(wires=0) res = qml.sample() @@ -170,7 +197,7 @@ def circuit(): circuit() - def test_providing_no_observable_and_no_wires_shot_vector(self): + def test_providing_no_observable_and_no_wires_shot_vector(self, mocker): """Test that we can provide no observable and no wires to sample function when using a shot vector""" num_wires = 2 @@ -178,9 +205,10 @@ def test_providing_no_observable_and_no_wires_shot_vector(self): shots1 = 1 shots2 = 10 shots3 = 1000 - dev = qml.device("default.qubit", wires=num_wires, shots=[shots1, shots2, shots3]) + self.dev = qml.device("default.qubit", wires=num_wires, shots=[shots1, shots2, shots3]) + self.spy = mocker.spy(self.dev, "sample") - @qml.qnode(dev) + @qml.qnode(self.dev) def circuit(): qml.Hadamard(wires=0) return qml.sample() @@ -193,13 +221,14 @@ def circuit(): assert len(res) == len(expected_shapes) assert (r.shape == exp_shape for r, exp_shape in zip(res, expected_shapes)) - def test_providing_no_observable_and_wires(self): + def test_providing_no_observable_and_wires(self, mocker): """Test that we can provide no observable but specify wires to the sample function""" wires = [0, 2] wires_obj = qml.wires.Wires(wires) - dev = qml.device("default.qubit", wires=3, shots=1000) + self.dev = qml.device("default.qubit", wires=3, shots=1000) + self.spy = mocker.spy(self.dev, "sample") - @qml.qnode(dev) + @qml.qnode(self.dev) def circuit(): qml.Hadamard(wires=0) res = qml.sample(wires=wires) From 69a1e0ae792c19e408dba7ddc5b4c64f9c929094 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 12:57:14 +0100 Subject: [PATCH 07/37] Fix coverage --- pennylane/measurements/classical_shadow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pennylane/measurements/classical_shadow.py b/pennylane/measurements/classical_shadow.py index a9d4dee853c..7fd87b238fb 100644 --- a/pennylane/measurements/classical_shadow.py +++ b/pennylane/measurements/classical_shadow.py @@ -298,4 +298,4 @@ def __copy__(self): def process( self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None ): - pass + """TODO: Implement this method.""" From 128a3bf8d68ef6b597bd3705101f7c464212ce9e Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 12:59:10 +0100 Subject: [PATCH 08/37] TODO --- pennylane/measurements/sample.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pennylane/measurements/sample.py b/pennylane/measurements/sample.py index 027fd6aabab..6c170f761d1 100644 --- a/pennylane/measurements/sample.py +++ b/pennylane/measurements/sample.py @@ -114,6 +114,7 @@ def circuit(x): return _Sample(Sample, obs=op, wires=wires) +# TODO: Make public when removing the ObservableReturnTypes enum class _Sample(MeasurementProcess): """Measurement process that returns the samples of a given observable.""" From 53a2766e3ff5a4066da25ce873a466fb34f66ce8 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 13:27:42 +0100 Subject: [PATCH 09/37] Fix test --- tests/measurements/test_sample.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index 5f0eda1a803..ad3c7f05cbd 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -18,7 +18,7 @@ import pennylane as qml from pennylane.measurements import Sample -from pennylane.operation import Observable +from pennylane.operation import Operator class TestSample: @@ -28,13 +28,13 @@ class TestSample: def teardown_method(self): """Method called at the end of every test. It loops over all the calls to QubitDevice.sample and compares its output with the new _Sample.process method.""" - if not getattr(self, "circuit", False): + if not getattr(self, "spy", False): return samples = self.dev._samples for call_args in self.spy.call_args_list: meas = call_args.args[0] shot_range, bin_size = (call_args.kwargs["shot_range"], call_args.kwargs["bin_size"]) - if isinstance(meas, Observable): + if isinstance(meas, Operator): meas = qml.sample(op=meas) assert qml.math.allequal( self.dev.sample(*call_args.args, **call_args.kwargs), From 89280694db2bb24c0988e255804ab393c0500c90 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 14:53:26 +0100 Subject: [PATCH 10/37] Fix equal --- pennylane/ops/functions/equal.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py index 2134b436768..3ef4443e2e8 100644 --- a/pennylane/ops/functions/equal.py +++ b/pennylane/ops/functions/equal.py @@ -16,6 +16,7 @@ """ # pylint: disable=too-many-arguments,too-many-return-statements from typing import Union + import pennylane as qml from pennylane.measurements import MeasurementProcess, ShadowMeasurementProcess from pennylane.operation import Operator @@ -85,12 +86,12 @@ def equal( if op1.__class__ is not op2.__class__: return False - if op1.__class__ is MeasurementProcess: - return equal_measurements(op1, op2) - - if op1.__class__ is ShadowMeasurementProcess: + if isinstance(op1, ShadowMeasurementProcess): return equal_shadow_measurements(op1, op2) + if isinstance(op1, MeasurementProcess): + return equal_measurements(op1, op2) + return equal_operator(op1, op2, check_interface, check_trainability, rtol, atol) From 4428bd153a9275fcb3f6a71b96d725b71a162555 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 15:53:17 +0100 Subject: [PATCH 11/37] Fix test --- pennylane/circuit_graph.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 3a858704cb9..6df2a9d3dae 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -18,11 +18,10 @@ # pylint: disable=too-many-branches,too-many-arguments,too-many-instance-attributes from collections import namedtuple -import retworkx as rx import numpy as np +import retworkx as rx import pennylane as qml - from pennylane.wires import Wires @@ -125,7 +124,7 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): elif op.return_type is qml.measurements.Sample and op.wires == Wires([]): # Sampling without specifying wires is treated as sampling all wires - op = qml.measurements.MeasurementProcess(qml.measurements.Sample, wires=wires) + op = qml.sample(wires=wires) op.queue_idx = k From ff9901adf5bdc59fb33e03eea4d79681965ca143 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 16:15:37 +0100 Subject: [PATCH 12/37] Support python3.7 --- tests/measurements/test_sample.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index ad3c7f05cbd..f9659f77162 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -32,8 +32,8 @@ def teardown_method(self): return samples = self.dev._samples for call_args in self.spy.call_args_list: - meas = call_args.args[0] - shot_range, bin_size = (call_args.kwargs["shot_range"], call_args.kwargs["bin_size"]) + meas = call_args[0][0] + shot_range, bin_size = (call_args[1]["shot_range"], call_args[1]["bin_size"]) if isinstance(meas, Operator): meas = qml.sample(op=meas) assert qml.math.allequal( From 1b4c96a3396f48deb0098b195838d60012ec744c Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Tue, 8 Nov 2022 16:42:06 +0100 Subject: [PATCH 13/37] Revert --- tests/measurements/test_sample.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index f9659f77162..ad3c7f05cbd 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -32,8 +32,8 @@ def teardown_method(self): return samples = self.dev._samples for call_args in self.spy.call_args_list: - meas = call_args[0][0] - shot_range, bin_size = (call_args[1]["shot_range"], call_args[1]["bin_size"]) + meas = call_args.args[0] + shot_range, bin_size = (call_args.kwargs["shot_range"], call_args.kwargs["bin_size"]) if isinstance(meas, Operator): meas = qml.sample(op=meas) assert qml.math.allequal( From 357ecbb4704b096a28eb898642a3ecbac354cdec Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Wed, 9 Nov 2022 11:37:15 +0100 Subject: [PATCH 14/37] Skip python@3.7 test --- tests/measurements/test_sample.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index ad3c7f05cbd..59356b0b38f 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -13,6 +13,8 @@ # limitations under the License. """Unit tests for the sample module""" +import sys + import numpy as np import pytest @@ -30,6 +32,8 @@ def teardown_method(self): QubitDevice.sample and compares its output with the new _Sample.process method.""" if not getattr(self, "spy", False): return + if sys.version_info[1] <= 7: + return # skip tests for python@3.7 because call_args.kwargs is a tuple instead of a dict samples = self.dev._samples for call_args in self.spy.call_args_list: meas = call_args.args[0] From 8e746beffdbf6038d6e06fe5e13abc307048121d Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Wed, 9 Nov 2022 11:48:23 +0100 Subject: [PATCH 15/37] Improve sample code --- pennylane/measurements/sample.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/pennylane/measurements/sample.py b/pennylane/measurements/sample.py index 6c170f761d1..485dc9136cb 100644 --- a/pennylane/measurements/sample.py +++ b/pennylane/measurements/sample.py @@ -119,36 +119,31 @@ class _Sample(MeasurementProcess): """Measurement process that returns the samples of a given observable.""" def process(self, samples: np.ndarray, shot_range: Tuple[int] = None, bin_size: int = None): - name = getattr(self.obs, "name", None) + name = self.obs.name if self.obs is not None else None # Select the samples from samples that correspond to ``shot_range`` if provided - if shot_range is None: - sub_samples = samples - else: + if shot_range is not None: # Indexing corresponds to: (potential broadcasting, shots, wires). Note that the last # colon (:) is required because shots is the second-to-last axis and the # Ellipsis (...) otherwise would take up broadcasting and shots axes. - sub_samples = samples[..., slice(*shot_range), :] + samples = samples[..., slice(*shot_range), :] + + if len(self.wires) != 0: + # if wires are provided, then we only return samples from those wires + samples = samples[..., np.array(self.wires)] + + num_wires = samples.shape[-1] # wires is the last dimension if self.obs is None: # if no observable was provided then return the raw samples - if len(self.wires) != 0: - # if wires are provided, then we only return samples from those wires - samples = sub_samples[..., np.array(self.wires)] - else: - samples = sub_samples - num_wires = qml.math.shape(samples)[-1] return samples if bin_size is None else samples.reshape(num_wires, bin_size, -1) - if isinstance(name, str) and name in {"PauliX", "PauliY", "PauliZ", "Hadamard"}: + if name in {"PauliX", "PauliY", "PauliZ", "Hadamard"}: # Process samples for observables with eigenvalues {1, -1} - samples = 1 - 2 * sub_samples[..., self.wires[0]] - + samples = 1 - 2 * qml.math.squeeze(samples) else: - # Replace the basis state in the computational basis with the correct eigenvalue. # Extract only the columns of the basis samples required based on ``wires``. - samples = sub_samples[..., np.array(self.wires)] # Add np.array here for Jax support. - powers_of_two = 2 ** np.arange(samples.shape[-1])[::-1] + powers_of_two = 2 ** np.arange(num_wires)[::-1] indices = samples @ powers_of_two indices = np.array(indices) # Add np.array here for Jax support. try: From 4bdcf38ebb4256c337b8ab38226a803a04f16b4f Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Wed, 9 Nov 2022 12:46:29 +0100 Subject: [PATCH 16/37] Coverage --- tests/measurements/test_sample.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index 59356b0b38f..2c29823bdd8 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -20,7 +20,7 @@ import pennylane as qml from pennylane.measurements import Sample -from pennylane.operation import Operator +from pennylane.operation import EigvalsUndefinedError, Operator class TestSample: @@ -322,3 +322,12 @@ def circuit(): assert isinstance(binned_samples, tuple) assert len(binned_samples) == len(shot_vec) assert binned_samples[0].shape == (shot_vec[0],) + + def test_new_sample_with_operator_with_no_eigvals(self): + """Test that calling process with an operator that has no eigvals defined raises an error.""" + + class DummyOp(Operator): + num_wires = 1 + + with pytest.raises(EigvalsUndefinedError, match="Cannot compute samples of"): + qml.sample(op=DummyOp(0)).process(samples=np.array([[1, 0]])) From 595e1a45c852358dbf3ddaa1c825784eedc56d92 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Thu, 10 Nov 2022 12:43:49 +0100 Subject: [PATCH 17/37] Improve tests --- tests/measurements/test_sample.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index 2c29823bdd8..e3c8a81809c 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -34,14 +34,17 @@ def teardown_method(self): return if sys.version_info[1] <= 7: return # skip tests for python@3.7 because call_args.kwargs is a tuple instead of a dict + + assert len(self.spy.call_args_list) > 0 # make sure method is mocked properly + samples = self.dev._samples for call_args in self.spy.call_args_list: - meas = call_args.args[0] + meas = call_args.args[1] shot_range, bin_size = (call_args.kwargs["shot_range"], call_args.kwargs["bin_size"]) if isinstance(meas, Operator): meas = qml.sample(op=meas) assert qml.math.allequal( - self.dev.sample(*call_args.args, **call_args.kwargs), + self.dev.sample(call_args.args[1], **call_args.kwargs), meas.process(samples=samples, shot_range=shot_range, bin_size=bin_size), ) @@ -50,8 +53,7 @@ def test_sample_dimension(self, mocker): n_sample = 10 self.dev = qml.device("default.qubit", wires=2, shots=n_sample) - - self.spy = mocker.spy(self.dev, "sample") + self.spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(self.dev) def circuit(): @@ -68,7 +70,7 @@ def test_sample_combination(self, mocker): n_sample = 10 self.dev = qml.device("default.qubit", wires=3, shots=n_sample) - self.spy = mocker.spy(self.dev, "sample") + self.spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(self.dev, diff_method="parameter-shift") def circuit(): @@ -88,7 +90,7 @@ def test_single_wire_sample(self, mocker): n_sample = 10 self.dev = qml.device("default.qubit", wires=1, shots=n_sample) - self.spy = mocker.spy(self.dev, "sample") + self.spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(self.dev) def circuit(): @@ -107,7 +109,7 @@ def test_multi_wire_sample_regular_shape(self, mocker): n_sample = 10 self.dev = qml.device("default.qubit", wires=3, shots=n_sample) - self.spy = mocker.spy(self.dev, "sample") + self.spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(self.dev) def circuit(): @@ -127,7 +129,7 @@ def test_sample_output_type_in_combination(self, mocker): n_sample = 10 self.dev = qml.device("default.qubit", wires=3, shots=n_sample) - self.spy = mocker.spy(self.dev, "sample") + self.spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(self.dev, diff_method="parameter-shift") def circuit(): @@ -146,7 +148,7 @@ def test_not_an_observable(self, mocker): """Test that a UserWarning is raised if the provided argument might not be hermitian.""" self.dev = qml.device("default.qubit", wires=2, shots=10) - self.spy = mocker.spy(self.dev, "sample") + self.spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(self.dev) def circuit(): @@ -160,7 +162,7 @@ def test_observable_return_type_is_sample(self, mocker): """Test that the return type of the observable is :attr:`ObservableReturnTypes.Sample`""" n_shots = 10 self.dev = qml.device("default.qubit", wires=1, shots=n_shots) - self.spy = mocker.spy(self.dev, "sample") + self.spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(self.dev) def circuit(): @@ -189,7 +191,7 @@ def circuit(): def test_providing_no_observable_and_no_wires(self, mocker): """Test that we can provide no observable and no wires to sample function""" self.dev = qml.device("default.qubit", wires=2, shots=1000) - self.spy = mocker.spy(self.dev, "sample") + self.spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(self.dev) def circuit(): @@ -210,7 +212,7 @@ def test_providing_no_observable_and_no_wires_shot_vector(self, mocker): shots2 = 10 shots3 = 1000 self.dev = qml.device("default.qubit", wires=num_wires, shots=[shots1, shots2, shots3]) - self.spy = mocker.spy(self.dev, "sample") + self.spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(self.dev) def circuit(): @@ -230,7 +232,7 @@ def test_providing_no_observable_and_wires(self, mocker): wires = [0, 2] wires_obj = qml.wires.Wires(wires) self.dev = qml.device("default.qubit", wires=3, shots=1000) - self.spy = mocker.spy(self.dev, "sample") + self.spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(self.dev) def circuit(): From e37dcee78bb81919a56ab302932db3e0aa952c5e Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Thu, 10 Nov 2022 13:01:22 +0100 Subject: [PATCH 18/37] Add process_state method --- pennylane/measurements/measurements.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index 6e2a12f562c..9b0f118d18c 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -21,7 +21,6 @@ import contextlib import copy import functools -from abc import abstractmethod from enum import Enum from typing import Sequence, Tuple @@ -108,7 +107,7 @@ class MeasurementShapeError(ValueError): quantum tape.""" -class MeasurementProcess: # TODO: Inherit from ABC +class MeasurementProcess: """Represents a measurement process occurring at the end of a quantum variational circuit. @@ -695,7 +694,6 @@ def map_wires(self, wire_map: dict): new_measurement.obs = self.obs.map_wires(wire_map=wire_map) return new_measurement - @abstractmethod def process( self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None ): @@ -709,3 +707,18 @@ def process( returns the measurement statistic separately over each bin. If not provided, the entire shot range is treated as a single bin. """ + raise NotImplementedError(f"The class {self.__class__} cannot process samples.") + + def process_state(self, state: np.ndarray, device_wires: Wires): + """Process the given quantum state. + + Args: + state (ndarray[complex]): quantum state + num_wires (int): total number of wires + shot_range (tuple[int]): 2-tuple of integers specifying the range of samples + to use. If not specified, all samples are used. + bin_size (int): Divides the shot range into bins of size ``bin_size``, and + returns the measurement statistic separately over each bin. If not + provided, the entire shot range is treated as a single bin. + """ + raise NotImplementedError(f"The class {self.__class__} cannot process quantum states.") From 50ec15207e733be28f6f0d971aad3d0ce4e73204 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Thu, 10 Nov 2022 13:18:58 +0100 Subject: [PATCH 19/37] Remove abstract --- pennylane/measurements/classical_shadow.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pennylane/measurements/classical_shadow.py b/pennylane/measurements/classical_shadow.py index 7fd87b238fb..52160ac06bd 100644 --- a/pennylane/measurements/classical_shadow.py +++ b/pennylane/measurements/classical_shadow.py @@ -299,3 +299,6 @@ def process( self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None ): """TODO: Implement this method.""" + + def process_state(self, state: np.ndarray, device_wires: Wires): + """TODO: Implement this method.""" From 9354df8e220a6ad19a3868b2babb1e49613d4b9d Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Thu, 10 Nov 2022 14:28:28 +0100 Subject: [PATCH 20/37] Coverage --- tests/measurements/test_measurements.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/measurements/test_measurements.py b/tests/measurements/test_measurements.py index 62306158149..9cb892f8fbc 100644 --- a/tests/measurements/test_measurements.py +++ b/tests/measurements/test_measurements.py @@ -88,6 +88,16 @@ def test_shape_unrecognized_error(): mp.shape() +def test_process_methods(): + """Test that ``process`` and ``process_state`` raise an error.""" + mp = MeasurementProcess("test") + with pytest.raises(NotImplementedError): + mp.process(samples=[]) + + with pytest.raises(NotImplementedError): + mp.process_state(state=[], device_wires=[]) + + @pytest.mark.parametrize( "stat_func,return_type", [(expval, Expectation), (var, Variance), (sample, Sample)] ) From 478d2684bccf2d03c73b1330c35b59631182b02e Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Thu, 10 Nov 2022 15:40:01 +0100 Subject: [PATCH 21/37] Add StateMeasurement and SampleMeasurement classes --- pennylane/measurements/__init__.py | 8 +++++++- pennylane/measurements/measurements.py | 13 +++++++++++-- tests/measurements/test_measurements.py | 12 ++---------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/pennylane/measurements/__init__.py b/pennylane/measurements/__init__.py index 9a5e8ca1c3b..0ef4582626d 100644 --- a/pennylane/measurements/__init__.py +++ b/pennylane/measurements/__init__.py @@ -23,7 +23,13 @@ ) from .counts import AllCounts, Counts, counts from .expval import Expectation, expval -from .measurements import MeasurementProcess, MeasurementShapeError, ObservableReturnTypes +from .measurements import ( + MeasurementProcess, + MeasurementShapeError, + ObservableReturnTypes, + SampleMeasurement, + StateMeasurement, +) from .mid_measure import MeasurementValue, MeasurementValueError, MidMeasure, measure from .mutual_info import MutualInfo, mutual_info from .probs import Probability, probs diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index 0d95049cbf1..9ed65862d6c 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -21,6 +21,7 @@ import contextlib import copy import functools +from abc import ABC, abstractmethod from enum import Enum from typing import Sequence, Tuple @@ -695,6 +696,11 @@ def map_wires(self, wire_map: dict): new_measurement._wires = Wires([wire_map.get(wire, wire) for wire in self.wires]) return new_measurement + +class SampleMeasurement(MeasurementProcess, ABC): + """Sample-based measurement process.""" + + @abstractmethod def process( self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None ): @@ -708,8 +714,12 @@ def process( returns the measurement statistic separately over each bin. If not provided, the entire shot range is treated as a single bin. """ - raise NotImplementedError(f"The class {self.__class__} cannot process samples.") + +class StateMeasurement(MeasurementProcess, ABC): + """State-based measurement process.""" + + @abstractmethod def process_state(self, state: np.ndarray, device_wires: Wires): """Process the given quantum state. @@ -722,4 +732,3 @@ def process_state(self, state: np.ndarray, device_wires: Wires): returns the measurement statistic separately over each bin. If not provided, the entire shot range is treated as a single bin. """ - raise NotImplementedError(f"The class {self.__class__} cannot process quantum states.") diff --git a/tests/measurements/test_measurements.py b/tests/measurements/test_measurements.py index 9cb892f8fbc..07720444eea 100644 --- a/tests/measurements/test_measurements.py +++ b/tests/measurements/test_measurements.py @@ -25,9 +25,11 @@ MutualInfo, Probability, Sample, + SampleMeasurement, Shadow, ShadowExpval, State, + StateMeasurement, Variance, VnEntropy, expval, @@ -88,16 +90,6 @@ def test_shape_unrecognized_error(): mp.shape() -def test_process_methods(): - """Test that ``process`` and ``process_state`` raise an error.""" - mp = MeasurementProcess("test") - with pytest.raises(NotImplementedError): - mp.process(samples=[]) - - with pytest.raises(NotImplementedError): - mp.process_state(state=[], device_wires=[]) - - @pytest.mark.parametrize( "stat_func,return_type", [(expval, Expectation), (var, Variance), (sample, Sample)] ) From c1df52a895ee68aee4f69738e2a3e8cf716c9506 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Thu, 10 Nov 2022 15:43:08 +0100 Subject: [PATCH 22/37] Change inheritance --- pennylane/measurements/sample.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/measurements/sample.py b/pennylane/measurements/sample.py index 485dc9136cb..fbfe39f5d42 100644 --- a/pennylane/measurements/sample.py +++ b/pennylane/measurements/sample.py @@ -24,7 +24,7 @@ from pennylane.operation import Observable from pennylane.wires import Wires -from .measurements import MeasurementProcess, Sample +from .measurements import Sample, SampleMeasurement def sample(op: Union[Observable, None] = None, wires=None): @@ -115,7 +115,7 @@ def circuit(x): # TODO: Make public when removing the ObservableReturnTypes enum -class _Sample(MeasurementProcess): +class _Sample(SampleMeasurement): """Measurement process that returns the samples of a given observable.""" def process(self, samples: np.ndarray, shot_range: Tuple[int] = None, bin_size: int = None): From 19c07c931350332933c568f5440239510df9a8b8 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Thu, 10 Nov 2022 15:49:15 +0100 Subject: [PATCH 23/37] Revert --- pennylane/measurements/classical_shadow.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pennylane/measurements/classical_shadow.py b/pennylane/measurements/classical_shadow.py index 52160ac06bd..f0bd1c09bc6 100644 --- a/pennylane/measurements/classical_shadow.py +++ b/pennylane/measurements/classical_shadow.py @@ -16,7 +16,6 @@ This module contains the qml.classical_shadow measurement. """ from collections.abc import Iterable -from typing import Sequence, Tuple import numpy as np @@ -294,11 +293,3 @@ def __copy__(self): obj.H = self.H obj.k = self.k return obj - - def process( - self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None - ): - """TODO: Implement this method.""" - - def process_state(self, state: np.ndarray, device_wires: Wires): - """TODO: Implement this method.""" From 7c5c14d72570671a1752e8523583b70b7a14760c Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Thu, 10 Nov 2022 15:52:05 +0100 Subject: [PATCH 24/37] Add changelog entry --- doc/releases/changelog-dev.md | 5 +++++ tests/measurements/test_measurements.py | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index bd50283ebdc..bedeea7864c 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -4,6 +4,10 @@

New features since last release

+* `SampleMeasurement` and `StateMeasurement` classes have been added. They contain an abstract + method to process samples/quantum state. + [#3286](https://github.com/PennyLaneAI/pennylane/pull/3286) +

Improvements

Breaking changes

@@ -21,3 +25,4 @@

Contributors

This release contains contributions from (in alphabetical order): +Albert Mitjans Coma diff --git a/tests/measurements/test_measurements.py b/tests/measurements/test_measurements.py index 07720444eea..62306158149 100644 --- a/tests/measurements/test_measurements.py +++ b/tests/measurements/test_measurements.py @@ -25,11 +25,9 @@ MutualInfo, Probability, Sample, - SampleMeasurement, Shadow, ShadowExpval, State, - StateMeasurement, Variance, VnEntropy, expval, From 7f58fa99bd005f68cac14ca76c0f76cc8f02a107 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Thu, 10 Nov 2022 15:55:39 +0100 Subject: [PATCH 25/37] Add changelog entry --- doc/releases/changelog-dev.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index bedeea7864c..ede1d131a38 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -4,9 +4,13 @@

New features since last release

-* `SampleMeasurement` and `StateMeasurement` classes have been added. They contain an abstract - method to process samples/quantum state. - [#3286](https://github.com/PennyLaneAI/pennylane/pull/3286) +* Support custom measurement processes: + * `SampleMeasurement` and `StateMeasurement` classes have been added. They contain an abstract + method to process samples/quantum state. + [#3286](https://github.com/PennyLaneAI/pennylane/pull/3286) + + * Add `_Sample` class. + [#3288](https://github.com/PennyLaneAI/pennylane/pull/3288)

Improvements

From 85ec3120aa1274e8ad7ff57a0d3cbd9f7b103b31 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Fri, 11 Nov 2022 15:39:12 +0100 Subject: [PATCH 26/37] Add changelog entry --- doc/releases/changelog-dev.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index bedeea7864c..0ecbb7f5558 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -4,9 +4,10 @@

New features since last release

-* `SampleMeasurement` and `StateMeasurement` classes have been added. They contain an abstract - method to process samples/quantum state. - [#3286](https://github.com/PennyLaneAI/pennylane/pull/3286) +* Support custom measurement processes: + * `SampleMeasurement` and `StateMeasurement` classes have been added. They contain an abstract + method to process samples/quantum state. + [#3286](https://github.com/PennyLaneAI/pennylane/pull/3286)

Improvements

From 2d36253ce662b147bc48101eb2f34f2ae978e965 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Wed, 16 Nov 2022 09:44:47 +0100 Subject: [PATCH 27/37] Address comments --- pennylane/measurements/measurements.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index 9ed65862d6c..9a334cb3922 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -701,7 +701,7 @@ class SampleMeasurement(MeasurementProcess, ABC): """Sample-based measurement process.""" @abstractmethod - def process( + def process_samples( self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None ): """Process the given samples. @@ -714,21 +714,23 @@ def process( returns the measurement statistic separately over each bin. If not provided, the entire shot range is treated as a single bin. """ + raise NotImplementedError( + "The process_samples method must be defined for a sample-based measurement." + ) class StateMeasurement(MeasurementProcess, ABC): """State-based measurement process.""" @abstractmethod - def process_state(self, state: np.ndarray, device_wires: Wires): + def process_state(self, state: np.ndarray, wires: Wires): """Process the given quantum state. Args: state (ndarray[complex]): quantum state - num_wires (int): total number of wires - shot_range (tuple[int]): 2-tuple of integers specifying the range of samples - to use. If not specified, all samples are used. - bin_size (int): Divides the shot range into bins of size ``bin_size``, and - returns the measurement statistic separately over each bin. If not - provided, the entire shot range is treated as a single bin. + wires (Wires): wires determining the subspace that ``state`` acts on; a matrix of + dimension :math:`2^n` acts on a subspace of :math:`n` wires """ + raise NotImplementedError( + "The process_state method must be defined for a state-based measurement." + ) From 859ca99e0c7b983de30b337fae56c1b550225cea Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Wed, 16 Nov 2022 10:14:46 +0100 Subject: [PATCH 28/37] Change state type --- pennylane/measurements/measurements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index 9a334cb3922..4bf3b0c6dbc 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -723,11 +723,11 @@ class StateMeasurement(MeasurementProcess, ABC): """State-based measurement process.""" @abstractmethod - def process_state(self, state: np.ndarray, wires: Wires): + def process_state(self, state: Sequence[complex], wires: Wires): """Process the given quantum state. Args: - state (ndarray[complex]): quantum state + state (Sequence[complex]): quantum state wires (Wires): wires determining the subspace that ``state`` acts on; a matrix of dimension :math:`2^n` acts on a subspace of :math:`n` wires """ From 77cfec024584b678d5d38763d79084feb736fded Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Wed, 16 Nov 2022 11:40:44 +0100 Subject: [PATCH 29/37] Remove error --- pennylane/measurements/measurements.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index 4bf3b0c6dbc..758b18fbfe7 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -714,9 +714,6 @@ def process_samples( returns the measurement statistic separately over each bin. If not provided, the entire shot range is treated as a single bin. """ - raise NotImplementedError( - "The process_samples method must be defined for a sample-based measurement." - ) class StateMeasurement(MeasurementProcess, ABC): @@ -731,6 +728,3 @@ def process_state(self, state: Sequence[complex], wires: Wires): wires (Wires): wires determining the subspace that ``state`` acts on; a matrix of dimension :math:`2^n` acts on a subspace of :math:`n` wires """ - raise NotImplementedError( - "The process_state method must be defined for a state-based measurement." - ) From b122d3a426b181ecdf45b8d1f511ac07fead33f0 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Wed, 16 Nov 2022 12:24:54 +0100 Subject: [PATCH 30/37] Small fix --- pennylane/measurements/sample.py | 14 +++++++------- tests/measurements/test_sample.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pennylane/measurements/sample.py b/pennylane/measurements/sample.py index fbfe39f5d42..0d2f7d2a3af 100644 --- a/pennylane/measurements/sample.py +++ b/pennylane/measurements/sample.py @@ -16,9 +16,7 @@ This module contains the qml.sample measurement. """ import warnings -from typing import Tuple, Union - -import numpy as np +from typing import Sequence, Tuple, Union import pennylane as qml from pennylane.operation import Observable @@ -118,7 +116,9 @@ def circuit(x): class _Sample(SampleMeasurement): """Measurement process that returns the samples of a given observable.""" - def process(self, samples: np.ndarray, shot_range: Tuple[int] = None, bin_size: int = None): + def process_samples( + self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None + ): name = self.obs.name if self.obs is not None else None # Select the samples from samples that correspond to ``shot_range`` if provided if shot_range is not None: @@ -129,7 +129,7 @@ def process(self, samples: np.ndarray, shot_range: Tuple[int] = None, bin_size: if len(self.wires) != 0: # if wires are provided, then we only return samples from those wires - samples = samples[..., np.array(self.wires)] + samples = samples[..., self.wires] num_wires = samples.shape[-1] # wires is the last dimension @@ -143,9 +143,9 @@ def process(self, samples: np.ndarray, shot_range: Tuple[int] = None, bin_size: else: # Replace the basis state in the computational basis with the correct eigenvalue. # Extract only the columns of the basis samples required based on ``wires``. - powers_of_two = 2 ** np.arange(num_wires)[::-1] + powers_of_two = 2 ** qml.math.arange(num_wires)[::-1] indices = samples @ powers_of_two - indices = np.array(indices) # Add np.array here for Jax support. + indices = qml.math.array(indices) # Add np.array here for Jax support. try: samples = self.obs.eigvals()[indices] except qml.operation.EigvalsUndefinedError as e: diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index e3c8a81809c..1fbc2a6c7df 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -45,7 +45,7 @@ def teardown_method(self): meas = qml.sample(op=meas) assert qml.math.allequal( self.dev.sample(call_args.args[1], **call_args.kwargs), - meas.process(samples=samples, shot_range=shot_range, bin_size=bin_size), + meas.process_samples(samples=samples, shot_range=shot_range, bin_size=bin_size), ) def test_sample_dimension(self, mocker): @@ -332,4 +332,4 @@ class DummyOp(Operator): num_wires = 1 with pytest.raises(EigvalsUndefinedError, match="Cannot compute samples of"): - qml.sample(op=DummyOp(0)).process(samples=np.array([[1, 0]])) + qml.sample(op=DummyOp(0)).process_samples(samples=np.array([[1, 0]])) From 07e65d7b997ab7152d77bb2dc19e940b8efeb9c0 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Fri, 18 Nov 2022 17:26:53 +0100 Subject: [PATCH 31/37] Add wire_order to process_samples --- pennylane/measurements/measurements.py | 8 ++++++-- pennylane/measurements/sample.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index 2556eda2f6c..2a1e483055c 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -699,7 +699,11 @@ class SampleMeasurement(MeasurementProcess, ABC): @abstractmethod def process_samples( - self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None + self, + samples: Sequence[complex], + wire_order: Wires, + shot_range: Tuple[int] = None, + bin_size: int = None, ): """Process the given samples. @@ -717,7 +721,7 @@ class StateMeasurement(MeasurementProcess, ABC): """State-based measurement process.""" @abstractmethod - def process_state(self, state: Sequence[complex], wires: Wires): + def process_state(self, state: Sequence[complex], wire_order: Wires): """Process the given quantum state. Args: diff --git a/pennylane/measurements/sample.py b/pennylane/measurements/sample.py index 0d2f7d2a3af..0a26044443d 100644 --- a/pennylane/measurements/sample.py +++ b/pennylane/measurements/sample.py @@ -117,8 +117,14 @@ class _Sample(SampleMeasurement): """Measurement process that returns the samples of a given observable.""" def process_samples( - self, samples: Sequence[complex], shot_range: Tuple[int] = None, bin_size: int = None + self, + samples: Sequence[complex], + wire_order: Wires, + shot_range: Tuple[int] = None, + bin_size: int = None, ): + wire_map = dict(zip(wire_order, range(len(wire_order)))) + mapped_wires = [wire_map[w] for w in range(self.wires)] name = self.obs.name if self.obs is not None else None # Select the samples from samples that correspond to ``shot_range`` if provided if shot_range is not None: @@ -127,9 +133,9 @@ def process_samples( # Ellipsis (...) otherwise would take up broadcasting and shots axes. samples = samples[..., slice(*shot_range), :] - if len(self.wires) != 0: + if mapped_wires: # if wires are provided, then we only return samples from those wires - samples = samples[..., self.wires] + samples = samples[..., mapped_wires] num_wires = samples.shape[-1] # wires is the last dimension From 508f59f276c4d76a8ee1aa8b123a77e9a2fd47fa Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Fri, 18 Nov 2022 17:27:29 +0100 Subject: [PATCH 32/37] Docstring --- pennylane/measurements/measurements.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index 2a1e483055c..727564bd42e 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -709,6 +709,7 @@ def process_samples( Args: samples (Sequence[complex]): computational basis samples generated for all wires + wire_order (Wires): wires determining the subspace that ``samples`` acts on shot_range (tuple[int]): 2-tuple of integers specifying the range of samples to use. If not specified, all samples are used. bin_size (int): Divides the shot range into bins of size ``bin_size``, and @@ -726,6 +727,6 @@ def process_state(self, state: Sequence[complex], wire_order: Wires): Args: state (Sequence[complex]): quantum state - wires (Wires): wires determining the subspace that ``state`` acts on; a matrix of + 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 """ From a0a8be7c17774da8041e79d2714ee1fc5dabd110 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Fri, 18 Nov 2022 17:55:25 +0100 Subject: [PATCH 33/37] Small fix --- pennylane/measurements/sample.py | 2 +- tests/measurements/test_sample.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pennylane/measurements/sample.py b/pennylane/measurements/sample.py index 0a26044443d..425f35f9b94 100644 --- a/pennylane/measurements/sample.py +++ b/pennylane/measurements/sample.py @@ -124,7 +124,7 @@ def process_samples( bin_size: int = None, ): wire_map = dict(zip(wire_order, range(len(wire_order)))) - mapped_wires = [wire_map[w] for w in range(self.wires)] + mapped_wires = [wire_map[w] for w in self.wires] name = self.obs.name if self.obs is not None else None # Select the samples from samples that correspond to ``shot_range`` if provided if shot_range is not None: diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index 1fbc2a6c7df..1a78a510813 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -45,7 +45,12 @@ def teardown_method(self): meas = qml.sample(op=meas) assert qml.math.allequal( self.dev.sample(call_args.args[1], **call_args.kwargs), - meas.process_samples(samples=samples, shot_range=shot_range, bin_size=bin_size), + meas.process_samples( + samples=samples, + wire_order=self.dev.wires, + shot_range=shot_range, + bin_size=bin_size, + ), ) def test_sample_dimension(self, mocker): @@ -332,4 +337,4 @@ class DummyOp(Operator): num_wires = 1 with pytest.raises(EigvalsUndefinedError, match="Cannot compute samples of"): - qml.sample(op=DummyOp(0)).process_samples(samples=np.array([[1, 0]])) + qml.sample(op=DummyOp(0)).process_samples(samples=np.array([[1, 0]]), wire_order=[0]) From 69567d2a3085b8283f75bddfa7016b2fb700d283 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Mon, 21 Nov 2022 09:48:19 +0100 Subject: [PATCH 34/37] Add wire_order to process_samples --- pennylane/measurements/mutual_info.py | 4 ++-- pennylane/measurements/vn_entropy.py | 4 ++-- tests/measurements/test_mutual_info.py | 2 +- tests/measurements/test_vn_entropy.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pennylane/measurements/mutual_info.py b/pennylane/measurements/mutual_info.py index 00078ceef3c..742c4ed8c24 100644 --- a/pennylane/measurements/mutual_info.py +++ b/pennylane/measurements/mutual_info.py @@ -87,7 +87,7 @@ def circuit_mutual(x): class _MutualInfo(StateMeasurement): """Measurement process that returns the mutual information.""" - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, unused-argument def __init__( self, return_type: ObservableReturnTypes, @@ -100,7 +100,7 @@ def __init__( self.log_base = log_base super().__init__(return_type=return_type, obs=obs, wires=wires, eigvals=eigvals, id=id) - def process_state(self, state: Sequence[complex], wires: Wires): + def process_state(self, state: Sequence[complex], wire_order: Wires): return qml.math.mutual_info( state, indices0=list(self._wires[0]), diff --git a/pennylane/measurements/vn_entropy.py b/pennylane/measurements/vn_entropy.py index 05d39b569db..87beb777383 100644 --- a/pennylane/measurements/vn_entropy.py +++ b/pennylane/measurements/vn_entropy.py @@ -72,7 +72,7 @@ def circuit_entropy(x): class _VnEntropy(StateMeasurement): """Measurement process that returns the Von Neumann entropy.""" - # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, unused-argument def __init__( self, return_type: ObservableReturnTypes, @@ -85,7 +85,7 @@ def __init__( self.log_base = log_base super().__init__(return_type=return_type, obs=obs, wires=wires, eigvals=eigvals, id=id) - def process_state(self, state: Sequence[complex], wires: Wires): + def process_state(self, state: Sequence[complex], wire_order: Wires): return qml.math.vn_entropy( state, indices=self.wires, c_dtype=state.dtype, base=self.log_base ) diff --git a/tests/measurements/test_mutual_info.py b/tests/measurements/test_mutual_info.py index 142c46fc90d..d73b13d3d63 100644 --- a/tests/measurements/test_mutual_info.py +++ b/tests/measurements/test_mutual_info.py @@ -45,7 +45,7 @@ def circuit(): res = circuit() new_res = qml.mutual_info(wires0=[0, 2], wires1=[1, 3]).process_state( - state=circuit.device.state, wires=circuit.device.wires + state=circuit.device.state, wire_order=circuit.device.wires ) assert np.allclose(res, expected, atol=1e-6) assert np.allclose(new_res, expected, atol=1e-6) diff --git a/tests/measurements/test_vn_entropy.py b/tests/measurements/test_vn_entropy.py index 238e45b2ee5..c03e3a991d0 100644 --- a/tests/measurements/test_vn_entropy.py +++ b/tests/measurements/test_vn_entropy.py @@ -39,7 +39,7 @@ def circuit(): res = circuit() new_res = qml.vn_entropy(wires=0).process_state( - state=circuit.device.state, wires=circuit.device.wires + state=circuit.device.state, wire_order=circuit.device.wires ) assert qml.math.allclose(res, expected) assert qml.math.allclose(new_res, expected) From 078c4a7fc8d457c7ffe87d4b5eead6d07720afc2 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Mon, 21 Nov 2022 10:06:17 +0100 Subject: [PATCH 35/37] Change tests --- tests/measurements/test_sample.py | 132 ++++++++++++++++-------------- 1 file changed, 72 insertions(+), 60 deletions(-) diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py index 1a78a510813..ed499a49349 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -12,9 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. """Unit tests for the sample module""" - -import sys - import numpy as np import pytest @@ -23,44 +20,39 @@ from pennylane.operation import EigvalsUndefinedError, Operator +# TODO: Remove this when new CustomMP are the default +def custom_measurement_process(device, spy): + assert len(spy.call_args_list) > 0 # make sure method is mocked properly + + samples = device._samples + call_args_list = list(spy.call_args_list) + for call_args in call_args_list: + meas = call_args.args[1] + shot_range, bin_size = (call_args.kwargs["shot_range"], call_args.kwargs["bin_size"]) + if isinstance(meas, Operator): + meas = qml.sample(op=meas) + assert qml.math.allequal( + device.sample(call_args.args[1], **call_args.kwargs), + meas.process_samples( + samples=samples, + wire_order=device.wires, + shot_range=shot_range, + bin_size=bin_size, + ), + ) + + class TestSample: """Tests for the sample function""" - # TODO: Remove this when new CustomMP are the default - def teardown_method(self): - """Method called at the end of every test. It loops over all the calls to - QubitDevice.sample and compares its output with the new _Sample.process method.""" - if not getattr(self, "spy", False): - return - if sys.version_info[1] <= 7: - return # skip tests for python@3.7 because call_args.kwargs is a tuple instead of a dict - - assert len(self.spy.call_args_list) > 0 # make sure method is mocked properly - - samples = self.dev._samples - for call_args in self.spy.call_args_list: - meas = call_args.args[1] - shot_range, bin_size = (call_args.kwargs["shot_range"], call_args.kwargs["bin_size"]) - if isinstance(meas, Operator): - meas = qml.sample(op=meas) - assert qml.math.allequal( - self.dev.sample(call_args.args[1], **call_args.kwargs), - meas.process_samples( - samples=samples, - wire_order=self.dev.wires, - shot_range=shot_range, - bin_size=bin_size, - ), - ) - def test_sample_dimension(self, mocker): """Test that the sample function outputs samples of the right size""" n_sample = 10 - self.dev = qml.device("default.qubit", wires=2, shots=n_sample) - self.spy = mocker.spy(qml.QubitDevice, "sample") + dev = qml.device("default.qubit", wires=2, shots=n_sample) + spy = mocker.spy(qml.QubitDevice, "sample") - @qml.qnode(self.dev) + @qml.qnode(dev) def circuit(): qml.RX(0.54, wires=0) return qml.sample(qml.PauliZ(0)), qml.sample(qml.PauliX(1)) @@ -69,15 +61,17 @@ def circuit(): assert np.array_equal(sample.shape, (2, n_sample)) + custom_measurement_process(dev, spy) + @pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences") def test_sample_combination(self, mocker): """Test the output of combining expval, var and sample""" n_sample = 10 - self.dev = qml.device("default.qubit", wires=3, shots=n_sample) - self.spy = mocker.spy(qml.QubitDevice, "sample") + dev = qml.device("default.qubit", wires=3, shots=n_sample) + spy = mocker.spy(qml.QubitDevice, "sample") - @qml.qnode(self.dev, diff_method="parameter-shift") + @qml.qnode(dev, diff_method="parameter-shift") def circuit(): qml.RX(0.54, wires=0) @@ -90,14 +84,16 @@ def circuit(): assert isinstance(result[1], np.ndarray) assert isinstance(result[2], np.ndarray) + custom_measurement_process(dev, spy) + def test_single_wire_sample(self, mocker): """Test the return type and shape of sampling a single wire""" n_sample = 10 - self.dev = qml.device("default.qubit", wires=1, shots=n_sample) - self.spy = mocker.spy(qml.QubitDevice, "sample") + dev = qml.device("default.qubit", wires=1, shots=n_sample) + spy = mocker.spy(qml.QubitDevice, "sample") - @qml.qnode(self.dev) + @qml.qnode(dev) def circuit(): qml.RX(0.54, wires=0) @@ -108,15 +104,17 @@ def circuit(): assert isinstance(result, np.ndarray) assert np.array_equal(result.shape, (n_sample,)) + custom_measurement_process(dev, spy) + def test_multi_wire_sample_regular_shape(self, mocker): """Test the return type and shape of sampling multiple wires where a rectangular array is expected""" n_sample = 10 - self.dev = qml.device("default.qubit", wires=3, shots=n_sample) - self.spy = mocker.spy(qml.QubitDevice, "sample") + dev = qml.device("default.qubit", wires=3, shots=n_sample) + spy = mocker.spy(qml.QubitDevice, "sample") - @qml.qnode(self.dev) + @qml.qnode(dev) def circuit(): return qml.sample(qml.PauliZ(0)), qml.sample(qml.PauliZ(1)), qml.sample(qml.PauliZ(2)) @@ -127,16 +125,18 @@ def circuit(): assert np.array_equal(result.shape, (3, n_sample)) assert result.dtype == np.dtype("int") + custom_measurement_process(dev, spy) + @pytest.mark.filterwarnings("ignore:Creating an ndarray from ragged nested sequences") def test_sample_output_type_in_combination(self, mocker): """Test the return type and shape of sampling multiple works in combination with expvals and vars""" n_sample = 10 - self.dev = qml.device("default.qubit", wires=3, shots=n_sample) - self.spy = mocker.spy(qml.QubitDevice, "sample") + dev = qml.device("default.qubit", wires=3, shots=n_sample) + spy = mocker.spy(qml.QubitDevice, "sample") - @qml.qnode(self.dev, diff_method="parameter-shift") + @qml.qnode(dev, diff_method="parameter-shift") def circuit(): return qml.expval(qml.PauliZ(0)), qml.var(qml.PauliZ(1)), qml.sample(qml.PauliZ(2)) @@ -149,13 +149,15 @@ def circuit(): assert result[2].dtype == np.dtype("int") assert np.array_equal(result[2].shape, (n_sample,)) + custom_measurement_process(dev, spy) + def test_not_an_observable(self, mocker): """Test that a UserWarning is raised if the provided argument might not be hermitian.""" - self.dev = qml.device("default.qubit", wires=2, shots=10) - self.spy = mocker.spy(qml.QubitDevice, "sample") + dev = qml.device("default.qubit", wires=2, shots=10) + spy = mocker.spy(qml.QubitDevice, "sample") - @qml.qnode(self.dev) + @qml.qnode(dev) def circuit(): qml.RX(0.52, wires=0) return qml.sample(qml.prod(qml.PauliX(0), qml.PauliZ(0))) @@ -163,13 +165,15 @@ def circuit(): with pytest.warns(UserWarning, match="Prod might not be hermitian."): _ = circuit() + custom_measurement_process(dev, spy) + def test_observable_return_type_is_sample(self, mocker): """Test that the return type of the observable is :attr:`ObservableReturnTypes.Sample`""" n_shots = 10 - self.dev = qml.device("default.qubit", wires=1, shots=n_shots) - self.spy = mocker.spy(qml.QubitDevice, "sample") + dev = qml.device("default.qubit", wires=1, shots=n_shots) + spy = mocker.spy(qml.QubitDevice, "sample") - @qml.qnode(self.dev) + @qml.qnode(dev) def circuit(): res = qml.sample(qml.PauliZ(0)) assert res.return_type is Sample @@ -177,6 +181,8 @@ def circuit(): circuit() + custom_measurement_process(dev, spy) + def test_providing_observable_and_wires(self): """Test that a ValueError is raised if both an observable is provided and wires are specified""" dev = qml.device("default.qubit", wires=2) @@ -195,10 +201,10 @@ def circuit(): def test_providing_no_observable_and_no_wires(self, mocker): """Test that we can provide no observable and no wires to sample function""" - self.dev = qml.device("default.qubit", wires=2, shots=1000) - self.spy = mocker.spy(qml.QubitDevice, "sample") + dev = qml.device("default.qubit", wires=2, shots=1000) + spy = mocker.spy(qml.QubitDevice, "sample") - @qml.qnode(self.dev) + @qml.qnode(dev) def circuit(): qml.Hadamard(wires=0) res = qml.sample() @@ -208,6 +214,8 @@ def circuit(): circuit() + custom_measurement_process(dev, spy) + def test_providing_no_observable_and_no_wires_shot_vector(self, mocker): """Test that we can provide no observable and no wires to sample function when using a shot vector""" @@ -216,10 +224,10 @@ def test_providing_no_observable_and_no_wires_shot_vector(self, mocker): shots1 = 1 shots2 = 10 shots3 = 1000 - self.dev = qml.device("default.qubit", wires=num_wires, shots=[shots1, shots2, shots3]) - self.spy = mocker.spy(qml.QubitDevice, "sample") + dev = qml.device("default.qubit", wires=num_wires, shots=[shots1, shots2, shots3]) + spy = mocker.spy(qml.QubitDevice, "sample") - @qml.qnode(self.dev) + @qml.qnode(dev) def circuit(): qml.Hadamard(wires=0) return qml.sample() @@ -232,14 +240,16 @@ def circuit(): assert len(res) == len(expected_shapes) assert (r.shape == exp_shape for r, exp_shape in zip(res, expected_shapes)) + custom_measurement_process(dev, spy) + def test_providing_no_observable_and_wires(self, mocker): """Test that we can provide no observable but specify wires to the sample function""" wires = [0, 2] wires_obj = qml.wires.Wires(wires) - self.dev = qml.device("default.qubit", wires=3, shots=1000) - self.spy = mocker.spy(qml.QubitDevice, "sample") + dev = qml.device("default.qubit", wires=3, shots=1000) + spy = mocker.spy(qml.QubitDevice, "sample") - @qml.qnode(self.dev) + @qml.qnode(dev) def circuit(): qml.Hadamard(wires=0) res = qml.sample(wires=wires) @@ -250,6 +260,8 @@ def circuit(): circuit() + custom_measurement_process(dev, spy) + @pytest.mark.parametrize( "obs,exp", [ From 5e4759b21cf6e130be3aa1e7b8b0f3d9a94d991c Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Mon, 21 Nov 2022 10:09:10 +0100 Subject: [PATCH 36/37] Add wire_order to process_samples --- pennylane/measurements/state.py | 4 ++-- tests/measurements/test_state.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pennylane/measurements/state.py b/pennylane/measurements/state.py index be32fe3dec2..84092e41980 100644 --- a/pennylane/measurements/state.py +++ b/pennylane/measurements/state.py @@ -126,10 +126,10 @@ class _State(StateMeasurement): """Measurement process that returns the quantum state.""" # pylint: disable=redefined-outer-name - def process_state(self, state: Sequence[complex], wires: Wires): + def process_state(self, state: Sequence[complex], wire_order: Wires): if self.wires: # qml.density_matrix - wire_map = dict(zip(wires, range(len(wires)))) + wire_map = dict(zip(wire_order, range(len(wire_order)))) mapped_wires = [wire_map[w] for w in self.wires] return qml.math.reduced_dm(state, indices=mapped_wires, c_dtype=state.dtype) # qml.state diff --git a/tests/measurements/test_state.py b/tests/measurements/test_state.py index 3c4fd8b08ea..a3f95be992a 100644 --- a/tests/measurements/test_state.py +++ b/tests/measurements/test_state.py @@ -71,7 +71,7 @@ def func(): assert np.allclose(state_val[0], 1 / np.sqrt(2)) assert np.allclose(state_val[-1], 1 / np.sqrt(2)) - assert np.allclose(state().process_state(state=dev.state, wires=dev.wires), state_val) + assert np.allclose(state().process_state(state=dev.state, wire_order=dev.wires), state_val) def test_return_with_other_types(self): """Test that an exception is raised when a state is returned along with another return @@ -397,7 +397,7 @@ def func(): assert np.allclose( expected, - qml.density_matrix(wires=0).process_state(state=dev.state, wires=dev.wires), + qml.density_matrix(wires=0).process_state(state=dev.state, wire_order=dev.wires), ) @pytest.mark.jax @@ -419,7 +419,7 @@ def func(): assert np.allclose( expected, - qml.density_matrix(wires=0).process_state(state=dev.state, wires=dev.wires), + qml.density_matrix(wires=0).process_state(state=dev.state, wire_order=dev.wires), ) @pytest.mark.tf @@ -441,7 +441,7 @@ def func(): assert np.allclose( expected, - qml.density_matrix(wires=0).process_state(state=dev.state, wires=dev.wires), + qml.density_matrix(wires=0).process_state(state=dev.state, wire_order=dev.wires), ) @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) @@ -464,7 +464,7 @@ def func(): assert np.allclose( expected, - qml.density_matrix(wires=0).process_state(state=dev.state, wires=dev.wires), + qml.density_matrix(wires=0).process_state(state=dev.state, wire_order=dev.wires), ) @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) @@ -486,7 +486,7 @@ def func(): assert np.allclose( expected, - qml.density_matrix(wires=1).process_state(state=dev.state, wires=dev.wires), + qml.density_matrix(wires=1).process_state(state=dev.state, wire_order=dev.wires), ) @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) @@ -513,7 +513,7 @@ def func(): assert np.allclose(expected, density_full) assert np.allclose( expected, - qml.density_matrix(wires=[0, 1]).process_state(state=dev.state, wires=dev.wires), + qml.density_matrix(wires=[0, 1]).process_state(state=dev.state, wire_order=dev.wires), ) @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) @@ -544,7 +544,7 @@ def func(): assert np.allclose(expected, density) assert np.allclose( expected, - qml.density_matrix(wires=[1, 2]).process_state(state=dev.state, wires=dev.wires), + qml.density_matrix(wires=[1, 2]).process_state(state=dev.state, wire_order=dev.wires), ) @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) @@ -588,7 +588,7 @@ def func(): assert np.allclose(expected, density) assert np.allclose( expected, - qml.density_matrix(wires=[0, 1]).process_state(state=dev.state, wires=dev.wires), + qml.density_matrix(wires=[0, 1]).process_state(state=dev.state, wire_order=dev.wires), ) @pytest.mark.parametrize("dev_name", ["default.qubit", "default.mixed"]) @@ -662,7 +662,7 @@ def func(): assert np.allclose(expected, density) assert np.allclose( expected, - qml.density_matrix(wires=wires[1]).process_state(state=dev.state, wires=dev.wires), + qml.density_matrix(wires=wires[1]).process_state(state=dev.state, wire_order=dev.wires), ) @pytest.mark.parametrize("wires", [[3, 1], ["b", 1000]]) From 4ff7278148b5c8cf6defe00209e6c0dee6ce7512 Mon Sep 17 00:00:00 2001 From: AlbertMitjans Date: Mon, 21 Nov 2022 10:12:11 +0100 Subject: [PATCH 37/37] Add breaking change message --- doc/releases/changelog-dev.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index c43e03828cf..119aa98f0b7 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -23,7 +23,7 @@ * Functionality for fetching symbols and geometry of a compound from the PubChem Database using `qchem.mol_data`. [(#3289)](https://github.com/PennyLaneAI/pennylane/pull/3289) - + ```pycon >>> mol_data("BeH2") (['Be', 'H', 'H'], @@ -43,23 +43,23 @@ * New basis sets, `6-311g` and `CC-PVDZ`, are added to the qchem basis set repo. [#3279](https://github.com/PennyLaneAI/pennylane/pull/3279) -

Improvements

* `qml.Tracker` now also logs results in `tracker.history` when tracking execution of a circuit. [(#3306)](https://github.com/PennyLaneAI/pennylane/pull/3306) - * Improve performance of `Wires.all_wires`. [(#3302)](https://github.com/PennyLaneAI/pennylane/pull/3302) - * A representation has been added to the `Molecule` class. [#3364](https://github.com/PennyLaneAI/pennylane/pull/3364) -

Breaking changes

+* The `log_base` attribute has been moved from `MeasurementProcess` to the new `_VnEntropy` and + `_MutualInfo` classes, which inherit from `MeasurementProcess`. + [#3326](https://github.com/PennyLaneAI/pennylane/pull/3326) +

Deprecations

Deprecations cycles are tracked at [doc/developement/deprecations.rst](https://docs.pennylane.ai/en/latest/development/deprecations.html). @@ -67,13 +67,13 @@ Deprecations cycles are tracked at [doc/developement/deprecations.rst](https://d * The following deprecated methods are removed: [(#3281)](https://github.com/PennyLaneAI/pennylane/pull/3281/) - - `qml.tape.get_active_tape`: Use `qml.QueuingManager.active_context()` - - `qml.transforms.qcut.remap_tape_wires`: Use `qml.map_wires` - - `qml.tape.QuantumTape.inv()`: Use `qml.tape.QuantumTape.adjoint()` - - `qml.tape.stop_recording()`: Use `qml.QueuingManager.stop_recording()` - - `qml.tape.QuantumTape.stop_recording()`: Use `qml.QueuingManager.stop_recording()` - - `qml.QueuingContext` is now `qml.QueuingManager` - - `QueuingManager.safe_update_info` and `AnnotatedQueue.safe_update_info`: Use plain `update_info` + * `qml.tape.get_active_tape`: Use `qml.QueuingManager.active_context()` + * `qml.transforms.qcut.remap_tape_wires`: Use `qml.map_wires` + * `qml.tape.QuantumTape.inv()`: Use `qml.tape.QuantumTape.adjoint()` + * `qml.tape.stop_recording()`: Use `qml.QueuingManager.stop_recording()` + * `qml.tape.QuantumTape.stop_recording()`: Use `qml.QueuingManager.stop_recording()` + * `qml.QueuingContext` is now `qml.QueuingManager` + * `QueuingManager.safe_update_info` and `AnnotatedQueue.safe_update_info`: Use plain `update_info`

Documentation

@@ -84,7 +84,7 @@ Deprecations cycles are tracked at [doc/developement/deprecations.rst](https://d [#3292](https://github.com/PennyLaneAI/pennylane/pull/3292) * An issue with `drain=False` in the adaptive optimizer is fixed. Before the fix, the operator pool - needed to be re-constructed inside the optimization pool when `drain=False`. With the new fix, + needed to be re-constructed inside the optimization pool when `drain=False`. With the new fix, this reconstruction is not needed. [#3361](https://github.com/PennyLaneAI/pennylane/pull/3361) @@ -92,7 +92,6 @@ Deprecations cycles are tracked at [doc/developement/deprecations.rst](https://d expansion now occurs. [(#3369)](https://github.com/PennyLaneAI/pennylane/pull/3369) -

Contributors

This release contains contributions from (in alphabetical order):