diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index cb2dce690cb..ab97bb268dd 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -107,8 +107,9 @@ [(#2709)](https://github.com/PennyLaneAI/pennylane/pull/2709) * Added `QutritDevice` as an abstract base class for qutrit devices. - [(#2781)](https://github.com/PennyLaneAI/pennylane/pull/2781) - * Added operation `qml.QutritUnitary` for applying user-specified unitary operations on qutrit devices. + ([#2781](https://github.com/PennyLaneAI/pennylane/pull/2781), [#2782](https://github.com/PennyLaneAI/pennylane/pull/2782)) + +* Added operation `qml.QutritUnitary` for applying user-specified unitary operations on qutrit devices. [(#2699)](https://github.com/PennyLaneAI/pennylane/pull/2699) **Operator Arithmetic:** diff --git a/pennylane/_qutrit_device.py b/pennylane/_qutrit_device.py index 3403bbd3e49..67b660608b8 100644 --- a/pennylane/_qutrit_device.py +++ b/pennylane/_qutrit_device.py @@ -17,7 +17,7 @@ # For now, arguments may be different from the signatures provided in QubitDevice to minimize size of pull request # e.g. instead of expval(self, observable, wires, par) have expval(self, observable) -# pylint: disable=abstract-method, no-value-for-parameter,too-many-instance-attributes,too-many-branches, no-member, bad-option-value, arguments-renamed +# pylint: disable=arguments-differ, abstract-method, no-value-for-parameter,too-many-instance-attributes,too-many-branches, no-member, bad-option-value, arguments-renamed import itertools import numpy as np @@ -80,10 +80,6 @@ def capabilities(cls): capabilities.update(model="qutrit") return capabilities - def statistics(self, observables, shot_range=None, bin_size=None): - # Overloading QubitDevice.statistics() as VnEntropy and MutualInfo not yet supported for QutritDevice - raise NotImplementedError - def generate_samples(self): r"""Returns the computational basis samples generated for all wires. @@ -152,15 +148,18 @@ def states_to_ternary(samples, num_wires, dtype=np.int64): def density_matrix(self, wires): """Returns the reduced density matrix prior to measurement. - .. note:: + Args: + wires (Wires): wires of the reduced system - Only state vector simulators support this property. Please see the - plugin documentation for more details. + Raises: + QuantumFunctionError: density matrix is currently unsupported on :class:`~.QutritDevice` """ # TODO: Add density matrix support. Currently, qml.math is hard-coded to work only with qubit states, # (see `qml.math.reduced_dm()`) so it needs to be updated to be able to handle calculations for qutrits # before this method can be implemented. - raise NotImplementedError + raise qml.QuantumFunctionError( + "Unsupported return type specified for observable density matrix" + ) def vn_entropy(self, wires, log_base): r"""Returns the Von Neumann entropy prior to measurement. @@ -172,13 +171,15 @@ def vn_entropy(self, wires, log_base): wires (Wires): Wires of the considered subsystem. log_base (float): Base for the logarithm, default is None the natural logarithm is used in this case. - Returns: - float: returns the Von Neumann entropy + Raises: + QuantumFunctionError: Von Neumann entropy is currently unsupported on :class:`~.QutritDevice` """ # TODO: Add support for VnEntropy return type. Currently, qml.math is hard coded to calculate this for qubit # states (see `qml.math.vn_entropy()`), so it needs to be updated before VnEntropy can be supported for qutrits. # For now, if a user tries to request this return type, an error will be raised. - raise NotImplementedError + raise qml.QuantumFunctionError( + "Unsupported return type specified for observable Von Neumann entropy" + ) def mutual_info(self, wires0, wires1, log_base): r"""Returns the mutual information prior to measurement: @@ -194,13 +195,15 @@ def mutual_info(self, wires0, wires1, log_base): wires1 (Wires): wires of the second subsystem log_base (float): base to use in the logarithm - Returns: - float: the mutual information + Raises: + QuantumFunctionError: Mutual information is currently unsupported on :class:`~.QutritDevice` """ # TODO: Add support for MutualInfo return type. Currently, qml.math is hard coded to calculate this for qubit # states (see `qml.math.mutual_info()`), so it needs to be updated before MutualInfo can be supported for qutrits. # For now, if a user tries to request this return type, an error will be raised. - raise NotImplementedError + raise qml.QuantumFunctionError( + "Unsupported return type specified for observable mutual information" + ) def estimate_probability(self, wires=None, shot_range=None, bin_size=None): """Return the estimated probability of each computational basis state @@ -317,10 +320,27 @@ def marginal_prob(self, prob, wires=None): perm = basis_states @ powers_of_three return self._gather(prob, perm) - # TODO: Update in next PR to add counts capability for binning - def sample( - self, observable, shot_range=None, bin_size=None - ): # pylint: disable=arguments-differ + def sample(self, observable, shot_range=None, bin_size=None, counts=False): + def _samples_to_counts(samples, no_observable_provided): + """Group the obtained samples into a dictionary. + + **Example** + + >>> samples + tensor([[0, 0, 1], + [0, 0, 1], + [1, 1, 1]], requires_grad=True) + >>> self._samples_to_counts(samples) + {'111':1, '001':2} + """ + if no_observable_provided: + # If we describe a state vector, we need to convert its list representation + # into string (it's hashable and good-looking). + # Before converting to str, we need to extract elements from arrays + # to satisfy the case of jax interface, as jax arrays do not support str. + samples = ["".join([str(s.item()) for s in sample]) for sample in samples] + states, counts = np.unique(samples, return_counts=True) + return dict(zip(states, counts)) # TODO: Add special cases for any observables that require them once list of # observables is updated. @@ -329,10 +349,9 @@ def sample( device_wires = self.map_wires(observable.wires) name = observable.name # pylint: disable=unused-variable sample_slice = Ellipsis if shot_range is None else slice(*shot_range) + no_observable_provided = isinstance(observable, MeasurementProcess) - if isinstance( - observable, MeasurementProcess - ): # if no observable was provided then return the raw samples + if no_observable_provided: # if no observable was provided then return the raw samples if ( len(observable.wires) != 0 ): # if wires are provided, then we only return samples from those wires @@ -359,9 +378,22 @@ def sample( ) from e if bin_size is None: + if counts: + return _samples_to_counts(samples, no_observable_provided) return samples - return samples.reshape((bin_size, -1)) + num_wires = len(device_wires) if len(device_wires) > 0 else self.num_wires + if counts: + shape = (-1, bin_size, num_wires) if no_observable_provided else (-1, bin_size) + return [ + _samples_to_counts(bin_sample, no_observable_provided) + for bin_sample in samples.reshape(shape) + ] + return ( + samples.reshape((num_wires, bin_size, -1)) + if no_observable_provided + else samples.reshape((bin_size, -1)) + ) # TODO: Implement function. Currently unimplemented due to lack of decompositions available # for existing operations and lack of non-parametrized observables. diff --git a/tests/test_qutrit_device.py b/tests/test_qutrit_device.py index 76e9cd1e89f..2763daeb8a4 100644 --- a/tests/test_qutrit_device.py +++ b/tests/test_qutrit_device.py @@ -23,7 +23,7 @@ from pennylane import numpy as pnp from pennylane import QutritDevice, DeviceError, QuantumFunctionError, QubitDevice from pennylane.devices import DefaultQubit -from pennylane.measurements import Sample, Variance, Expectation, Probability, State +from pennylane.measurements import Sample, Variance, Expectation, Probability, State, Counts from pennylane.circuit_graph import CircuitGraph from pennylane.wires import Wires from pennylane.tape import QuantumTape @@ -249,6 +249,24 @@ def test_unsupported_observables_raise_error(self, mock_qutrit_device): with pytest.raises(DeviceError, match="Observable Hadamard not supported on device"): dev.execute(tape) + def test_unsupported_observable_return_type_raise_error(self, mock_qutrit_device, monkeypatch): + """Check that an error is raised if the return type of an observable is unsupported""" + U = unitary_group.rvs(3, random_state=10) + + with qml.tape.QuantumTape() as tape: + qml.QutritUnitary(U, wires=0) + qml.measurements.MeasurementProcess( + return_type="SomeUnsupportedReturnType", obs=qml.Identity(0) + ) + + with monkeypatch.context() as m: + m.setattr(QutritDevice, "apply", lambda self, x, **kwargs: None) + dev = mock_qutrit_device() + with pytest.raises( + qml.QuantumFunctionError, match="Unsupported return type specified for observable" + ): + dev.execute(tape) + class TestParameters: """Test for checking device parameter mappings""" @@ -264,11 +282,99 @@ def test_parameters_accessed_outside_execution_context(self, mock_qutrit_device) dev.parameters +class TestExtractStatistics: + """Test the statistics method""" + + @pytest.mark.parametrize( + "returntype", [Expectation, Variance, Sample, Probability, State, Counts] + ) + def test_results_created(self, mock_qutrit_device_extract_stats, monkeypatch, returntype): + """Tests that the statistics method simply builds a results list without any side-effects""" + + class SomeObservable(qml.operation.Observable): + num_wires = 1 + return_type = returntype + + obs = SomeObservable(wires=0) + + with monkeypatch.context() as m: + dev = mock_qutrit_device_extract_stats() + results = dev.statistics([obs]) + + assert results == [0] + + def test_results_no_state(self, mock_qutrit_device, monkeypatch): + """Tests that the statistics method raises an AttributeError when a State return type is + requested when QutritDevice does not have a state attribute""" + with monkeypatch.context() as m: + dev = mock_qutrit_device() + m.delattr(QubitDevice, "state") + with pytest.raises( + qml.QuantumFunctionError, match="The state is not available in the current" + ): + dev.statistics([qml.state()]) + + @pytest.mark.parametrize("returntype", [None]) + def test_results_created_empty(self, mock_qutrit_device_extract_stats, monkeypatch, returntype): + """Tests that the statistics method returns an empty list if the return type is None""" + + class SomeObservable(qml.operation.Observable): + num_wires = 1 + return_type = returntype + + obs = SomeObservable(wires=0) + + with monkeypatch.context() as m: + dev = mock_qutrit_device_extract_stats() + results = dev.statistics([obs]) + + assert results == [] + + @pytest.mark.parametrize("returntype", ["not None"]) + def test_error_return_type_not_none( + self, mock_qutrit_device_extract_stats, monkeypatch, returntype + ): + """Tests that the statistics method raises an error if the return type is not well-defined and is not None""" + + assert returntype not in [Expectation, Variance, Sample, Probability, State, Counts, None] + + class SomeObservable(qml.operation.Observable): + num_wires = 1 + return_type = returntype + + obs = SomeObservable(wires=0) + + dev = mock_qutrit_device_extract_stats() + with pytest.raises(qml.QuantumFunctionError, match="Unsupported return type"): + dev.statistics([obs]) + + def test_return_state_with_multiple_observables(self, mock_qutrit_device_extract_stats): + """Checks that an error is raised if multiple observables are being returned + and one of them is state + """ + U = unitary_group.rvs(3, random_state=10) + + with qml.tape.QuantumTape() as tape: + qml.QutritUnitary(U, wires=0) + qml.state() + qml.probs(wires=0) + + dev = mock_qutrit_device_extract_stats() + + with pytest.raises( + qml.QuantumFunctionError, + match="The state or density matrix cannot be returned in combination", + ): + dev.execute(tape) + + class TestSample: """Test the sample method""" # TODO: Add tests for sampling with observables that have eigenvalues to sample from once # such observables are added for qutrits. + # TODO: Add tests for counts for observables with eigenvalues once such observables are + # added for qutrits. def test_sample_with_no_observable_and_no_wires( self, mock_qutrit_device_with_original_statistics, tol @@ -323,10 +429,59 @@ def test_samples_with_bins(self, mock_qutrit_device_with_original_statistics, mo bin_size = 3 out = dev.sample(obs, shot_range=shot_range, bin_size=bin_size) - expected_samples = samples.reshape(3, -1) + expected_samples = samples.reshape(-1, 3, 2) assert np.array_equal(out, expected_samples) + def test_counts(self, mock_qutrit_device_with_original_statistics, monkeypatch): + dev = mock_qutrit_device_with_original_statistics(wires=2) + samples = np.array([[0, 1], [2, 0], [2, 0], [0, 1], [2, 2], [1, 2]]) + dev._samples = samples + obs = qml.measurements.sample(op=None, wires=[0, 1]) + + out = dev.sample(obs, counts=True) + expected_counts = { + "01": 2, + "20": 2, + "22": 1, + "12": 1, + } + + assert out == expected_counts + + def test_raw_counts_with_bins(self, mock_qutrit_device_with_original_statistics, monkeypatch): + dev = mock_qutrit_device_with_original_statistics(wires=2) + samples = np.array( + [ + [0, 1], + [2, 0], + [2, 0], + [0, 1], + [2, 2], + [1, 2], + [0, 1], + [2, 0], + [2, 1], + [0, 2], + [2, 1], + [1, 2], + ] + ) + dev._samples = samples + obs = qml.measurements.sample(op=None, wires=[0, 1]) + + shot_range = [0, 12] + bin_size = 4 + out = dev.sample(obs, shot_range=shot_range, bin_size=bin_size, counts=True) + + expected_counts = [ + {"01": 2, "20": 2}, + {"22": 1, "12": 1, "01": 1, "20": 1}, + {"21": 2, "02": 1, "12": 1}, + ] + + assert out == expected_counts + class TestGenerateSamples: """Test the generate_samples method""" @@ -618,6 +773,255 @@ def test_defines_correct_capabilities(self): assert capabilities == QutritDevice.capabilities() +class TestExecution: + """Tests for the execute method""" + + def test_device_executions(self, mock_qutrit_device_extract_stats): + """Test the number of times a qutrit device is executed over a QNode's + lifetime is tracked by `num_executions`""" + + dev_1 = mock_qutrit_device_extract_stats(wires=2) + + def circuit_1(U1, U2, U3): + qml.QutritUnitary(U1, wires=[0]) + qml.QutritUnitary(U2, wires=[1]) + qml.QutritUnitary(U3, wires=[0, 1]) + return qml.state() + + node_1 = qml.QNode(circuit_1, dev_1) + num_evals_1 = 10 + + for _ in range(num_evals_1): + node_1(np.eye(3), np.eye(3), np.eye(9)) + assert dev_1.num_executions == num_evals_1 + + # test a new circuit on an existing instance of a qutrit device + def circuit_3(U1, U2): + qml.QutritUnitary(U1, wires=[0]) + qml.QutritUnitary(U2, wires=[0, 1]) + return qml.state() + + node_3 = qml.QNode(circuit_3, dev_1) + num_evals_3 = 7 + + for _ in range(num_evals_3): + node_3(np.eye(3), np.eye(9)) + assert dev_1.num_executions == num_evals_1 + num_evals_3 + + +class TestBatchExecution: + """Tests for the batch_execute method.""" + + with qml.tape.QuantumTape() as tape1: + qml.QutritUnitary(np.eye(3), wires=0) + qml.expval(qml.Identity(0)), qml.expval(qml.Identity(1)) + + with qml.tape.QuantumTape() as tape2: + qml.QutritUnitary(np.eye(3), wires=0) + qml.expval(qml.Identity(0)) + + @pytest.mark.parametrize("n_tapes", [1, 2, 3]) + def test_calls_to_execute(self, n_tapes, mocker, mock_qutrit_device): + """Tests that the device's execute method is called the correct number of times.""" + + dev = mock_qutrit_device(wires=2) + spy = mocker.spy(QutritDevice, "execute") + + tapes = [self.tape1] * n_tapes + dev.batch_execute(tapes) + + assert spy.call_count == n_tapes + + @pytest.mark.parametrize("n_tapes", [1, 2, 3]) + def test_calls_to_reset(self, n_tapes, mocker, mock_qutrit_device): + """Tests that the device's reset method is called the correct number of times.""" + + dev = mock_qutrit_device(wires=2) + + spy = mocker.spy(QutritDevice, "reset") + + tapes = [self.tape1] * n_tapes + dev.batch_execute(tapes) + + assert spy.call_count == n_tapes + + @pytest.mark.parametrize("r_dtype", [np.float32, np.float64]) + def test_result(self, mock_qutrit_device, r_dtype, tol): + """Tests that the result has the correct shape and entry types.""" + + dev = mock_qutrit_device(wires=2) + dev.R_DTYPE = r_dtype + + tapes = [self.tape1, self.tape2] + res = dev.batch_execute(tapes) + + assert len(res) == 2 + assert np.allclose(res[0], dev.execute(self.tape1), rtol=tol, atol=0) + assert np.allclose(res[1], dev.execute(self.tape2), rtol=tol, atol=0) + assert res[0].dtype == r_dtype + assert res[1].dtype == r_dtype + + def test_result_empty_tape(self, mock_qutrit_device, tol): + """Tests that the result has the correct shape and entry types for empty tapes.""" + + dev = mock_qutrit_device(wires=2) + + empty_tape = qml.tape.QuantumTape() + tapes = [empty_tape] * 3 + res = dev.batch_execute(tapes) + + 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""" + + # TODO: Add tests for expval and sample with shot lists after observables are added + + def test_invalid_shot_list(self, mock_qutrit_device_shots): + """Test exception raised if the shot list is the wrong type""" + with pytest.raises(qml.DeviceError, match="Shots must be"): + mock_qutrit_device_shots(wires=2, shots=0.5) + + with pytest.raises(ValueError, match="Unknown shot sequence"): + mock_qutrit_device_shots(wires=2, shots=["a", "b", "c"]) + + shot_data = [ + [[1, 2, 3, 10], [(1, 1), (2, 1), (3, 1), (10, 1)], (4, 9), 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, 9), + 27, + ], + [[10, 10, 10], [(10, 3)], (3, 9), 30], + [[(10, 3)], [(10, 3)], (3, 9), 30], + ] + + @pytest.mark.autograd + @pytest.mark.parametrize("shot_list,shot_vector,expected_shape,total_shots", shot_data) + def test_probs( + self, mock_qutrit_device_shots, shot_list, shot_vector, expected_shape, total_shots + ): + """Test a probability return""" + dev = mock_qutrit_device_shots(wires=2, shots=shot_list) + + @qml.qnode(dev) + def circuit(x, z): + RZ_01 = pnp.array( + [ + [pnp.exp(-1j * z / 2), 0.0, 0.0], + [0.0, pnp.exp(1j * z / 2), 0.0], + [0.0, 0.0, 1.0], + ] + ) + + c = pnp.cos(x / 2) + s = pnp.sin(x / 2) * 1j + RX_01 = pnp.array([[c, -s, 0.0], [-s, c, 0.0], [0.0, 0.0, 1.0]]) + + qml.QutritUnitary(RZ_01, wires=0) + qml.QutritUnitary(RX_01, wires=1) + return qml.probs(wires=[0, 1]) + + res = circuit(0.1, 0.6) + + assert res.shape == expected_shape + assert circuit.device._shot_vector == shot_vector + assert circuit.device.shots == total_shots + + # test gradient works + # TODO: Add after differentiability of qutrit circuits is implemented + # res = qml.jacobian(circuit, argnum=[0, 1])(0.1, 0.6) + + marginal_shot_data = [ + [[1, 2, 3, 10], [(1, 1), (2, 1), (3, 1), (10, 1)], (4, 3), 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, 3), + 27, + ], + [[10, 10, 10], [(10, 3)], (3, 3), 30], + [[(10, 3)], [(10, 3)], (3, 3), 30], + ] + + @pytest.mark.autograd + @pytest.mark.parametrize("shot_list,shot_vector,expected_shape,total_shots", marginal_shot_data) + def test_marginal_probs( + self, mock_qutrit_device_shots, shot_list, shot_vector, expected_shape, total_shots + ): + dev = mock_qutrit_device_shots(wires=2, shots=shot_list) + + @qml.qnode(dev) + def circuit(x, z): + RZ_01 = pnp.array( + [ + [pnp.exp(-1j * z / 2), 0.0, 0.0], + [0.0, pnp.exp(1j * z / 2), 0.0], + [0.0, 0.0, 1.0], + ] + ) + + c = pnp.cos(x / 2) + s = pnp.sin(x / 2) * 1j + RX_01 = pnp.array([[c, -s, 0.0], [-s, c, 0.0], [0.0, 0.0, 1.0]]) + + qml.QutritUnitary(RZ_01, wires=0) + qml.QutritUnitary(RX_01, wires=1) + return qml.probs(wires=0) + + res = circuit(0.1, 0.6) + + assert res.shape == expected_shape + assert circuit.device._shot_vector == shot_vector + assert circuit.device.shots == total_shots + + # test gradient works + # TODO: Uncomment after parametric operations are added for qutrits and decomposition + # for QutritUnitary exists + # res = qml.jacobian(circuit, argnum=[0, 1])(0.1, 0.6) + + shot_data = [ + [[1, 2, 3, 10], [(1, 1), (2, 1), (3, 1), (10, 1)], (4, 3, 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, 3, 2), + 27, + ], + [[10, 10, 10], [(10, 3)], (3, 3, 2), 30], + [[(10, 3)], [(10, 3)], (3, 3, 2), 30], + ] + + @pytest.mark.autograd + @pytest.mark.parametrize("shot_list,shot_vector,expected_shape,total_shots", shot_data) + def test_multiple_probs( + self, mock_qutrit_device_shots, shot_list, shot_vector, expected_shape, total_shots + ): + """Test multiple probability returns""" + dev = mock_qutrit_device_shots(wires=2, shots=shot_list) + + @qml.qnode(dev) + def circuit(U): + qml.QutritUnitary(np.eye(3), wires=0) + qml.QutritUnitary(np.eye(3), wires=0) + qml.QutritUnitary(U, wires=[0, 1]) + return qml.probs(wires=0), qml.probs(wires=1) + + res = circuit(pnp.eye(9)) + + assert res.shape == expected_shape + assert circuit.device._shot_vector == shot_vector + assert circuit.device.shots == total_shots + + # test gradient works + # TODO: Uncomment after parametric operations are added for qutrits and decomposition + # for QutritUnitary exists + # res = qml.jacobian(circuit, argnum=[0])(pnp.eye(9, dtype=np.complex128)) + + class TestUnimplemented: """Tests for class methods that aren't implemented @@ -634,13 +1038,6 @@ def test_adjoint_jacobian(self, mock_qutrit_device): with pytest.raises(NotImplementedError): dev.adjoint_jacobian(tape) - def test_density_matrix(self, mock_qutrit_device): - """Test that density_matrix is unimplemented""" - dev = mock_qutrit_device() - - with pytest.raises(NotImplementedError): - dev.density_matrix(wires=0) - def test_state(self, mock_qutrit_device): """Test that state is unimplemented""" dev = mock_qutrit_device() @@ -648,23 +1045,23 @@ def test_state(self, mock_qutrit_device): with pytest.raises(NotImplementedError): dev.state() + def test_density_matrix(self, mock_qutrit_device): + """Test that density_matrix is unimplemented""" + dev = mock_qutrit_device() + + with pytest.raises(qml.QuantumFunctionError, match="Unsupported return type"): + dev.density_matrix(wires=0) + def test_vn_entropy(self, mock_qutrit_device): """Test that vn_entropy is unimplemented""" dev = mock_qutrit_device() - with pytest.raises(NotImplementedError): + with pytest.raises(qml.QuantumFunctionError, match="Unsupported return type"): dev.vn_entropy(wires=0, log_base=3) def test_mutual_info(self, mock_qutrit_device): """Test that mutual_info is unimplemented""" dev = mock_qutrit_device() - with pytest.raises(NotImplementedError): + with pytest.raises(qml.QuantumFunctionError, match="Unsupported return type"): dev.mutual_info(0, 1, log_base=3) - - def test_statistics(self, mock_qutrit_device): - """Test that statistics is unimplemented""" - dev = mock_qutrit_device() - - with pytest.raises(NotImplementedError): - dev.statistics([qml.Identity(wires=0)])