diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 63cc5c1c62d..9392c0d00c0 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,42 @@

New features since last release

+* Batches of shots can now be specified as a list, allowing measurement statistics + to be course-grained with a single QNode evaluation. + [(#1103)](https://github.com/PennyLaneAI/pennylane/pull/1103) + + Consider + + ```pycon + >>> shots_list = [5, 10, 1000] + >>> dev = qml.device("default.qubit", wires=2, analytic=False, shots=shots_list) + ``` + + When QNodes are executed on this device, a single execution of 1015 shots will be submitted. + However, three sets of measurement statistics will be returned; using the first 5 shots, + second set of 10 shots, and final 1000 shots, separately. + + For example: + + ```python + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)), qml.expval(qml.PauliZ(0)) + ``` + + Executing this, we will get an output of size `(3, 2)`: + + ```pycon + >>> circuit(0.5) + [[0.33333333 1. ] + [0.2 1. ] + [0.012 0.868 ]] + ``` + + This output remains fully differentiable. + - The number of shots can now be specified on a temporary basis when evaluating a QNode. [(#1075)](https://github.com/PennyLaneAI/pennylane/pull/1075) @@ -93,6 +129,34 @@ argument to change the number of shots on a per-call basis. [(#1075)](https://github.com/PennyLaneAI/pennylane/pull/1075) +* For devices inheriting from `QubitDevice`, the methods `expval`, `var`, `sample` + accept two new keyword arguments --- `shot_range` and `bin_size`. + [(#1103)](https://github.com/PennyLaneAI/pennylane/pull/1103) + + These new arguments allow for the statistics to be performed on only a subset of device samples. + This finer level of control is accessible from the main UI by instantiating a device with a batch + of shots. + + For example, consider the following device: + + ```pycon + >>> dev = qml.device("my_device", shots=[5, (10, 3), 100]) + ``` + + This device will execute QNodes using 135 shots, however + measurement statistics will be **course grained** across these 135 + shots: + + * All measurement statistics will first be computed using the + first 5 shots --- that is, `shots_range=[0, 5]`, `bin_size=5`. + + * Next, the tuple `(10, 3)` indicates 10 shots, repeated 3 times. We will want to use + `shot_range=[5, 35]`, performing the expectation value in bins of size 10 + (`bin_size=10`). + + * Finally, we repeat the measurement statistics for the final 100 shots, + `shot_range=[35, 135]`, `bin_size=100`. +

Bug fixes

* The `ExpvalCost` class raises an error if instantiated diff --git a/doc/introduction/circuits.rst b/doc/introduction/circuits.rst index 9cfd494bea1..50e67b24299 100644 --- a/doc/introduction/circuits.rst +++ b/doc/introduction/circuits.rst @@ -120,6 +120,38 @@ devices, the options are: For a plugin device, refer to the plugin documentation for available device options. +Shot batches +^^^^^^^^^^^^ + +Batches of shots can be specified by passing a list, allowing measurement statistics +to be course-grained with a single QNode evaluation. + +Consider + +>>> shots_list = [5, 10, 1000] +>>> dev = qml.device("default.qubit", wires=2, analytic=False, shots=shots_list) + +When QNodes are executed on this device, a single execution of 1015 shots will be submitted. +However, three sets of measurement statistics will be returned; using the first 5 shots, +second set of 10 shots, and final 1000 shots, separately. + +For example: + +.. code-block:: python + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)), qml.expval(qml.PauliZ(0)) + +Executing this, we will get an output of size ``(3, 2)``: + +>>> circuit(0.5) +[[0.33333333 1. ] +[0.2 1. ] +[0.012 0.868 ]] + Custom wire labels ^^^^^^^^^^^^^^^^^^ diff --git a/pennylane/_device.py b/pennylane/_device.py index 30936169b5b..e7087361410 100644 --- a/pennylane/_device.py +++ b/pennylane/_device.py @@ -16,8 +16,8 @@ """ # pylint: disable=too-many-format-args import abc -from collections.abc import Iterable -from collections import OrderedDict +from collections.abc import Iterable, Sequence +from collections import OrderedDict, namedtuple import numpy as np @@ -36,6 +36,62 @@ from pennylane.wires import Wires, WireError +ShotTuple = namedtuple("ShotTuple", ["shots", "copies"]) +"""tuple[int, int]: Represents copies of a shot number.""" + + +def _process_shot_sequence(shot_list): + """Process the shot sequence, to determine the total + number of shots and the shot vector. + + Args: + shot_list (Sequence[int, tuple[int]]): sequence of non-negative shot integers + + Returns: + tuple[int, list[.ShotTuple[int]]]: A tuple containing the total number + of shots, as well as a list of shot tuples. + + **Example** + + >>> shot_list = [3, 1, 2, 2, 2, 2, 6, 1, 1, 5, 12, 10, 10] + >>> _process_shot_sequence(shot_list) + (57, + [ShotTuple(shots=3, copies=1), + ShotTuple(shots=1, copies=1), + ShotTuple(shots=2, copies=4), + ShotTuple(shots=6, copies=1), + ShotTuple(shots=1, copies=2), + ShotTuple(shots=5, copies=1), + ShotTuple(shots=12, copies=1), + ShotTuple(shots=10, copies=2)]) + + The total number of shots (57), and a sparse representation of the shot + sequence is returned, where tuples indicate the number of times a shot + integer is repeated. + """ + if all(isinstance(s, int) for s in shot_list): + + if len(set(shot_list)) == 1: + # All shots are identical, only require a single shot tuple + shot_vector = [ShotTuple(shots=shot_list[0], copies=len(shot_list))] + else: + # Iterate through the shots, and group consecutive identical shots + split_at_repeated = np.split(shot_list, np.diff(shot_list).nonzero()[0] + 1) + shot_vector = [ShotTuple(shots=i[0], copies=len(i)) for i in split_at_repeated] + + elif all(isinstance(s, (int, tuple)) for s in shot_list): + # shot list contains tuples; assume it is already in a sparse representation + shot_vector = [ + ShotTuple(*i) if isinstance(i, tuple) else ShotTuple(i, 1) for i in shot_list + ] + + else: + raise ValueError(f"Unknown shot sequence format {shot_list}") + + total_shots = np.sum(np.prod(shot_vector, axis=1)) + return total_shots, shot_vector + + class DeviceError(Exception): """Exception raised by a :class:`~.pennylane._device.Device` when it encounters an illegal operation in the quantum circuit. @@ -178,13 +234,23 @@ def shots(self, shots): Raises: DeviceError: if number of shots is less than 1 """ - if shots < 1: + if isinstance(shots, int): + if shots < 1: + raise DeviceError( + "The specified number of shots needs to be at least 1. Got {}.".format(shots) + ) + + self._shots = int(shots) + self._shot_vector = None + + elif isinstance(shots, Sequence) and not isinstance(shots, str): + self._shots, self._shot_vector = _process_shot_sequence(shots) + + else: raise DeviceError( - "The specified number of shots needs to be at least 1. Got {}.".format(shots) + "Shots must be a single non-negative integer or a sequence of non-negative integers." ) - self._shots = int(shots) - def define_wire_map(self, wires): """Create the map from user-provided wire labels to the wire labels used by the device. diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py index 3455b02c1f5..56cad73fce0 100644 --- a/pennylane/_qubit_device.py +++ b/pennylane/_qubit_device.py @@ -17,7 +17,7 @@ # For now, arguments may be different from the signatures provided in Device # e.g. instead of expval(self, observable, wires, par) have expval(self, observable) -# pylint: disable=arguments-differ, abstract-method, no-value-for-parameter,too-many-instance-attributes +# pylint: disable=arguments-differ, abstract-method, no-value-for-parameter,too-many-instance-attributes,too-many-branches import abc from collections import OrderedDict import itertools @@ -71,8 +71,9 @@ class QubitDevice(Device): wires (int, Iterable[Number, str]]): Number of subsystems represented by the device, or iterable that contains unique labels for the subsystems as numbers (i.e., ``[-1, 0, 2]``) or strings (``['ancilla', 'q1', 'q2']``). Default 1 if not specified. - shots (int): number of circuit evaluations/random samples used to estimate - expectation values of observables + shots (int, list[int]): Number of circuit evaluations/random samples used to estimate + expectation values of observables. If a list of integers is passed, the circuit + evaluations are batched over the list of shots. analytic (bool): If ``True``, the device calculates probability, expectation values, and variances analytically. If ``False``, a finite number of samples set by the argument ``shots`` are used to estimate these quantities. @@ -208,7 +209,29 @@ def execute(self, circuit, **kwargs): self._samples = self.generate_samples() # compute the required statistics - results = self.statistics(circuit.observables) + if not self.analytic and self._shot_vector is not None: + + results = [] + s1 = 0 + + for shot_tuple in self._shot_vector: + s2 = s1 + np.prod(shot_tuple) + r = self.statistics( + circuit.observables, shot_range=[s1, s2], bin_size=shot_tuple.shots + ) + r = qml.math.squeeze(r) + + if shot_tuple.copies > 1: + results.extend(r.T) + else: + results.append(r) + + s1 = s2 + + results = qml.math.stack(results) + + else: + results = self.statistics(circuit.observables) if circuit.all_sampled or not circuit.is_sampled: results = self._asarray(results) @@ -317,7 +340,7 @@ def active_wires(operators): return Wires.all_wires(list_of_wires) - def statistics(self, observables): + def statistics(self, observables, shot_range=None, bin_size=None): """Process measurement results from circuit execution and return statistics. This includes returning expectation values, variance, samples, probabilities, states, and @@ -325,28 +348,60 @@ def statistics(self, observables): Args: observables (List[.Observable]): the observables to be measured + 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. Raises: QuantumFunctionError: if the value of :attr:`~.Observable.return_type` is not supported Returns: Union[float, List[float]]: the corresponding statistics + + .. UsageDetails:: + + The ``shot_range`` and ``bin_size`` arguments allow for the statistics + to be performed on only a subset of device samples. This finer level + of control is accessible from the main UI by instantiating a device + with a batch of shots. + + For example, consider the following device: + + >>> dev = qml.device("my_device", shots=[5, (10, 3), 100]) + + This device will execute QNodes using 135 shots, however + measurement statistics will be **course grained** across these 135 + shots: + + * All measurement statistics will first be computed using the + first 5 shots --- that is, ``shots_range=[0, 5]``, ``bin_size=5``. + + * Next, the tuple ``(10, 3)`` indicates 10 shots, repeated 3 times. We will want to use + ``shot_range=[5, 35]``, performing the expectation value in bins of size 10 + (``bin_size=10``). + + * Finally, we repeat the measurement statistics for the final 100 shots, + ``shot_range=[35, 135]``, ``bin_size=100``. """ results = [] for obs in observables: # Pass instances directly if obs.return_type is Expectation: - results.append(self.expval(obs)) + results.append(self.expval(obs, shot_range=shot_range, bin_size=bin_size)) elif obs.return_type is Variance: - results.append(self.var(obs)) + results.append(self.var(obs, shot_range=shot_range, bin_size=bin_size)) elif obs.return_type is Sample: - results.append(self.sample(obs)) + results.append(self.sample(obs, shot_range=shot_range, bin_size=bin_size)) elif obs.return_type is Probability: - results.append(self.probability(wires=obs.wires)) + results.append( + self.probability(wires=obs.wires, shot_range=shot_range, bin_size=bin_size) + ) elif obs.return_type is State: if len(observables) > 1: @@ -425,6 +480,7 @@ def sample_basis_states(self, number_of_states, state_probability): Args: number_of_states (int): the number of basis states to sample from + state_probability (array[float]): the computational basis probability vector Returns: List[int]: the sampled basis states @@ -543,13 +599,18 @@ def analytic_probability(self, wires=None): """ raise NotImplementedError - def estimate_probability(self, wires=None): + def estimate_probability(self, wires=None, shot_range=None, bin_size=None): """Return the estimated probability of each computational basis state using the generated samples. Args: wires (Iterable[Number, str], Number, str, Wires): wires to calculate marginal probabilities for. Wires not provided are traced out of the system. + 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. Returns: List[float]: list of the probabilities @@ -561,19 +622,33 @@ def estimate_probability(self, wires=None): # translate to wire labels used by device device_wires = self.map_wires(wires) - samples = self._samples[:, device_wires] + sample_slice = Ellipsis if shot_range is None else slice(*shot_range) + samples = self._samples[sample_slice, device_wires] # convert samples from a list of 0, 1 integers, to base 10 representation powers_of_two = 2 ** np.arange(len(device_wires))[::-1] indices = samples @ powers_of_two # count the basis state occurrences, and construct the probability vector - basis_states, counts = np.unique(indices, return_counts=True) - prob = np.zeros([2 ** len(device_wires)], dtype=np.float64) - prob[basis_states] = counts / len(samples) + if bin_size is not None: + bins = len(samples) // bin_size + + indices = indices.reshape((bins, -1)) + prob = np.zeros([2 ** len(device_wires), bins], dtype=np.float64) + + # count the basis state occurrences, and construct the probability vector + for b, idx in enumerate(indices): + basis_states, counts = np.unique(idx, return_counts=True) + prob[basis_states, b] = counts / bin_size + + else: + basis_states, counts = np.unique(indices, return_counts=True) + prob = np.zeros([2 ** len(device_wires)], dtype=np.float64) + prob[basis_states] = counts / len(samples) + return self._asarray(prob, dtype=self.R_DTYPE) - def probability(self, wires=None): + def probability(self, wires=None, shot_range=None, bin_size=None): """Return either the analytic probability or estimated probability of each computational basis state. @@ -591,7 +666,7 @@ def probability(self, wires=None): if hasattr(self, "analytic") and self.analytic: return self.analytic_probability(wires=wires) - return self.estimate_probability(wires=wires) + return self.estimate_probability(wires=wires, shot_range=shot_range, bin_size=bin_size) def marginal_prob(self, prob, wires=None): r"""Return the marginal probability of the computational basis @@ -662,7 +737,7 @@ def marginal_prob(self, prob, wires=None): perm = basis_states @ powers_of_two return self._gather(prob, perm) - def expval(self, observable): + def expval(self, observable, shot_range=None, bin_size=None): if self.analytic: # exact expectation value @@ -671,9 +746,10 @@ def expval(self, observable): return self._dot(eigvals, prob) # estimate the ev - return np.mean(self.sample(observable)) + samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) + return np.squeeze(np.mean(samples, axis=0)) - def var(self, observable): + def var(self, observable, shot_range=None, bin_size=None): if self.analytic: # exact variance value @@ -682,24 +758,34 @@ def var(self, observable): return self._dot((eigvals ** 2), prob) - self._dot(eigvals, prob) ** 2 # estimate the variance - return np.var(self.sample(observable)) + samples = self.sample(observable, shot_range=shot_range, bin_size=bin_size) + return np.squeeze(np.var(samples, axis=0)) - def sample(self, observable): + def sample(self, observable, shot_range=None, bin_size=None): # translate to wire labels used by device device_wires = self.map_wires(observable.wires) name = observable.name + sample_slice = Ellipsis if shot_range is None else slice(*shot_range) if isinstance(name, str) and name in {"PauliX", "PauliY", "PauliZ", "Hadamard"}: # Process samples for observables with eigenvalues {1, -1} - return 1 - 2 * self._samples[:, device_wires[0]] + samples = 1 - 2 * self._samples[sample_slice, device_wires[0]] - # 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 = self._samples[:, np.array(device_wires)] # Add np.array here for Jax support. - powers_of_two = 2 ** np.arange(samples.shape[-1])[::-1] - indices = samples @ powers_of_two - return observable.eigvals[indices] + 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 = self._samples[ + sample_slice, np.array(device_wires) + ] # Add np.array here for Jax support. + powers_of_two = 2 ** np.arange(samples.shape[-1])[::-1] + indices = samples @ powers_of_two + samples = observable.eigvals[indices] + + if bin_size is None: + return samples + + return samples.reshape((bin_size, -1)) def adjoint_jacobian(self, tape): """Implements the adjoint method outlined in diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py index 03c0e4dd07d..9cbc68635ed 100644 --- a/tests/test_qubit_device.py +++ b/tests/test_qubit_device.py @@ -45,10 +45,10 @@ def mock_qubit_device(monkeypatch): m.setattr(QubitDevice, "operations", ["PauliY", "RX", "Rot"]) m.setattr(QubitDevice, "observables", ["PauliZ"]) m.setattr(QubitDevice, "short_name", "MockDevice") - m.setattr(QubitDevice, "expval", lambda self, x: 0) - m.setattr(QubitDevice, "var", lambda self, x: 0) - m.setattr(QubitDevice, "sample", lambda self, x: 0) - m.setattr(QubitDevice, "apply", lambda self, x: None) + m.setattr(QubitDevice, "expval", lambda self, *args, **kwargs: 0) + m.setattr(QubitDevice, "var", lambda self, *args, **kwargs: 0) + m.setattr(QubitDevice, "sample", lambda self, *args, **kwargs: 0) + m.setattr(QubitDevice, "apply", lambda self, *args, **kwargs: None) def get_qubit_device(wires=1): return QubitDevice(wires=wires) @@ -66,13 +66,13 @@ def mock_qubit_device_extract_stats(monkeypatch): m.setattr(QubitDevice, "operations", ["PauliY", "RX", "Rot"]) m.setattr(QubitDevice, "observables", ["PauliZ"]) m.setattr(QubitDevice, "short_name", "MockDevice") - m.setattr(QubitDevice, "expval", lambda self, x: 0) - m.setattr(QubitDevice, "var", lambda self, x: 0) - m.setattr(QubitDevice, "sample", lambda self, x: 0) + m.setattr(QubitDevice, "expval", lambda self, *args, **kwargs: 0) + m.setattr(QubitDevice, "var", lambda self, *args, **kwargs: 0) + m.setattr(QubitDevice, "sample", lambda self, *args, **kwargs: 0) m.setattr(QubitDevice, "state", 0) m.setattr(QubitDevice, "density_matrix", lambda self, wires=None: 0) m.setattr( - QubitDevice, "probability", lambda self, wires=None: 0 + QubitDevice, "probability", lambda self, wires=None, *args, **kwargs: 0 ) m.setattr(QubitDevice, "apply", lambda self, x: x) @@ -115,9 +115,9 @@ def mock_qubit_device_with_paulis_and_methods(monkeypatch): m.setattr(QubitDevice, "operations", mock_qubit_device_paulis) m.setattr(QubitDevice, "observables", mock_qubit_device_paulis) m.setattr(QubitDevice, "short_name", "MockDevice") - m.setattr(QubitDevice, "expval", lambda self, x: 0) - m.setattr(QubitDevice, "var", lambda self, x: 0) - m.setattr(QubitDevice, "sample", lambda self, x: 0) + m.setattr(QubitDevice, "expval", lambda self, *args, **kwargs: 0) + m.setattr(QubitDevice, "var", lambda self, *args, **kwargs: 0) + m.setattr(QubitDevice, "sample", lambda self, *args, **kwargs: 0) m.setattr(QubitDevice, "apply", lambda self, x, rotations: None) def get_qubit_device(wires=1): @@ -135,9 +135,9 @@ def mock_qubit_device_with_paulis_rotations_and_methods(monkeypatch): m.setattr(QubitDevice, "operations", mock_qubit_device_paulis + mock_qubit_device_rotations) m.setattr(QubitDevice, "observables", mock_qubit_device_paulis) m.setattr(QubitDevice, "short_name", "MockDevice") - m.setattr(QubitDevice, "expval", lambda self, x: 0) - m.setattr(QubitDevice, "var", lambda self, x: 0) - m.setattr(QubitDevice, "sample", lambda self, x: 0) + m.setattr(QubitDevice, "expval", lambda self, *args, **kwargs: 0) + m.setattr(QubitDevice, "var", lambda self, *args, **kwargs: 0) + m.setattr(QubitDevice, "sample", lambda self, *args, **kwargs: 0) m.setattr(QubitDevice, "apply", lambda self, x: None) def get_qubit_device(wires=1): @@ -500,8 +500,8 @@ def test_non_analytic_expval(self, mock_qubit_device_with_original_statistics, m call_history = [] with monkeypatch.context() as m: - m.setattr(QubitDevice, "sample", lambda self, obs: obs) - m.setattr("numpy.mean", lambda obs: obs) + m.setattr(QubitDevice, "sample", lambda self, obs, *args, **kwargs: obs) + m.setattr("numpy.mean", lambda obs, axis=None: obs) res = dev.expval(obs) assert res == obs @@ -548,8 +548,8 @@ def test_non_analytic_var(self, mock_qubit_device_with_original_statistics, monk call_history = [] with monkeypatch.context() as m: - m.setattr(QubitDevice, "sample", lambda self, obs: obs) - m.setattr("numpy.var", lambda obs: obs) + m.setattr(QubitDevice, "sample", lambda self, obs, *args, **kwargs: obs) + m.setattr("numpy.var", lambda obs, axis=None: obs) res = dev.var(obs) assert res == obs @@ -861,3 +861,124 @@ def test_result_empty_tape(self, mock_qubit_device_with_paulis_and_methods, tol) assert len(res) == 3 assert np.allclose(res[0], dev.execute(empty_tape), rtol=tol, atol=0) + + +class TestShotList: + """Tests for passing shots as a list""" + + shot_data = [ + [[1, 2, 3, 10], [(1, 1), (2, 1), (3, 1), (10, 1)], (4,), 16], + [[1, 2, 2, 2, 10, 1, 1, 5, 1, 1, 1], [(1, 1), (2, 3), (10, 1), (1, 2), (5, 1), (1, 3)], (11,), 27], + [[10, 10, 10], [(10, 3)], (3,), 30], + [[(10, 3)], [(10, 3)], (3,), 30], + ] + + @pytest.mark.parametrize("shot_list,shot_vector,expected_shape,total_shots", shot_data) + def test_single_expval(self, shot_list, shot_vector, expected_shape, total_shots): + """Test a single expectation value""" + dev = qml.device("default.qubit", wires=2, analytic=False, shots=shot_list) + + @qml.qnode(dev) + def circuit(x): + qml.RX(x, wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)) + + res = circuit(0.5) + + assert res.shape == expected_shape + assert circuit.device._shot_vector == shot_vector + assert circuit.device.shots == total_shots + + shot_data = [ + [[1, 2, 3, 10], [(1, 1), (2, 1), (3, 1), (10, 1)], (4, 2), 16], + [[1, 2, 2, 2, 10, 1, 1, 5, 1, 1, 1], [(1, 1), (2, 3), (10, 1), (1, 2), (5, 1), (1, 3)], (11, 2), 27], + [[10, 10, 10], [(10, 3)], (3, 2), 30], + [[(10, 3)], [(10, 3)], (3, 2), 30], + ] + + @pytest.mark.parametrize("shot_list,shot_vector,expected_shape,total_shots", shot_data) + def test_multiple_expval(self, shot_list, shot_vector, expected_shape, total_shots): + """Test multiple expectation values""" + dev = qml.device("default.qubit", wires=2, analytic=False, shots=shot_list) + + @qml.qnode(dev) + def circuit(x, y): + qml.RX(x, wires=0) + qml.RY(y, wires=0) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0) @ qml.PauliX(1)), qml.expval(qml.PauliZ(0)) + + res = circuit(0.5, 0.1) + + assert res.shape == expected_shape + assert circuit.device._shot_vector == shot_vector + assert circuit.device.shots == total_shots + + # test gradient works + res = qml.jacobian(circuit)(0.5, 0.1) + assert res.shape == (2,) + expected_shape + + shot_data = [ + [[1, 2, 3, 10], [(1, 1), (2, 1), (3, 1), (10, 1)], (4, 4), 16], + [[1, 2, 2, 2, 10, 1, 1, 5, 1, 1, 1], [(1, 1), (2, 3), (10, 1), (1, 2), (5, 1), (1, 3)], (11, 4), 27], + [[10, 10, 10], [(10, 3)], (3, 4), 30], + [[(10, 3)], [(10, 3)], (3, 4), 30], + ] + + @pytest.mark.parametrize("shot_list,shot_vector,expected_shape,total_shots", shot_data) + def test_probs(self, shot_list, shot_vector, expected_shape, total_shots): + """Test a probability return""" + dev = qml.device("default.qubit", wires=2, analytic=False, shots=shot_list) + + @qml.qnode(dev) + def circuit(x, y): + qml.RX(x, wires=0) + qml.RY(y, wires=0) + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=[0, 1]) + + res = circuit(0.5, 0.1) + + assert res.shape == expected_shape + assert circuit.device._shot_vector == shot_vector + assert circuit.device.shots == total_shots + + # test gradient works + res = qml.jacobian(circuit)(0.5, 0.1) + + shot_data = [ + [[1, 2, 3, 10], [(1, 1), (2, 1), (3, 1), (10, 1)], (4, 2, 2), 16], + [[1, 2, 2, 2, 10, 1, 1, 5, 1, 1, 1], [(1, 1), (2, 3), (10, 1), (1, 2), (5, 1), (1, 3)], (11, 2, 2), 27], + [[10, 10, 10], [(10, 3)], (3, 2, 2), 30], + [[(10, 3)], [(10, 3)], (3, 2, 2), 30], + ] + + @pytest.mark.parametrize("shot_list,shot_vector,expected_shape,total_shots", shot_data) + def test_multiple_probs(self, shot_list, shot_vector, expected_shape, total_shots): + """Test multiple probability returns""" + dev = qml.device("default.qubit", wires=2, analytic=False, shots=shot_list) + + @qml.qnode(dev) + def circuit(x, y): + qml.RX(x, wires=0) + qml.RY(y, wires=0) + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=0), qml.probs(wires=1) + + res = circuit(0.5, 0.1) + + assert res.shape == expected_shape + assert circuit.device._shot_vector == shot_vector + assert circuit.device.shots == total_shots + + # test gradient works + res = qml.jacobian(circuit)(0.5, 0.1) + + def test_invalid_shot_list(self): + """Test exception raised if the shot list is the wrong type""" + with pytest.raises(qml.DeviceError, match="Shots must be"): + qml.device("default.qubit", wires=2, analytic=False, shots=0.5) + + with pytest.raises(ValueError, match="Unknown shot sequence"): + qml.device("default.qubit", wires=2, analytic=False, shots=["a", "b", "c"])