diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index eec9fcb353c..ad5e62c7289 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -9,6 +9,9 @@ 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) + * Add `_State` class. [#3287](https://github.com/PennyLaneAI/pennylane/pull/3287) @@ -41,7 +44,6 @@ * 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

* Continuous integration checks are now performed for Python 3.11 and Torch v1.13. Python 3.7 is dropped. @@ -56,9 +58,12 @@ * 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) + * Python 3.7 support is no longer maintained. [(#3276)](https://github.com/PennyLaneAI/pennylane/pull/3276) @@ -69,13 +74,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

@@ -104,7 +109,6 @@ Deprecations cycles are tracked at [doc/developement/deprecations.rst](https://d with all interfaces [(#3392)](https://github.com/PennyLaneAI/pennylane/pull/3392) -

Contributors

This release contains contributions from (in alphabetical order): diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py index 286459346ab..727564bd42e 100644 --- a/pennylane/measurements/measurements.py +++ b/pennylane/measurements/measurements.py @@ -23,12 +23,12 @@ import functools from abc import ABC, 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 # ============================================================================= @@ -131,7 +131,7 @@ class MeasurementProcess: def __init__( self, return_type: ObservableReturnTypes, - obs: Operator = None, + obs: Union[Observable, None] = None, wires=None, eigvals=None, id=None, @@ -699,12 +699,17 @@ 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. 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 @@ -717,11 +722,11 @@ 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: 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 """ 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/sample.py b/pennylane/measurements/sample.py index f609f5a718a..425f35f9b94 100644 --- a/pennylane/measurements/sample.py +++ b/pennylane/measurements/sample.py @@ -16,13 +16,16 @@ This module contains the qml.sample measurement. """ import warnings +from typing import Sequence, Tuple, Union +import pennylane as qml +from pennylane.operation import Observable from pennylane.wires import Wires -from .measurements import MeasurementProcess, Sample +from .measurements import Sample, SampleMeasurement -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 +109,55 @@ def circuit(x): ) wires = Wires(wires) - return MeasurementProcess(Sample, obs=op, wires=wires) + return _Sample(Sample, obs=op, wires=wires) + + +# TODO: Make public when removing the ObservableReturnTypes enum +class _Sample(SampleMeasurement): + """Measurement process that returns the samples of a given observable.""" + + def process_samples( + 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 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: + # 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. + samples = samples[..., slice(*shot_range), :] + + if mapped_wires: + # if wires are provided, then we only return samples from those wires + samples = samples[..., mapped_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 + return samples if bin_size is None else samples.reshape(num_wires, bin_size, -1) + + if name in {"PauliX", "PauliY", "PauliZ", "Hadamard"}: + # Process samples for observables with eigenvalues {1, -1} + 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``. + powers_of_two = 2 ** qml.math.arange(num_wires)[::-1] + indices = samples @ powers_of_two + indices = qml.math.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 + + return samples if bin_size is None else samples.reshape((bin_size, -1)) 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/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_sample.py b/tests/measurements/test_sample.py index 0bacd921be7..ed499a49349 100644 --- a/tests/measurements/test_sample.py +++ b/tests/measurements/test_sample.py @@ -12,22 +12,45 @@ # See the License for the specific language governing permissions and # limitations under the License. """Unit tests for the sample module""" - import numpy as np import pytest import pennylane as qml from pennylane.measurements import Sample +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""" - def test_sample_dimension(self): + 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) + spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(dev) def circuit(): @@ -38,12 +61,15 @@ 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): + 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) + spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(dev, diff_method="parameter-shift") def circuit(): @@ -58,11 +84,14 @@ def circuit(): assert isinstance(result[1], np.ndarray) assert isinstance(result[2], np.ndarray) - def test_single_wire_sample(self): + 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 dev = qml.device("default.qubit", wires=1, shots=n_sample) + spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(dev) def circuit(): @@ -75,12 +104,15 @@ def circuit(): assert isinstance(result, np.ndarray) assert np.array_equal(result.shape, (n_sample,)) - def test_multi_wire_sample_regular_shape(self): + 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 dev = qml.device("default.qubit", wires=3, shots=n_sample) + spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(dev) def circuit(): @@ -93,13 +125,16 @@ 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): + 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) + spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(dev, diff_method="parameter-shift") def circuit(): @@ -114,10 +149,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): + 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.""" dev = qml.device("default.qubit", wires=2, shots=10) + spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(dev) def circuit(): @@ -127,10 +165,13 @@ def circuit(): with pytest.warns(UserWarning, match="Prod might not be hermitian."): _ = circuit() - def test_observable_return_type_is_sample(self): + 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 dev = qml.device("default.qubit", wires=1, shots=n_shots) + spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(dev) def circuit(): @@ -140,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) @@ -156,9 +199,10 @@ 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) + spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(dev) def circuit(): @@ -170,7 +214,9 @@ def circuit(): circuit() - def test_providing_no_observable_and_no_wires_shot_vector(self): + 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""" num_wires = 2 @@ -179,6 +225,7 @@ def test_providing_no_observable_and_no_wires_shot_vector(self): shots2 = 10 shots3 = 1000 dev = qml.device("default.qubit", wires=num_wires, shots=[shots1, shots2, shots3]) + spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(dev) def circuit(): @@ -193,11 +240,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): + 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) dev = qml.device("default.qubit", wires=3, shots=1000) + spy = mocker.spy(qml.QubitDevice, "sample") @qml.qnode(dev) def circuit(): @@ -210,6 +260,8 @@ def circuit(): circuit() + custom_measurement_process(dev, spy) + @pytest.mark.parametrize( "obs,exp", [ @@ -289,3 +341,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(samples=np.array([[1, 0]]), wire_order=[0]) 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]]) 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)