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)