diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index db0af43a71c..6e4d0fa4a1e 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,6 +2,11 @@

New features since last release

+ +* A new pytorch device, `qml.device('default.qubit.torch', wires=wires)`, supports + backpropogation with the torch interface. + [(#1225)](https://github.com/PennyLaneAI/pennylane/pull/1360) + * The ability to define *batch* transforms has been added via the new `@qml.batch_transform` decorator. [(#1493)](https://github.com/PennyLaneAI/pennylane/pull/1493) @@ -364,10 +369,9 @@ and requirements-ci.txt (unpinned). This latter would be used by the CI. This release contains contributions from (in alphabetical order): - -Vishnu Ajith, Akash Narayanan B, Thomas Bromley, Tanya Garg, Josh Izaac, Prateek Jain, Johannes Jakob Meyer, Pratul Saini, Maria Schuld, -Ingrid Strandberg, David Wierichs, Vincent Wong. - +Vishnu Ajith, Akash Narayanan B, Thomas Bromley, Tanya Garg, Josh Izaac, Prateek Jain, Christina Lee, +Johannes Jakob Meyer, Esteban Payares, Pratul Saini, Maria Schuld, Arshpreet Singh, Ingrid Strandberg, +Slimane Thabet, David Wierichs, Vincent Wong. # Release 0.17.0 (current release) diff --git a/pennylane/devices/__init__.py b/pennylane/devices/__init__.py index 291df3fc927..2090dc5a3d4 100644 --- a/pennylane/devices/__init__.py +++ b/pennylane/devices/__init__.py @@ -24,11 +24,13 @@ default_qubit default_qubit_jax + default_qubit_torch default_qubit_tf default_qubit_autograd default_gaussian default_mixed tf_ops + torch_ops autograd_ops tests """ diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py index 647283ea3d7..8da6c67c7a8 100644 --- a/pennylane/devices/default_qubit.py +++ b/pennylane/devices/default_qubit.py @@ -502,7 +502,7 @@ def expval(self, observable, shot_range=None, bin_size=None): if isinstance(coeff, qml.numpy.tensor) and not coeff.requires_grad: coeff = qml.math.toarray(coeff) - res = res + ( + res = qml.math.convert_like(res, product) + ( qml.math.cast(qml.math.convert_like(coeff, product), "complex128") * product ) return qml.math.real(res) @@ -536,6 +536,7 @@ def capabilities(cls): returns_state=True, passthru_devices={ "tf": "default.qubit.tf", + "torch": "default.qubit.torch", "autograd": "default.qubit.autograd", "jax": "default.qubit.jax", }, @@ -609,7 +610,7 @@ def _apply_state_vector(self, state, device_wires): if state.ndim != 1 or n_state_vector != 2 ** len(device_wires): raise ValueError("State vector must be of length 2**wires.") - if not np.allclose(np.linalg.norm(state, ord=2), 1.0, atol=tolerance): + if not qml.math.allclose(qml.math.linalg.norm(state, ord=2), 1.0, atol=tolerance): raise ValueError("Sum of amplitudes-squared does not equal one.") if len(device_wires) == self.num_wires and sorted(device_wires) == device_wires: diff --git a/pennylane/devices/default_qubit_torch.py b/pennylane/devices/default_qubit_torch.py new file mode 100644 index 00000000000..711331ba723 --- /dev/null +++ b/pennylane/devices/default_qubit_torch.py @@ -0,0 +1,301 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""This module contains a PyTorch implementation of the :class:`~.DefaultQubit` +reference plugin. +""" +import semantic_version + +try: + import torch + + VERSION_SUPPORT = semantic_version.match(">=1.8.1", torch.__version__) + if not VERSION_SUPPORT: + raise ImportError("default.qubit.torch device requires Torch>=1.8.1") + +except ImportError as e: + raise ImportError("default.qubit.torch device requires Torch>=1.8.1") from e + +import numpy as np +from pennylane.operation import DiagonalOperation +from pennylane.devices import torch_ops +from . import DefaultQubit + + +class DefaultQubitTorch(DefaultQubit): + """Simulator plugin based on ``"default.qubit"``, written using PyTorch. + + **Short name:** ``default.qubit.torch`` + + This device provides a pure-state qubit simulator written using PyTorch. + As a result, it supports classical backpropagation as a means to compute the Jacobian. This can + be faster than the parameter-shift rule for analytic quantum gradients + when the number of parameters to be optimized is large. + + To use this device, you will need to install PyTorch: + + .. code-block:: console + + pip install torch>=1.8.0 + + **Example** + + The ``default.qubit.torch`` is designed to be used with end-to-end classical backpropagation + (``diff_method="backprop"``) and the PyTorch interface. This is the default method + of differentiation when creating a QNode with this device. + + Using this method, the created QNode is a 'white-box', and is + tightly integrated with your PyTorch computation: + + .. code-block:: python + + dev = qml.device("default.qubit.torch", wires=1) + + @qml.qnode(dev, interface="torch", diff_method="backprop") + def circuit(x): + qml.RX(x[1], wires=0) + qml.Rot(x[0], x[1], x[2], wires=0) + return qml.expval(qml.PauliZ(0)) + + >>> weights = torch.tensor([0.2, 0.5, 0.1], requires_grad=True) + >>> res = circuit(weights) + >>> res.backward() + >>> print(weights.grad) + tensor([-2.2527e-01, -1.0086e+00, 1.3878e-17]) + + Autograd mode will also work when using classical backpropagation: + + >>> def cost(weights): + ... return torch.sum(circuit(weights)**3) - 1 + >>> res = circuit(weights) + >>> res.backward() + >>> print(weights.grad) + tensor([-4.5053e-01, -2.0173e+00, 5.9837e-17]) + + Executing the pipeline in PyTorch will allow the whole computation to be run on the GPU, + and therefore providing an acceleration. Your parameters need to be instantiated on the same + device as the backend device. + + .. code-block:: python + + dev = qml.device("default.qubit.torch", wires=1, torch_device='cuda') + + @qml.qnode(dev, interface="torch", diff_method="backprop") + def circuit(x): + qml.RX(x[1], wires=0) + qml.Rot(x[0], x[1], x[2], wires=0) + return qml.expval(qml.PauliZ(0)) + + >>> weights = torch.tensor([0.2, 0.5, 0.1], requires_grad=True, device='cuda') + >>> res = circuit(weights) + >>> res.backward() + >>> print(weights.grad) + tensor([-2.2527e-01, -1.0086e+00, 1.3878e-17]) + + + There are a couple of things to keep in mind when using the ``"backprop"`` + differentiation method for QNodes: + + * You must use the ``"torch"`` interface for classical backpropagation, as PyTorch is + used as the device backend. + + * Only exact expectation values, variances, and probabilities are differentiable. + When instantiating the device with ``shots!=None``, differentiating QNode + outputs will result in ``None``. + + If you wish to use a different machine-learning interface, or prefer to calculate quantum + gradients using the ``parameter-shift`` or ``finite-diff`` differentiation methods, + consider using the ``default.qubit`` device instead. + + Args: + wires (int, Iterable): Number of subsystems represented by the device, + or iterable that contains unique labels for the subsystems. Default 1 if not specified. + shots (None, int): How many times the circuit should be evaluated (or sampled) to estimate + the expectation values. Defaults to ``None`` if not specified, which means + that the device returns analytical results. + If ``shots > 0`` is used, the ``diff_method="backprop"`` + QNode differentiation method is not supported and it is recommended to consider + switching device to ``default.qubit`` and using ``diff_method="parameter-shift"``. + torch_device='cpu' (str): the device on which the computation will be run, ``'cpu'`` or ``'cuda'`` + """ + + name = "Default qubit (Torch) PennyLane plugin" + short_name = "default.qubit.torch" + + parametric_ops = { + "PhaseShift": torch_ops.PhaseShift, + "ControlledPhaseShift": torch_ops.ControlledPhaseShift, + "RX": torch_ops.RX, + "RY": torch_ops.RY, + "RZ": torch_ops.RZ, + "MultiRZ": torch_ops.MultiRZ, + "Rot": torch_ops.Rot, + "CRX": torch_ops.CRX, + "CRY": torch_ops.CRY, + "CRZ": torch_ops.CRZ, + "CRot": torch_ops.CRot, + "IsingXX": torch_ops.IsingXX, + "IsingYY": torch_ops.IsingYY, + "IsingZZ": torch_ops.IsingZZ, + "SingleExcitation": torch_ops.SingleExcitation, + "SingleExcitationPlus": torch_ops.SingleExcitationPlus, + "SingleExcitationMinus": torch_ops.SingleExcitationMinus, + "DoubleExcitation": torch_ops.DoubleExcitation, + "DoubleExcitationPlus": torch_ops.DoubleExcitationPlus, + "DoubleExcitationMinus": torch_ops.DoubleExcitationMinus, + } + + C_DTYPE = torch.complex128 + R_DTYPE = torch.float64 + + _abs = staticmethod(torch.abs) + _einsum = staticmethod(torch.einsum) + _flatten = staticmethod(torch.flatten) + _reshape = staticmethod(torch.reshape) + _roll = staticmethod(torch.roll) + _stack = staticmethod(lambda arrs, axis=0, out=None: torch.stack(arrs, axis=axis, out=out)) + _tensordot = staticmethod( + lambda a, b, axes: torch.tensordot( + a, b, axes if isinstance(axes, int) else tuple(map(list, axes)) + ) + ) + _transpose = staticmethod(lambda a, axes=None: a.permute(*axes)) + _asnumpy = staticmethod(lambda x: x.cpu().numpy()) + _conj = staticmethod(torch.conj) + _imag = staticmethod(torch.imag) + _norm = staticmethod(torch.norm) + _flatten = staticmethod(torch.flatten) + + def __init__(self, wires, *, shots=None, analytic=None, torch_device="cpu"): + self._torch_device = torch_device + super().__init__(wires, shots=shots, cache=0, analytic=analytic) + + # Move state to torch device (e.g. CPU, GPU, XLA, ...) + self._state.requires_grad = True + self._state = self._state.to(self._torch_device) + self._pre_rotated_state = self._state + + @staticmethod + def _asarray(a, dtype=None): + if isinstance(a, list): + # Handle unexpected cases where we don't have a list of tensors + if not isinstance(a[0], torch.Tensor): + res = np.asarray(a) + res = torch.from_numpy(res) + else: + res = torch.cat([torch.reshape(i, (-1,)) for i in a], dim=0) + res = torch.cat([torch.reshape(i, (-1,)) for i in res], dim=0) + else: + res = torch.as_tensor(a, dtype=dtype) + return res + + @staticmethod + def _dot(x, y): + if x.device != y.device: + if x.device != "cpu": + return torch.tensordot(x, y.to(x.device), dims=1) + if y.device != "cpu": + return torch.tensordot(x.to(y.device), y, dims=1) + + return torch.tensordot(x, y, dims=1) + + def _cast(self, a, dtype=None): + return torch.as_tensor(self._asarray(a, dtype=dtype), device=self._torch_device) + + @staticmethod + def _reduce_sum(array, axes): + if not axes: + return array + return torch.sum(array, dim=axes) + + @staticmethod + def _conj(array): + if isinstance(array, torch.Tensor): + return torch.conj(array) + return np.conj(array) + + @staticmethod + def _scatter(indices, array, new_dimensions): + + # `array` is now a torch tensor + tensor = array + new_tensor = torch.zeros(new_dimensions, dtype=tensor.dtype, device=tensor.device) + new_tensor[indices] = tensor + return new_tensor + + @classmethod + def capabilities(cls): + capabilities = super().capabilities().copy() + capabilities.update(passthru_interface="torch", supports_reversible_diff=False) + return capabilities + + def _get_unitary_matrix(self, unitary): + """Return the matrix representing a unitary operation. + + Args: + unitary (~.Operation): a PennyLane unitary operation + + Returns: + torch.Tensor[complex]: Returns a 2D matrix representation of + the unitary in the computational basis, or, in the case of a diagonal unitary, + a 1D array representing the matrix diagonal. + """ + op_name = unitary.base_name + if op_name in self.parametric_ops: + if op_name == "MultiRZ": + mat = self.parametric_ops[op_name]( + *unitary.parameters, len(unitary.wires), device=self._torch_device + ) + else: + mat = self.parametric_ops[op_name](*unitary.parameters, device=self._torch_device) + if unitary.inverse: + if isinstance(unitary, DiagonalOperation): + mat = self._conj(mat) + else: + mat = self._transpose(self._conj(mat), axes=[1, 0]) + return mat + + if isinstance(unitary, DiagonalOperation): + return self._asarray(unitary.eigvals, dtype=self.C_DTYPE) + return self._asarray(unitary.matrix, dtype=self.C_DTYPE) + + def sample_basis_states(self, number_of_states, state_probability): + """Sample from the computational basis states based on the state + probability. + + This is an auxiliary method to the ``generate_samples`` method. + + Args: + number_of_states (int): the number of basis states to sample from + state_probability (torch.Tensor[float]): the computational basis probability vector + + Returns: + List[int]: the sampled basis states + """ + return super().sample_basis_states( + number_of_states, state_probability.cpu().detach().numpy() + ) + + def _apply_operation(self, state, operation): + """Applies operations to the input state. + + Args: + state (torch.Tensor[complex]): input state + operation (~.Operation): operation to apply on the device + + Returns: + torch.Tensor[complex]: output state + """ + if state.device != self._torch_device: + state = state.to(self._torch_device) + return super()._apply_operation(state, operation) diff --git a/pennylane/devices/tests/conftest.py b/pennylane/devices/tests/conftest.py index a748006be6a..7d6844c3080 100755 --- a/pennylane/devices/tests/conftest.py +++ b/pennylane/devices/tests/conftest.py @@ -33,7 +33,12 @@ # Number of shots to call the devices with N_SHOTS = 1e6 # List of all devices that are included in PennyLane -LIST_CORE_DEVICES = {"default.qubit", "default.qubit.tf", "default.qubit.autograd"} +LIST_CORE_DEVICES = { + "default.qubit", + "default.qubit.torch", + "default.qubit.tf", + "default.qubit.autograd", +} @pytest.fixture(scope="function") diff --git a/pennylane/devices/tests/test_properties.py b/pennylane/devices/tests/test_properties.py index 6d32d94563b..e5e19609769 100755 --- a/pennylane/devices/tests/test_properties.py +++ b/pennylane/devices/tests/test_properties.py @@ -27,6 +27,14 @@ except ImportError: TF_SUPPORT = False +try: + import torch + + TORCH_SUPPORT = True + +except ImportError: + TORCH_SUPPORT = False + try: import jax @@ -130,11 +138,10 @@ def test_passthru_interface_is_correct(self, device_kwargs): pytest.skip("No passthru_interface capability specified by device.") interface = cap["passthru_interface"] - assert interface in ["tf", "autograd", "jax"] # for new interface, add test case + assert interface in ["tf", "autograd", "jax", "torch"] # for new interface, add test case qfunc = qfunc_with_scalar_input(cap["model"]) - qnode = qml.QNode(qfunc, dev) - qnode.interface = interface + qnode = qml.QNode(qfunc, dev, interface=interface) # assert that we can do a simple gradient computation in the passthru interface # without raising an error @@ -161,6 +168,15 @@ def test_passthru_interface_is_correct(self, device_kwargs): else: pytest.skip("Cannot import jax") + if interface == "torch": + if TORCH_SUPPORT: + x = torch.tensor(0.1, requires_grad=True) + res = qnode(x) + res.backward() + assert hasattr(x, "grad") + else: + pytest.skip("Cannot import torch") + def test_provides_jacobian(self, device_kwargs): """Test that the device computes the jacobian.""" device_kwargs["wires"] = 1 diff --git a/pennylane/devices/torch_ops.py b/pennylane/devices/torch_ops.py new file mode 100644 index 00000000000..3c58d8ca64c --- /dev/null +++ b/pennylane/devices/torch_ops.py @@ -0,0 +1,467 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +r""" +Utility functions and numerical implementations of quantum operations PyTorch device. +""" + +import torch +import numpy as np +from pennylane.utils import pauli_eigs + + +C_DTYPE = torch.complex128 +R_DTYPE = torch.float64 + + +# Instantiating on the device (rather than moving) is approx. 100x faster +def op_matrix(elements): + r"""Decorator to instantiate a tensor on a device. + + Args: + element : torch.Tensor + + Returns: + lambda dev : torch.Tensor elements instantiated on torch device dev + """ + return lambda dev: torch.as_tensor(elements, dtype=C_DTYPE, device=dev) + + +I_array = np.array([[1, 0], [0, 1]]) +X_array = np.array([[0, 1], [1, 0]]) +Y_array = np.array([[0j, -1j], [1j, 0j]]) +Z_array = np.array([[1, 0], [0, -1]]) + + +I = op_matrix(I_array) +X = op_matrix(X_array) +Y = op_matrix(Y_array) +Z = op_matrix(Z_array) + +II = op_matrix(np.eye(4)) +ZZ = op_matrix(np.kron(Z_array, Z_array)) +XX = op_matrix(np.kron(X_array, X_array)) +YY = op_matrix(np.kron(Y_array, Y_array)) + + +IX = op_matrix(np.kron(I_array, X_array)) +IY = op_matrix(np.kron(I_array, Y_array)) +IZ = op_matrix(np.kron(I_array, Z_array)) + +ZI = op_matrix(np.kron(Z_array, I_array)) +ZX = op_matrix(np.kron(Z_array, X_array)) +ZY = op_matrix(np.kron(Z_array, Y_array)) + +A = op_matrix(np.array([[0, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 0]])) +B = op_matrix(np.array([[0, 0, 0, 0], [0, 0, -1, 0], [0, 1, 0, 0], [0, 0, 0, 0]])) +C = op_matrix(np.array([[1, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 1]])) + +USin = op_matrix( + np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ] + ) +) + +UCos = op_matrix( + np.array( + [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ] + ) +) + +I4_array = np.eye(16) +I4_array[3, 3] = 0 +I4_array[-4, -4] = 0 +I4 = op_matrix(I4_array) + + +def PhaseShift(phi, device=None): + r"""One-qubit phase shift. + + Args: + phi (float): phase shift angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: diagonal part of the phase shift matrix + """ + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + p = torch.exp(1j * phi) + return torch.tensor([1, 0], dtype=torch.complex128, device=device) + p * torch.tensor( + [0, 1], dtype=torch.complex128, device=device + ) + + +def ControlledPhaseShift(phi, device=None): + r"""Two-qubit controlled phase shift. + + Args: + phi (float): phase shift angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: diagonal part of the controlled phase shift matrix + """ + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + p = torch.exp(1j * phi) + return torch.tensor([1, 1, 1, 0], dtype=torch.complex128) + p * torch.tensor( + [0, 0, 0, 1], dtype=torch.complex128 + ) + + +def RX(theta, device=None): + r"""One-qubit rotation about the x axis. + + Args: + theta (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: unitary 2x2 rotation matrix :math:`e^{-i \sigma_x \theta/2}` + """ + theta = torch.as_tensor(theta, dtype=C_DTYPE, device=device) + return torch.cos(theta / 2) * I(device) + 1j * torch.sin(-theta / 2) * X(device) + + +def RY(theta, device=None): + r"""One-qubit rotation about the y axis. + + Args: + theta (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: unitary 2x2 rotation matrix :math:`e^{-i \sigma_y \theta/2}` + """ + theta = torch.as_tensor(theta, dtype=C_DTYPE, device=device) + + return torch.cos(theta / 2) * I(device) + 1j * torch.sin(-theta / 2) * Y(device) + + +def RZ(theta, device=None): + r"""One-qubit rotation about the z axis. + + Args: + theta (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: the diagonal part of the rotation matrix :math:`e^{-i \sigma_z \theta/2}` + """ + theta = torch.as_tensor(theta, dtype=C_DTYPE, device=device) + p = torch.exp(-0.5j * theta) + return p * torch.tensor([1, 0], dtype=torch.complex128, device=device) + torch.conj( + p + ) * torch.tensor([0, 1], dtype=torch.complex128, device=device) + + +def MultiRZ(theta, n, device=None): + r"""Arbitrary multi Z rotation. + + Args: + theta (float): rotation angle + n (int): number of wires the rotation acts on + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: diagonal part of the MultiRZ matrix + """ + + theta = torch.as_tensor(theta, dtype=C_DTYPE, device=device) + eigs = torch.as_tensor(pauli_eigs(n), dtype=C_DTYPE, device=device) + return torch.exp(-1j * theta / 2 * eigs) + + +def Rot(a, b, c, device=None): + r"""Arbitrary one-qubit rotation using three Euler angles. + + Args: + a,b,c (float): rotation angles + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: unitary 2x2 rotation matrix ``rz(c) @ ry(b) @ rz(a)`` + """ + return torch.diag(RZ(c, device)) @ RY(b, device) @ torch.diag(RZ(a, device)) + + +def CRX(theta, device=None): + r"""Two-qubit controlled rotation about the x axis. + + Args: + theta (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: unitary 4x4 rotation matrix + :math:`|0\rangle\langle 0|\otimes \mathbb{I}+|1\rangle\langle 1|\otimes R_x(\theta)` + """ + theta = torch.as_tensor(theta, dtype=C_DTYPE, device=device) + return ( + torch.cos(theta / 4) ** 2 * II(device) + - 1j * torch.sin(theta / 2) / 2 * IX(device) + + torch.sin(theta / 4) ** 2 * ZI(device) + + 1j * torch.sin(theta / 2) / 2 * ZX(device) + ) + + +def CRY(theta, device): + r"""Two-qubit controlled rotation about the y axis. + + Args: + theta (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: unitary 4x4 rotation matrix :math:`|0\rangle\langle 0|\otimes \mathbb{I}+|1\rangle\langle 1|\otimes R_y(\theta)` + """ + theta = torch.as_tensor(theta, dtype=C_DTYPE, device=device) + return ( + torch.cos(theta / 4) ** 2 * II(device) + - 1j * torch.sin(theta / 2) / 2 * IY(device) + + torch.sin(theta / 4) ** 2 * ZI(device) + + 1j * torch.sin(theta / 2) / 2 * ZY(device) + ) + + +def CRZ(theta, device): + r"""Two-qubit controlled rotation about the z axis. + + Args: + theta (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: diagonal part of the 4x4 rotation matrix + :math:`|0\rangle\langle 0|\otimes \mathbb{I}+|1\rangle\langle 1|\otimes R_z(\theta)` + """ + theta = torch.as_tensor(theta, dtype=C_DTYPE, device=device) + return torch.cat([torch.as_tensor([1.0, 1.0], device=device), RZ(theta, device)], dim=0) + + +def CRot(a, b, c, device): + r"""Arbitrary two-qubit controlled rotation using three Euler angles. + + Args: + a,b,c (float): rotation angles + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: unitary 4x4 rotation matrix + :math:`|0\rangle\langle 0|\otimes \mathbb{I}+|1\rangle\langle 1|\otimes R(a,b,c)` + """ + return torch.diag(CRZ(c, device)) @ CRY(b, device) @ torch.diag(CRZ(a, device)) + + +def IsingXX(phi, device): + r"""Ising XX coupling gate + + .. math:: XX(\phi) = \begin{bmatrix} + \cos(\phi / 2) & 0 & 0 & -i \sin(\phi / 2) \\ + 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ + 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ + -i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) + \end{bmatrix}. + + Args: + phi (float): rotation angle :math:`\phi` + device: torch device on which the computation is made 'cpu' or 'cuda' + Returns: + torch.Tensor[complex]:: unitary 4x4 rotation matrix + """ + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + return torch.cos(phi / 2) * II(device) - 1j * torch.sin(phi / 2) * XX(device) + + +def IsingYY(phi, device): + r"""Ising YY coupling gate + + .. math:: YY(\phi) = \begin{bmatrix} + \cos(\phi / 2) & 0 & 0 & i \sin(\phi / 2) \\ + 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ + 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ + i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) + \end{bmatrix}. + + Args: + phi (float): rotation angle :math:`\phi` + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]:: unitary 4x4 rotation matrix + """ + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + return torch.cos(phi / 2) * II(device) - 1j * torch.sin(phi / 2) * YY(device) + + +def IsingZZ(phi, device): + r"""Ising ZZ coupling gate + + .. math:: ZZ(\phi) = \begin{bmatrix} + e^{-i \phi / 2} & 0 & 0 & 0 \\ + 0 & e^{i \phi / 2} & 0 & 0 \\ + 0 & 0 & e^{i \phi / 2} & 0 \\ + 0 & 0 & 0 & e^{-i \phi / 2} + \end{bmatrix}. + + Args: + phi (float): rotation :math:`\phi` + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]:: unitary 4x4 rotation matrix + """ + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + e_m = torch.exp(-1j * phi / 2) + e = torch.exp(1j * phi / 2) + return torch.as_tensor([[e_m, 0, 0, 0], [0, e, 0, 0], [0, 0, e, 0], [0, 0, 0, e_m]]) + + +def SingleExcitation(phi, device): + r"""Single excitation rotation. + + Args: + phi (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: Single excitation rotation matrix + """ + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + c = torch.cos(phi / 2) + s = torch.sin(phi / 2) + + return c * A(device) + s * B(device) + C(device) + + +def SingleExcitationPlus(phi, device): + r"""Single excitation rotation with positive phase-shift outside the rotation subspace. + + Args: + phi (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: Single excitation rotation matrix with positive phase-shift + """ + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + c = torch.cos(phi / 2) + s = torch.sin(phi / 2) + e = torch.exp(1j * phi / 2) + + return c * A(device) + s * B(device) + e * C(device) + + +def SingleExcitationMinus(phi, device): + r"""Single excitation rotation with negative phase-shift outside the rotation subspace. + + Args: + phi (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: Single excitation rotation matrix with negative phase-shift + """ + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + c = torch.cos(phi / 2) + s = torch.sin(phi / 2) + e = torch.exp(-1j * phi / 2) + + return c * A(device) + s * B(device) + e * C(device) + + +def DoubleExcitation(phi, device): + r"""Double excitation rotation. + + Args: + phi (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: Double excitation rotation matrix + """ + + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + c = torch.cos(phi / 2) + s = torch.sin(phi / 2) + + return I4(device) + c * UCos(device) + s * USin(device) + + +def DoubleExcitationPlus(phi, device): + r"""Double excitation rotation with positive phase-shift. + + Args: + phi (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: rotation matrix + """ + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + c = torch.cos(phi / 2) + s = torch.sin(phi / 2) + e = torch.exp(1j * phi / 2) + + return e * I4(device) + c * UCos(device) + s * USin(device) + + +def DoubleExcitationMinus(phi, device): + r"""Double excitation rotation with negative phase-shift. + + Args: + phi (float): rotation angle + device: torch device on which the computation is made 'cpu' or 'cuda' + + Returns: + torch.Tensor[complex]: rotation matrix + """ + phi = torch.as_tensor(phi, dtype=C_DTYPE, device=device) + c = torch.cos(phi / 2) + s = torch.sin(phi / 2) + e = torch.exp(-1j * phi / 2) + + return e * I4(device) + c * UCos(device) + s * USin(device) diff --git a/pennylane/interfaces/torch.py b/pennylane/interfaces/torch.py index 2bfdfcbcbf5..d6eee5c945f 100644 --- a/pennylane/interfaces/torch.py +++ b/pennylane/interfaces/torch.py @@ -23,7 +23,7 @@ import pennylane as qml from pennylane.queuing import AnnotatedQueue -COMPLEX_SUPPORT = semantic_version.match(">=1.6.0", torch.__version__) +COMPLEX_SUPPORT = semantic_version.match(">=1.8.0", torch.__version__) def args_to_numpy(args): @@ -303,7 +303,7 @@ def apply(cls, tape, dtype=torch.float64): """ if (dtype is torch.complex64 or dtype is torch.complex128) and not COMPLEX_SUPPORT: raise qml.QuantumFunctionError( - "Version 1.6.0 or above of PyTorch must be installed for complex support, " + "Version 1.8.0 or above of PyTorch must be installed for complex support, " "which is required for quantum functions that return the state." ) diff --git a/pennylane/ops/qubit/matrix_ops.py b/pennylane/ops/qubit/matrix_ops.py index be056b38499..da4b560d58b 100644 --- a/pennylane/ops/qubit/matrix_ops.py +++ b/pennylane/ops/qubit/matrix_ops.py @@ -251,16 +251,16 @@ class DiagonalQubitUnitary(DiagonalOperation): @classmethod def _eigvals(cls, *params): - D = np.asarray(params[0]) + D = qml.math.asarray(params[0]) - if not np.allclose(D * D.conj(), np.ones_like(D)): + if not qml.math.allclose(D * D.conj(), qml.math.ones_like(D)): raise ValueError("Operator must be unitary.") return D @staticmethod def decomposition(D, wires): - return [QubitUnitary(np.diag(D), wires=wires)] + return [QubitUnitary(qml.math.diag(D), wires=wires)] def adjoint(self): return DiagonalQubitUnitary(qml.math.conj(self.parameters[0]), wires=self.wires) diff --git a/setup.py b/setup.py index 6d99a623329..6cd0d0fdfc5 100644 --- a/setup.py +++ b/setup.py @@ -44,6 +44,7 @@ 'default.qubit = pennylane.devices:DefaultQubit', 'default.gaussian = pennylane.devices:DefaultGaussian', 'default.qubit.tf = pennylane.devices.default_qubit_tf:DefaultQubitTF', + 'default.qubit.torch = pennylane.devices.default_qubit_torch:DefaultQubitTorch', 'default.qubit.autograd = pennylane.devices.default_qubit_autograd:DefaultQubitAutograd', 'default.qubit.jax = pennylane.devices.default_qubit_jax:DefaultQubitJax', 'default.tensor = pennylane.beta.devices.default_tensor:DefaultTensor', diff --git a/tests/devices/test_default_qubit.py b/tests/devices/test_default_qubit.py index bfe6c6fdb64..c2fb635273e 100644 --- a/tests/devices/test_default_qubit.py +++ b/tests/devices/test_default_qubit.py @@ -1127,6 +1127,7 @@ def test_defines_correct_capabilities(self): "supports_inverse_operations": True, "supports_analytic_computation": True, "passthru_devices": { + "torch": "default.qubit.torch", "tf": "default.qubit.tf", "autograd": "default.qubit.autograd", "jax": "default.qubit.jax", diff --git a/tests/devices/test_default_qubit_autograd.py b/tests/devices/test_default_qubit_autograd.py index 01f4ab921bd..15089353160 100644 --- a/tests/devices/test_default_qubit_autograd.py +++ b/tests/devices/test_default_qubit_autograd.py @@ -54,6 +54,7 @@ def test_defines_correct_capabilities(self): "supports_analytic_computation": True, "passthru_interface": "autograd", "passthru_devices": { + "torch": "default.qubit.torch", "tf": "default.qubit.tf", "autograd": "default.qubit.autograd", "jax": "default.qubit.jax", diff --git a/tests/devices/test_default_qubit_jax.py b/tests/devices/test_default_qubit_jax.py index 0571ecd27a1..ecb4cf6d225 100644 --- a/tests/devices/test_default_qubit_jax.py +++ b/tests/devices/test_default_qubit_jax.py @@ -41,6 +41,7 @@ def test_defines_correct_capabilities(self): "supports_analytic_computation": True, "passthru_interface": "jax", "passthru_devices": { + "torch": "default.qubit.torch", "tf": "default.qubit.tf", "autograd": "default.qubit.autograd", "jax": "default.qubit.jax", diff --git a/tests/devices/test_default_qubit_tf.py b/tests/devices/test_default_qubit_tf.py index 6ea373aa4e7..6ca5d1d5329 100644 --- a/tests/devices/test_default_qubit_tf.py +++ b/tests/devices/test_default_qubit_tf.py @@ -905,6 +905,7 @@ def test_defines_correct_capabilities(self): "supports_analytic_computation": True, "passthru_interface": "tf", "passthru_devices": { + "torch": "default.qubit.torch", "tf": "default.qubit.tf", "autograd": "default.qubit.autograd", "jax": "default.qubit.jax", diff --git a/tests/devices/test_default_qubit_torch.py b/tests/devices/test_default_qubit_torch.py new file mode 100644 index 00000000000..851f202518a --- /dev/null +++ b/tests/devices/test_default_qubit_torch.py @@ -0,0 +1,1569 @@ +# Copyright 2018-2021 Xanadu Quantum Technologies Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Unit tests and integration tests for the ``default.qubit.torch`` device. +""" +from itertools import product + +import numpy as np +import pytest +import cmath +import math + +torch = pytest.importorskip("torch", minversion="1.8.1") + +import pennylane as qml +from pennylane import DeviceError +from pennylane.wires import Wires +from pennylane.devices.default_qubit_torch import DefaultQubitTorch +from gate_data import ( + I, + X, + Y, + Z, + H, + S, + T, + CNOT, + CZ, + SWAP, + CNOT, + Toffoli, + CSWAP, + Rphi, + Rotx, + Roty, + Rotz, + Rot3, + CRotx, + CRoty, + CRotz, + CRot3, + IsingXX, + IsingYY, + IsingZZ, + MultiRZ1, + MultiRZ2, + ControlledPhaseShift, + SingleExcitation, + SingleExcitationPlus, + SingleExcitationMinus, + DoubleExcitation, + DoubleExcitationPlus, + DoubleExcitationMinus, +) + +np.random.seed(42) + + +##################################################### +# Test matrices +##################################################### + +U = torch.tensor( + [ + [0.83645892 - 0.40533293j, -0.20215326 + 0.30850569j], + [-0.23889780 - 0.28101519j, -0.88031770 - 0.29832709j], + ], + dtype=torch.complex128, +) + +U2 = torch.tensor( + [[0, 1, 1, 1], [1, 0, 1, -1], [1, -1, 0, 1], [1, 1, -1, 0]], dtype=torch.complex128 +) / np.sqrt(3) + + +##################################################### +# Define standard qubit operations +##################################################### + +single_qubit = [ + (qml.S, torch.tensor(S)), + (qml.T, torch.tensor(T)), + (qml.PauliX, torch.tensor(X, dtype=torch.complex128)), + (qml.PauliY, torch.tensor(Y)), + (qml.PauliZ, torch.tensor(Z, dtype=torch.complex128)), + (qml.Hadamard, torch.tensor(H, dtype=torch.complex128)), +] +single_qubit_param = [ + (qml.PhaseShift, Rphi), + (qml.RX, Rotx), + (qml.RY, Roty), + (qml.RZ, Rotz), + (qml.MultiRZ, MultiRZ1), +] +two_qubit = [(qml.CZ, CZ), (qml.CNOT, CNOT), (qml.SWAP, SWAP)] +two_qubit_param = [ + (qml.CRX, CRotx), + (qml.CRY, CRoty), + (qml.CRZ, CRotz), + (qml.IsingXX, IsingXX), + (qml.IsingYY, IsingYY), + (qml.IsingZZ, IsingZZ), + (qml.MultiRZ, MultiRZ2), + (qml.ControlledPhaseShift, ControlledPhaseShift), + (qml.SingleExcitation, SingleExcitation), + (qml.SingleExcitationPlus, SingleExcitationPlus), + (qml.SingleExcitationMinus, SingleExcitationMinus), +] +three_qubit = [(qml.Toffoli, Toffoli), (qml.CSWAP, CSWAP)] +four_qubit_param = [ + (qml.DoubleExcitation, DoubleExcitation), + (qml.DoubleExcitationPlus, DoubleExcitationPlus), + (qml.DoubleExcitationMinus, DoubleExcitationMinus), +] + + +##################################################### +# Fixtures +##################################################### + + +@pytest.fixture +def init_state(scope="session"): + """Generates a random initial state""" + + def _init_state(n): + """random initial state""" + torch.manual_seed(42) + state = torch.rand([2 ** n], dtype=torch.complex128) + torch.rand([2 ** n]) * 1j + state /= torch.linalg.norm(state) + return state + + return _init_state + + +##################################################### +# Initialization test +##################################################### + + +def test_analytic_deprecation(): + """Tests if the kwarg `analytic` is used and displays error message.""" + msg = "The analytic argument has been replaced by shots=None. " + msg += "Please use shots=None instead of analytic=True." + + with pytest.raises(DeviceError, match=msg): + qml.device("default.qubit.torch", wires=1, shots=1, analytic=True) + + +##################################################### +# Device-level integration tests +##################################################### + + +class TestApply: + """Test application of PennyLane operations.""" + + def test_basis_state(self, tol): + """Test basis state initialization""" + dev = DefaultQubitTorch(wires=4) + state = torch.tensor([0, 0, 1, 0]) + + dev.apply([qml.BasisState(state, wires=[0, 1, 2, 3])]) + + res = dev.state + expected = torch.zeros([2 ** 4], dtype=torch.complex128) + expected[2] = 1 + + assert isinstance(res, torch.Tensor) + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_invalid_basis_state_length(self, tol): + """Test that an exception is raised if the basis state is the wrong size""" + dev = DefaultQubitTorch(wires=4) + state = torch.tensor([0, 0, 1, 0]) + + with pytest.raises( + ValueError, match=r"BasisState parameter and wires must be of equal length" + ): + dev.apply([qml.BasisState(state, wires=[0, 1, 2])]) + + def test_invalid_basis_state(self, tol): + """Test that an exception is raised if the basis state is invalid""" + dev = DefaultQubitTorch(wires=4) + state = torch.tensor([0, 0, 1, 2]) + + with pytest.raises( + ValueError, match=r"BasisState parameter must consist of 0 or 1 integers" + ): + dev.apply([qml.BasisState(state, wires=[0, 1, 2, 3])]) + + def test_qubit_state_vector(self, init_state, tol): + """Test qubit state vector application""" + dev = DefaultQubitTorch(wires=1) + state = init_state(1) + + dev.apply([qml.QubitStateVector(state, wires=[0])]) + + res = dev.state + expected = state + assert isinstance(res, torch.Tensor) + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_full_subsystem_statevector(self, mocker): + """Test applying a state vector to the full subsystem""" + dev = DefaultQubitTorch(wires=["a", "b", "c"]) + state = torch.tensor([1, 0, 0, 0, 1, 0, 1, 1], dtype=torch.complex128) / 2.0 + state_wires = qml.wires.Wires(["a", "b", "c"]) + + spy = mocker.spy(dev, "_scatter") + dev._apply_state_vector(state=state, device_wires=state_wires) + + assert torch.allclose(torch.reshape(dev._state, (-1,)), state) + spy.assert_not_called() + + def test_partial_subsystem_statevector(self, mocker): + """Test applying a state vector to a subset of wires of the full subsystem""" + dev = DefaultQubitTorch(wires=["a", "b", "c"]) + state = torch.tensor([1, 0, 1, 0], dtype=torch.complex128) / math.sqrt(2.0) + state_wires = qml.wires.Wires(["a", "c"]) + + spy = mocker.spy(dev, "_scatter") + dev._apply_state_vector(state=state, device_wires=state_wires) + res = torch.reshape(torch.sum(dev._state, axis=(1,)), [-1]) + + assert torch.allclose(res, state) + spy.assert_called() + + def test_invalid_qubit_state_vector_size(self): + """Test that an exception is raised if the state + vector is the wrong size""" + dev = DefaultQubitTorch(wires=2) + state = torch.tensor([0, 1]) + + with pytest.raises(ValueError, match=r"State vector must be of length 2\*\*wires"): + dev.apply([qml.QubitStateVector(state, wires=[0, 1])]) + + @pytest.mark.parametrize( + "state", [torch.tensor([0, 12]), torch.tensor([1.0, -1.0], requires_grad=True)] + ) + def test_invalid_qubit_state_vector_norm(self, state): + """Test that an exception is raised if the state + vector is not normalized""" + dev = DefaultQubitTorch(wires=2) + + with pytest.raises(ValueError, match=r"Sum of amplitudes-squared does not equal one"): + dev.apply([qml.QubitStateVector(state, wires=[0])]) + + def test_invalid_state_prep(self): + """Test that an exception is raised if a state preparation is not the + first operation in the circuit.""" + dev = DefaultQubitTorch(wires=2) + state = torch.tensor([0, 12]) + + with pytest.raises( + qml.DeviceError, + match=r"cannot be used after other Operations have already been applied", + ): + dev.apply([qml.PauliZ(0), qml.QubitStateVector(state, wires=[0])]) + + @pytest.mark.parametrize("op,mat", single_qubit) + def test_single_qubit_no_parameters(self, init_state, op, mat, tol): + """Test non-parametrized single qubit operations""" + dev = DefaultQubitTorch(wires=1) + state = init_state(1) + + queue = [qml.QubitStateVector(state, wires=[0])] + queue += [op(wires=0)] + dev.apply(queue) + + res = dev.state + # assert mat.dtype == state.dtype + expected = torch.matmul(mat, state) + assert isinstance(res, torch.Tensor) + assert torch.allclose(res, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("theta", [0.5432, -0.232]) + @pytest.mark.parametrize("op,func", single_qubit_param) + def test_single_qubit_parameters(self, init_state, op, func, theta, tol): + """Test parametrized single qubit operations""" + dev = DefaultQubitTorch(wires=1) + state = init_state(1) + + queue = [qml.QubitStateVector(state, wires=[0])] + queue += [op(theta, wires=0)] + dev.apply(queue) + + res = dev.state + op_mat = torch.tensor(func(theta), dtype=torch.complex128) + expected = torch.matmul(op_mat, state) + assert torch.allclose(res, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("theta", [0.5432, -0.232]) + @pytest.mark.parametrize("op,func", single_qubit_param) + def test_single_qubit_parameters_inverse(self, init_state, op, func, theta, tol): + """Test parametrized single qubit operations""" + dev = DefaultQubitTorch(wires=1) + state = init_state(1) + + queue = [qml.QubitStateVector(state, wires=[0])] + queue += [op(theta, wires=0).inv()] + dev.apply(queue) + + res = dev.state + op_mat = torch.tensor(func(theta), dtype=torch.complex128) + op_mat = torch.transpose(torch.conj(op_mat), 0, 1) + expected = torch.matmul(op_mat, state) + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_rotation(self, init_state, tol): + """Test three axis rotation gate""" + dev = DefaultQubitTorch(wires=1) + state = init_state(1) + + a = 0.542 + b = 1.3432 + c = -0.654 + + queue = [qml.QubitStateVector(state, wires=[0])] + queue += [qml.Rot(a, b, c, wires=0)] + dev.apply(queue) + + res = dev.state + op_mat = torch.tensor(Rot3(a, b, c), dtype=torch.complex128) + expected = op_mat @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_controlled_rotation(self, init_state, tol): + """Test three axis controlled-rotation gate""" + dev = DefaultQubitTorch(wires=2) + state = init_state(2) + + a = 0.542 + b = 1.3432 + c = -0.654 + + queue = [qml.QubitStateVector(state, wires=[0, 1])] + queue += [qml.CRot(a, b, c, wires=[0, 1])] + dev.apply(queue) + + res = dev.state + op_mat = torch.tensor(CRot3(a, b, c), dtype=torch.complex128) + expected = op_mat @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_inverse_operation(self, init_state, tol): + """Test that the inverse of an operation is correctly applied""" + """Test three axis rotation gate""" + dev = DefaultQubitTorch(wires=1) + state = init_state(1) + + a = 0.542 + b = 1.3432 + c = -0.654 + + queue = [qml.QubitStateVector(state, wires=[0])] + queue += [qml.Rot(a, b, c, wires=0).inv()] + dev.apply(queue) + + res = dev.state + op_mat = torch.tensor(Rot3(a, b, c), dtype=torch.complex128) + expected = torch.linalg.inv(op_mat) @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("op,mat", two_qubit) + def test_two_qubit_no_parameters(self, init_state, op, mat, tol): + """Test non-parametrized two qubit operations""" + dev = DefaultQubitTorch(wires=2) + state = init_state(2) + + queue = [qml.QubitStateVector(state, wires=[0, 1])] + queue += [op(wires=[0, 1])] + dev.apply(queue) + + res = dev.state + expected = torch.tensor(mat, dtype=torch.complex128) @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("mat", [U, U2]) + def test_qubit_unitary(self, init_state, mat, tol): + """Test application of arbitrary qubit unitaries""" + N = int(math.log(len(mat), 2)) + dev = DefaultQubitTorch(wires=N) + state = init_state(N) + + queue = [qml.QubitStateVector(state, wires=range(N))] + queue += [qml.QubitUnitary(mat, wires=range(N))] + dev.apply(queue) + + res = dev.state + expected = mat @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_diagonal_qubit_unitary(self, init_state, tol): + """Tests application of a diagonal qubit unitary""" + dev = DefaultQubitTorch(wires=1) + state = init_state(1) + + diag = torch.tensor( + [-1.0 + 1j, 1.0 + 1j], requires_grad=True, dtype=torch.complex128 + ) / math.sqrt(2) + + queue = [qml.QubitStateVector(state, wires=0), qml.DiagonalQubitUnitary(diag, wires=0)] + dev.apply(queue) + + res = dev.state + expected = torch.diag(diag) @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_diagonal_qubit_unitary_inverse(self, init_state, tol): + """Tests application of a diagonal qubit unitary""" + dev = DefaultQubitTorch(wires=1) + state = init_state(1) + + diag = torch.tensor( + [-1.0 + 1j, 1.0 + 1j], requires_grad=True, dtype=torch.complex128 + ) / math.sqrt(2) + + queue = [ + qml.QubitStateVector(state, wires=0), + qml.DiagonalQubitUnitary(diag, wires=0).inv(), + ] + dev.apply(queue) + + res = dev.state + expected = torch.diag(diag).conj() @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("op, mat", three_qubit) + def test_three_qubit_no_parameters(self, init_state, op, mat, tol): + """Test non-parametrized three qubit operations""" + dev = DefaultQubitTorch(wires=3) + state = init_state(3) + + queue = [qml.QubitStateVector(state, wires=[0, 1, 2])] + queue += [op(wires=[0, 1, 2])] + dev.apply(queue) + + res = dev.state + expected = torch.tensor(mat, dtype=torch.complex128) @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("theta", [0.5432, -0.232]) + @pytest.mark.parametrize("op,func", two_qubit_param) + def test_two_qubit_parameters(self, init_state, op, func, theta, tol): + """Test two qubit parametrized operations""" + dev = DefaultQubitTorch(wires=2) + state = init_state(2) + + queue = [qml.QubitStateVector(state, wires=[0, 1])] + queue += [op(theta, wires=[0, 1])] + dev.apply(queue) + + res = dev.state + op_mat = torch.tensor(func(theta), dtype=torch.complex128) + expected = op_mat @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("theta", [0.5432, -0.232]) + @pytest.mark.parametrize("op,func", four_qubit_param) + def test_four_qubit_parameters(self, init_state, op, func, theta, tol): + """Test two qubit parametrized operations""" + dev = DefaultQubitTorch(wires=4) + state = init_state(4) + + queue = [qml.QubitStateVector(state, wires=[0, 1, 2, 3])] + queue += [op(theta, wires=[0, 1, 2, 3])] + dev.apply(queue) + + res = dev.state + op_mat = torch.tensor(func(theta), dtype=torch.complex128) + expected = op_mat @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_apply_ops_above_8_wires_using_special(self): + """Test that special apply methods that involve slicing function correctly when using 9 + wires""" + dev = DefaultQubitTorch(wires=9) + dev._apply_ops = {"CNOT": dev._apply_cnot} + + queue = [qml.CNOT(wires=[1, 2])] + dev.apply(queue) + + +THETA = torch.linspace(0.11, 1, 3, dtype=torch.float64) +PHI = torch.linspace(0.32, 1, 3, dtype=torch.float64) +VARPHI = torch.linspace(0.02, 1, 3, dtype=torch.float64) + + +@pytest.mark.parametrize("theta, phi, varphi", list(zip(THETA, PHI, VARPHI))) +class TestExpval: + """Test expectation values""" + + # test data; each tuple is of the form (GATE, OBSERVABLE, EXPECTED) + single_wire_expval_test_data = [ + (qml.RX, qml.Identity, lambda t, p: torch.tensor([1.0, 1.0], dtype=torch.float64)), + ( + qml.RX, + qml.PauliZ, + lambda t, p: torch.tensor( + [torch.cos(t), torch.cos(t) * torch.cos(p)], dtype=torch.float64 + ), + ), + ( + qml.RY, + qml.PauliX, + lambda t, p: torch.tensor( + [torch.sin(t) * torch.sin(p), torch.sin(p)], dtype=torch.float64 + ), + ), + ( + qml.RX, + qml.PauliY, + lambda t, p: torch.tensor([0, -torch.cos(t) * torch.sin(p)], dtype=torch.float64), + ), + ( + qml.RY, + qml.Hadamard, + lambda t, p: torch.tensor( + [ + torch.sin(t) * torch.sin(p) + torch.cos(t), + torch.cos(t) * torch.cos(p) + torch.sin(p), + ], + dtype=torch.float64, + ) + / math.sqrt(2), + ), + ] + + @pytest.mark.parametrize("gate,obs,expected", single_wire_expval_test_data) + def test_single_wire_expectation(self, gate, obs, expected, theta, phi, varphi, tol): + """Test that single qubit gates with single qubit expectation values""" + dev = DefaultQubitTorch(wires=2) + + with qml.tape.QuantumTape() as tape: + queue = [gate(theta, wires=0), gate(phi, wires=1), qml.CNOT(wires=[0, 1])] + observables = [qml.expval(obs(wires=[i])) for i in range(2)] + + res = dev.execute(tape) + + expected_res = expected(theta, phi) + assert torch.allclose(res, expected_res, atol=tol, rtol=0) + + def test_hermitian_expectation(self, theta, phi, varphi, tol): + """Test that arbitrary Hermitian expectation values are correct""" + dev = DefaultQubitTorch(wires=2) + + Hermitian_mat = torch.tensor( + [[1.02789352, 1.61296440 - 0.3498192j], [1.61296440 + 0.3498192j, 1.23920938 + 0j]], + dtype=torch.complex128, + ) + + with qml.tape.QuantumTape() as tape: + queue = [qml.RY(theta, wires=0), qml.RY(phi, wires=1), qml.CNOT(wires=[0, 1])] + observables = [qml.expval(qml.Hermitian(Hermitian_mat, wires=[i])) for i in range(2)] + + res = dev.execute(tape) + + a = Hermitian_mat[0, 0] + re_b = Hermitian_mat[0, 1].real + d = Hermitian_mat[1, 1] + ev1 = ( + (a - d) * torch.cos(theta) + 2 * re_b * torch.sin(theta) * torch.sin(phi) + a + d + ) / 2 + ev2 = ((a - d) * torch.cos(theta) * torch.cos(phi) + 2 * re_b * torch.sin(phi) + a + d) / 2 + expected = torch.tensor([ev1, ev2], dtype=torch.float64) + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_multi_mode_hermitian_expectation(self, theta, phi, varphi, tol): + """Test that arbitrary multi-mode Hermitian expectation values are correct""" + Hermit_mat2 = torch.tensor( + [ + [-6, 2 + 1j, -3, -5 + 2j], + [2 - 1j, 0, 2 - 1j, -5 + 4j], + [-3, 2 + 1j, 0, -4 + 3j], + [-5 - 2j, -5 - 4j, -4 - 3j, -6], + ], + dtype=torch.complex128, + ) + + dev = DefaultQubitTorch(wires=2) + + with qml.tape.QuantumTape() as tape: + queue = [qml.RY(theta, wires=0), qml.RY(phi, wires=1), qml.CNOT(wires=[0, 1])] + observables = [qml.expval(qml.Hermitian(Hermit_mat2, wires=[0, 1]))] + + res = dev.execute(tape) + + # below is the analytic expectation value for this circuit with arbitrary + # Hermitian observable Hermit_mat2 + expected = 0.5 * ( + 6 * torch.cos(theta) * torch.sin(phi) + - torch.sin(theta) * (8 * torch.sin(phi) + 7 * torch.cos(phi) + 3) + - 2 * torch.sin(phi) + - 6 * torch.cos(phi) + - 6 + ) + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_paulix_pauliy(self, theta, phi, varphi, tol): + """Test that a tensor product involving PauliX and PauliY works correctly""" + dev = qml.device("default.qubit.torch", wires=3) + dev.reset() + + obs = qml.PauliX(0) @ qml.PauliY(2) + + dev.apply( + [ + qml.RX(theta, wires=[0]), + qml.RX(phi, wires=[1]), + qml.RX(varphi, wires=[2]), + qml.CNOT(wires=[0, 1]), + qml.CNOT(wires=[1, 2]), + ], + obs.diagonalizing_gates(), + ) + + res = dev.expval(obs) + + expected = torch.sin(theta) * torch.sin(phi) * torch.sin(varphi) + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_pauliz_identity(self, theta, phi, varphi, tol): + """Test that a tensor product involving PauliZ and Identity works correctly""" + dev = qml.device("default.qubit.torch", wires=3) + dev.reset() + + obs = qml.PauliZ(0) @ qml.Identity(1) @ qml.PauliZ(2) + + dev.apply( + [ + qml.RX(theta, wires=[0]), + qml.RX(phi, wires=[1]), + qml.RX(varphi, wires=[2]), + qml.CNOT(wires=[0, 1]), + qml.CNOT(wires=[1, 2]), + ], + obs.diagonalizing_gates(), + ) + + res = dev.expval(obs) + + expected = torch.cos(varphi) * torch.cos(phi) + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_pauliz_hadamard(self, theta, phi, varphi, tol): + """Test that a tensor product involving PauliZ and PauliY and hadamard works correctly""" + dev = qml.device("default.qubit.torch", wires=3) + obs = qml.PauliZ(0) @ qml.Hadamard(1) @ qml.PauliY(2) + + dev.reset() + dev.apply( + [ + qml.RX(theta, wires=[0]), + qml.RX(phi, wires=[1]), + qml.RX(varphi, wires=[2]), + qml.CNOT(wires=[0, 1]), + qml.CNOT(wires=[1, 2]), + ], + obs.diagonalizing_gates(), + ) + + res = dev.expval(obs) + + expected = -( + torch.cos(varphi) * torch.sin(phi) + torch.sin(varphi) * torch.cos(theta) + ) / math.sqrt(2) + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_hermitian(self, theta, phi, varphi, tol): + """Test that a tensor product involving qml.Hermitian works correctly""" + dev = qml.device("default.qubit.torch", wires=3) + dev.reset() + + Hermit_mat3 = torch.tensor( + [ + [-6, 2 + 1j, -3, -5 + 2j], + [2 - 1j, 0, 2 - 1j, -5 + 4j], + [-3, 2 + 1j, 0, -4 + 3j], + [-5 - 2j, -5 - 4j, -4 - 3j, -6], + ], + dtype=torch.complex128, + ) + + obs = qml.PauliZ(0) @ qml.Hermitian(Hermit_mat3, wires=[1, 2]) + + dev.apply( + [ + qml.RX(theta, wires=[0]), + qml.RX(phi, wires=[1]), + qml.RX(varphi, wires=[2]), + qml.CNOT(wires=[0, 1]), + qml.CNOT(wires=[1, 2]), + ], + obs.diagonalizing_gates(), + ) + + res = dev.expval(obs) + + expected = 0.5 * ( + -6 * torch.cos(theta) * (torch.cos(varphi) + 1) + - 2 * torch.sin(varphi) * (torch.cos(theta) + torch.sin(phi) - 2 * torch.cos(phi)) + + 3 * torch.cos(varphi) * torch.sin(phi) + + torch.sin(phi) + ) + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_hermitian_hermitian(self, theta, phi, varphi, tol): + """Test that a tensor product involving two Hermitian matrices works correctly""" + dev = qml.device("default.qubit.torch", wires=3) + + A1 = torch.tensor([[1, 2], [2, 4]], dtype=torch.complex128) + + A2 = torch.tensor( + [ + [-6, 2 + 1j, -3, -5 + 2j], + [2 - 1j, 0, 2 - 1j, -5 + 4j], + [-3, 2 + 1j, 0, -4 + 3j], + [-5 - 2j, -5 - 4j, -4 - 3j, -6], + ], + dtype=torch.complex128, + ) + + obs = qml.Hermitian(A1, wires=[0]) @ qml.Hermitian(A2, wires=[1, 2]) + + dev.apply( + [ + qml.RX(theta, wires=[0]), + qml.RX(phi, wires=[1]), + qml.RX(varphi, wires=[2]), + qml.CNOT(wires=[0, 1]), + qml.CNOT(wires=[1, 2]), + ], + obs.diagonalizing_gates(), + ) + + res = dev.expval(obs) + + expected = 0.25 * ( + -30 + + 4 * torch.cos(phi) * torch.sin(theta) + + 3 + * torch.cos(varphi) + * (-10 + 4 * torch.cos(phi) * torch.sin(theta) - 3 * torch.sin(phi)) + - 3 * torch.sin(phi) + - 2 + * ( + 5 + + torch.cos(phi) * (6 + 4 * torch.sin(theta)) + + (-3 + 8 * torch.sin(theta)) * torch.sin(phi) + ) + * torch.sin(varphi) + + torch.cos(theta) + * ( + 18 + + 5 * torch.sin(phi) + + 3 * torch.cos(varphi) * (6 + 5 * torch.sin(phi)) + + 2 * (3 + 10 * torch.cos(phi) - 5 * torch.sin(phi)) * torch.sin(varphi) + ) + ) + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_hermitian_identity_expectation(self, theta, phi, varphi, tol): + """Test that a tensor product involving an Hermitian matrix and the identity works correctly""" + dev = qml.device("default.qubit.torch", wires=2) + + A = torch.tensor( + [[1.02789352, 1.61296440 - 0.3498192j], [1.61296440 + 0.3498192j, 1.23920938 + 0j]], + dtype=torch.complex128, + ) + + obs = qml.Hermitian(A, wires=[0]) @ qml.Identity(wires=[1]) + + dev.apply( + [qml.RY(theta, wires=[0]), qml.RY(phi, wires=[1]), qml.CNOT(wires=[0, 1])], + obs.diagonalizing_gates(), + ) + + res = dev.expval(obs) + + a = A[0, 0] + re_b = A[0, 1].real + d = A[1, 1] + expected = ( + (a - d) * torch.cos(theta) + 2 * re_b * torch.sin(theta) * torch.sin(phi) + a + d + ) / 2 + + assert torch.allclose(res, torch.real(expected), atol=tol, rtol=0) + + def test_hermitian_two_wires_identity_expectation(self, theta, phi, varphi, tol): + """Test that a tensor product involving an Hermitian matrix for two wires and the identity works correctly""" + dev = qml.device("default.qubit.torch", wires=3, shots=None) + + A = torch.tensor( + [[1.02789352, 1.61296440 - 0.3498192j], [1.61296440 + 0.3498192j, 1.23920938 + 0j]], + dtype=torch.complex128, + ) + Identity = torch.tensor([[1, 0], [0, 1]]) + H = torch.kron(torch.kron(Identity, Identity), A) + obs = qml.Hermitian(H, wires=[2, 1, 0]) + + dev.apply( + [qml.RY(theta, wires=[0]), qml.RY(phi, wires=[1]), qml.CNOT(wires=[0, 1])], + obs.diagonalizing_gates(), + ) + res = dev.expval(obs) + + a = A[0, 0] + re_b = A[0, 1].real + d = A[1, 1] + + expected = ( + (a - d) * torch.cos(theta) + 2 * re_b * torch.sin(theta) * torch.sin(phi) + a + d + ) / 2 + assert torch.allclose(res, torch.real(expected), atol=tol, rtol=0) + + +@pytest.mark.parametrize("theta, phi, varphi", list(zip(THETA, PHI, VARPHI))) +class TestVar: + """Tests for the variance""" + + def test_var(self, theta, phi, varphi, tol): + """Tests for variance calculation""" + dev = DefaultQubitTorch(wires=1) + # test correct variance for of a rotated state + + with qml.tape.QuantumTape() as tape: + queue = [qml.RX(phi, wires=0), qml.RY(theta, wires=0)] + observables = [qml.var(qml.PauliZ(wires=[0]))] + + res = dev.execute(tape) + expected = 0.25 * ( + 3 - torch.cos(2 * theta) - 2 * torch.cos(theta) ** 2 * torch.cos(2 * phi) + ) + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_var_hermitian(self, theta, phi, varphi, tol): + """Tests for variance calculation using an arbitrary Hermitian observable""" + dev = DefaultQubitTorch(wires=2) + + # test correct variance for of a rotated state + H = torch.tensor([[4, -1 + 6j], [-1 - 6j, 2]], dtype=torch.complex128) + + with qml.tape.QuantumTape() as tape: + queue = [qml.RX(phi, wires=0), qml.RY(theta, wires=0)] + observables = [qml.var(qml.Hermitian(H, wires=[0]))] + + res = dev.execute(tape) + expected = 0.5 * ( + 2 * torch.sin(2 * theta) * torch.cos(phi) ** 2 + + 24 * torch.sin(phi) * torch.cos(phi) * (torch.sin(theta) - torch.cos(theta)) + + 35 * torch.cos(2 * phi) + + 39 + ) + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_paulix_pauliy(self, theta, phi, varphi, tol): + """Test that a tensor product involving PauliX and PauliY works correctly""" + dev = qml.device("default.qubit.torch", wires=3) + + obs = qml.PauliX(0) @ qml.PauliY(2) + + dev.apply( + [ + qml.RX(theta, wires=[0]), + qml.RX(phi, wires=[1]), + qml.RX(varphi, wires=[2]), + qml.CNOT(wires=[0, 1]), + qml.CNOT(wires=[1, 2]), + ], + obs.diagonalizing_gates(), + ) + + res = dev.var(obs) + + expected = ( + 8 * torch.sin(theta) ** 2 * torch.cos(2 * varphi) * torch.sin(phi) ** 2 + - torch.cos(2 * (theta - phi)) + - torch.cos(2 * (theta + phi)) + + 2 * torch.cos(2 * theta) + + 2 * torch.cos(2 * phi) + + 14 + ) / 16 + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_pauliz_hadamard(self, theta, phi, varphi, tol): + """Test that a tensor product involving PauliZ and PauliY and hadamard works correctly""" + dev = qml.device("default.qubit.torch", wires=3) + obs = qml.PauliZ(0) @ qml.Hadamard(1) @ qml.PauliY(2) + + dev.reset() + dev.apply( + [ + qml.RX(theta, wires=[0]), + qml.RX(phi, wires=[1]), + qml.RX(varphi, wires=[2]), + qml.CNOT(wires=[0, 1]), + qml.CNOT(wires=[1, 2]), + ], + obs.diagonalizing_gates(), + ) + + res = dev.var(obs) + + expected = ( + 3 + + torch.cos(2 * phi) * torch.cos(varphi) ** 2 + - torch.cos(2 * theta) * torch.sin(varphi) ** 2 + - 2 * torch.cos(theta) * torch.sin(phi) * torch.sin(2 * varphi) + ) / 4 + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_hermitian(self, theta, phi, varphi, tol): + """Test that a tensor product involving qml.Hermitian works correctly""" + dev = qml.device("default.qubit.torch", wires=3) + + A = torch.tensor( + [ + [-6, 2 + 1j, -3, -5 + 2j], + [2 - 1j, 0, 2 - 1j, -5 + 4j], + [-3, 2 + 1j, 0, -4 + 3j], + [-5 - 2j, -5 - 4j, -4 - 3j, -6], + ], + dtype=torch.complex128, + ) + + obs = qml.PauliZ(0) @ qml.Hermitian(A, wires=[1, 2]) + + dev.apply( + [ + qml.RX(theta, wires=[0]), + qml.RX(phi, wires=[1]), + qml.RX(varphi, wires=[2]), + qml.CNOT(wires=[0, 1]), + qml.CNOT(wires=[1, 2]), + ], + obs.diagonalizing_gates(), + ) + + res = dev.var(obs) + + expected = ( + 1057 + - torch.cos(2 * phi) + + 12 * (27 + torch.cos(2 * phi)) * torch.cos(varphi) + - 2 + * torch.cos(2 * varphi) + * torch.sin(phi) + * (16 * torch.cos(phi) + 21 * torch.sin(phi)) + + 16 * torch.sin(2 * phi) + - 8 * (-17 + torch.cos(2 * phi) + 2 * torch.sin(2 * phi)) * torch.sin(varphi) + - 8 * torch.cos(2 * theta) * (3 + 3 * torch.cos(varphi) + torch.sin(varphi)) ** 2 + - 24 * torch.cos(phi) * (torch.cos(phi) + 2 * torch.sin(phi)) * torch.sin(2 * varphi) + - 8 + * torch.cos(theta) + * ( + 4 + * torch.cos(phi) + * ( + 4 + + 8 * torch.cos(varphi) + + torch.cos(2 * varphi) + - (1 + 6 * torch.cos(varphi)) * torch.sin(varphi) + ) + + torch.sin(phi) + * ( + 15 + + 8 * torch.cos(varphi) + - 11 * torch.cos(2 * varphi) + + 42 * torch.sin(varphi) + + 3 * torch.sin(2 * varphi) + ) + ) + ) / 16 + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + +##################################################### +# QNode-level integration tests +##################################################### + + +class TestQNodeIntegration: + """Integration tests for default.qubit.torch. This test ensures it integrates + properly with the PennyLane UI, in particular the new QNode.""" + + def test_defines_correct_capabilities(self): + """Test that the device defines the right capabilities""" + + dev = qml.device("default.qubit.torch", wires=1) + cap = dev.capabilities() + capabilities = { + "model": "qubit", + "supports_finite_shots": True, + "supports_tensor_observables": True, + "returns_probs": True, + "returns_state": True, + "supports_reversible_diff": False, + "supports_inverse_operations": True, + "supports_analytic_computation": True, + "passthru_interface": "torch", + "passthru_devices": { + "torch": "default.qubit.torch", + "tf": "default.qubit.tf", + "autograd": "default.qubit.autograd", + "jax": "default.qubit.jax", + }, + } + assert cap == capabilities + + def test_load_torch_device(self): + """Test that the torch device plugin loads correctly""" + dev = qml.device("default.qubit.torch", wires=2) + assert dev.num_wires == 2 + assert dev.shots is None + assert dev.short_name == "default.qubit.torch" + assert dev.capabilities()["passthru_interface"] == "torch" + + def test_qubit_circuit(self, tol, torch_device="cpu"): + """Test that the torch device provides correct + result for a simple circuit using the old QNode.""" + p = torch.tensor([0.543], device=torch_device, dtype=torch.float64) + + dev = qml.device("default.qubit.torch", wires=1, torch_device=torch_device) + + @qml.qnode(dev, interface="torch") + def circuit(x): + qml.RX(x, wires=0) + return qml.expval(qml.PauliY(0)) + + expected = -torch.sin(p) + + assert circuit.diff_options["method"] == "backprop" + assert torch.allclose(circuit(p), expected, atol=tol, rtol=0) + + def test_correct_state(self, tol, torch_device="cpu"): + """Test that the device state is correct after applying a + quantum function on the device""" + + dev = qml.device("default.qubit.torch", wires=2, torch_device=torch_device) + + state = dev.state + expected = torch.tensor([1, 0, 0, 0], dtype=torch.complex128) + assert torch.allclose(state, expected, atol=tol, rtol=0) + + @qml.qnode(dev, interface="torch", diff_method="backprop") + def circuit(): + qml.Hadamard(wires=0) + qml.RZ(math.pi / 4, wires=0) + return qml.expval(qml.PauliZ(0)) + + circuit() + state = dev.state + + amplitude = cmath.exp(-1j * cmath.pi / 8) / cmath.sqrt(2) + + expected = torch.tensor([amplitude, 0, amplitude.conjugate(), 0], dtype=torch.complex128) + assert torch.allclose(state, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("theta", [0.5432, -0.232]) + @pytest.mark.parametrize("op,func", single_qubit_param) + def test_one_qubit_param_gates(self, theta, op, func, init_state, tol): + """Test the integration of the one-qubit single parameter rotations by passing + a Torch data structure as a parameter""" + dev = qml.device("default.qubit.torch", wires=1) + state = init_state(1) + + @qml.qnode(dev, interface="torch") + def circuit(params): + qml.QubitStateVector(state, wires=[0]) + op(params[0], wires=[0]) + return qml.expval(qml.PauliZ(0)) + + params = torch.tensor([theta]) + circuit(params) + res = dev.state + expected = torch.tensor(func(theta), dtype=torch.complex128) @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("theta", [0.5432, 4.213]) + @pytest.mark.parametrize("op,func", two_qubit_param) + def test_two_qubit_param_gates(self, theta, op, func, init_state, tol): + """Test the integration of the two-qubit single parameter rotations by passing + a Torch data structure as a parameter""" + dev = qml.device("default.qubit.torch", wires=2) + state = init_state(2) + + @qml.qnode(dev, interface="torch") + def circuit(params): + qml.QubitStateVector(state, wires=[0, 1]) + op(params[0], wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + + # Pass a Torch Variable to the qfunc + params = torch.tensor([theta]) + circuit(params) + res = dev.state + expected = torch.tensor(func(theta), dtype=torch.complex128) @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + @pytest.mark.parametrize("theta", [0.5432, 4.213]) + @pytest.mark.parametrize("op,func", four_qubit_param) + def test_four_qubit_param_gates(self, theta, op, func, init_state, tol): + """Test the integration of the two-qubit single parameter rotations by passing + a Torch data structure as a parameter""" + dev = qml.device("default.qubit.torch", wires=4) + state = init_state(4) + + @qml.qnode(dev, interface="torch") + def circuit(params): + qml.QubitStateVector(state, wires=[0, 1, 2, 3]) + op(params[0], wires=[0, 1, 2, 3]) + return qml.expval(qml.PauliZ(0)) + + # Pass a Torch Variable to the qfunc + params = torch.tensor([theta]) + circuit(params) + res = dev.state + expected = torch.tensor(func(theta), dtype=torch.complex128) @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_controlled_rotation_integration(self, init_state, tol): + """Test the integration of the two-qubit controlled rotation by passing + a Torch data structure as a parameter""" + dev = qml.device("default.qubit.torch", wires=2) + a = 1.7 + b = 1.3432 + c = -0.654 + state = init_state(2) + + @qml.qnode(dev, interface="torch") + def circuit(params): + qml.QubitStateVector(state, wires=[0, 1]) + qml.CRot(params[0], params[1], params[2], wires=[0, 1]) + return qml.expval(qml.PauliZ(0)) + + # Pass a Torch Variable to the qfunc + params = torch.tensor([a, b, c]) + circuit(params) + res = dev.state + expected = torch.tensor(CRot3(a, b, c), dtype=torch.complex128) @ state + assert torch.allclose(res, expected, atol=tol, rtol=0) + + +class TestPassthruIntegration: + """Tests for integration with the PassthruQNode""" + + def test_jacobian_variable_multiply(self, tol): + """Test that jacobian of a QNode with an attached default.qubit.torch device + gives the correct result in the case of parameters multiplied by scalars""" + x = torch.tensor([0.43316321], dtype=torch.float64, requires_grad=True) + y = torch.tensor([0.43316321], dtype=torch.float64, requires_grad=True) + z = torch.tensor([0.43316321], dtype=torch.float64, requires_grad=True) + + dev = qml.device("default.qubit.torch", wires=1) + + @qml.qnode(dev, interface="torch", diff_method="backprop") + def circuit(p): + qml.RX(3 * p[0], wires=0) + qml.RY(p[1], wires=0) + qml.RX(p[2] / 2, wires=0) + return qml.expval(qml.PauliZ(0)) + + res = circuit([x, y, z]) + res.backward() + + expected = torch.cos(3 * x) * torch.cos(y) * torch.cos(z / 2) - torch.sin( + 3 * x + ) * torch.sin(z / 2) + assert torch.allclose(res, expected, atol=tol, rtol=0) + + x_grad = -3 * ( + torch.sin(3 * x) * torch.cos(y) * torch.cos(z / 2) + torch.cos(3 * x) * torch.sin(z / 2) + ) + y_grad = -torch.cos(3 * x) * torch.sin(y) * torch.cos(z / 2) + z_grad = -0.5 * ( + torch.sin(3 * x) * torch.cos(z / 2) + torch.cos(3 * x) * torch.cos(y) * torch.sin(z / 2) + ) + + assert torch.allclose(x.grad, x_grad) + assert torch.allclose(y.grad, y_grad) + assert torch.allclose(z.grad, z_grad) + + def test_jacobian_repeated(self, tol): + """Test that jacobian of a QNode with an attached default.qubit.torch device + gives the correct result in the case of repeated parameters""" + x = torch.tensor(0.43316321, dtype=torch.float64, requires_grad=True) + y = torch.tensor(0.2162158, dtype=torch.float64, requires_grad=True) + z = torch.tensor(0.75110998, dtype=torch.float64, requires_grad=True) + p = torch.tensor([x, y, z], requires_grad=True) + dev = qml.device("default.qubit.torch", wires=1) + + @qml.qnode(dev, interface="torch", diff_method="backprop") + def circuit(x): + qml.RX(x[1], wires=0) + qml.Rot(x[0], x[1], x[2], wires=0) + return qml.expval(qml.PauliZ(0)) + + res = circuit(p) + res.backward() + + expected = torch.cos(y) ** 2 - torch.sin(x) * torch.sin(y) ** 2 + + assert torch.allclose(res, expected, atol=tol, rtol=0) + + expected_grad = torch.tensor( + [ + -torch.cos(x) * torch.sin(y) ** 2, + -2 * (torch.sin(x) + 1) * torch.sin(y) * torch.cos(y), + 0, + ], + dtype=torch.float64, + ) + assert torch.allclose(p.grad, expected_grad, atol=tol, rtol=0) + + def test_jacobian_agrees_backprop_parameter_shift(self, tol): + """Test that jacobian of a QNode with an attached default.qubit.torch device + gives the correct result with respect to the parameter-shift method""" + p = np.array([0.43316321, 0.2162158, 0.75110998, 0.94714242]) + + def circuit(x): + for i in range(0, len(p), 2): + qml.RX(x[i], wires=0) + qml.RY(x[i + 1], wires=1) + for i in range(2): + qml.CNOT(wires=[i, i + 1]) + return qml.expval(qml.PauliZ(0)) # , qml.var(qml.PauliZ(1)) + + dev1 = qml.device("default.qubit.torch", wires=3) + dev2 = qml.device("default.qubit", wires=3) + + circuit1 = qml.QNode(circuit, dev1, diff_method="backprop", interface="torch") + circuit2 = qml.QNode(circuit, dev2, diff_method="parameter-shift") + + p_torch = torch.tensor(p, requires_grad=True) + res = circuit1(p_torch) + res.backward() + + assert np.allclose(res.detach().numpy(), circuit2(p).numpy(), atol=tol, rtol=0) + + p_grad = p_torch.grad + assert np.allclose(p_grad.detach().numpy(), qml.jacobian(circuit2)(p), atol=tol, rtol=0) + + def test_state_differentiability(self, tol): + """Test that the device state can be differentiated""" + dev = qml.device("default.qubit.torch", wires=1) + + @qml.qnode(dev, diff_method="backprop", interface="torch") + def circuit(a): + qml.RY(a, wires=0) + return qml.expval(qml.PauliZ(0)) + + a = torch.tensor(0.54, requires_grad=True) + + circuit(a) + res = torch.abs(dev.state) ** 2 + res = res[1] - res[0] + res.backward() + + grad = a.grad + expected = torch.sin(a) + assert torch.allclose(grad, expected, atol=tol, rtol=0) + + def test_prob_differentiability(self, tol): + """Test that the device probability can be differentiated""" + dev = qml.device("default.qubit.torch", wires=2) + + @qml.qnode(dev, diff_method="backprop", interface="torch") + def circuit(a, b): + qml.RX(a, wires=0) + qml.RY(b, wires=1) + qml.CNOT(wires=[0, 1]) + return qml.probs(wires=[1]) + + a = torch.tensor([0.54], requires_grad=True, dtype=torch.float64) + b = torch.tensor([0.12], requires_grad=True, dtype=torch.float64) + + # get the probability of wire 1 + prob_wire_1 = circuit(a, b) + # compute Prob(|1>_1) - Prob(|0>_1) + res = prob_wire_1[1] - prob_wire_1[0] + res.backward() + + expected = -torch.cos(a) * torch.cos(b) + assert torch.allclose(res, expected, atol=tol, rtol=0) + + assert torch.allclose(a.grad, torch.sin(a) * torch.cos(b), atol=tol, rtol=0) + assert torch.allclose(b.grad, torch.cos(a) * torch.sin(b), atol=tol, rtol=0) + + def test_backprop_gradient(self, tol): + """Tests that the gradient of the qnode is correct""" + dev = qml.device("default.qubit.torch", wires=2) + + @qml.qnode(dev, diff_method="backprop", interface="torch") + def circuit(a, b): + qml.RX(a, wires=0) + qml.CRX(b, wires=[0, 1]) + return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)) + + a = torch.tensor([-0.234], dtype=torch.float64, requires_grad=True) + b = torch.tensor([0.654], dtype=torch.float64, requires_grad=True) + + res = circuit(a, b) + res.backward() + + # the analytic result of evaluating circuit(a, b) + expected_cost = 0.5 * (torch.cos(a) * torch.cos(b) + torch.cos(a) - torch.cos(b) + 1) + + assert torch.allclose(res, expected_cost, atol=tol, rtol=0) + + assert torch.allclose(a.grad, -0.5 * torch.sin(a) * (torch.cos(b) + 1), atol=tol, rtol=0) + assert torch.allclose(b.grad, 0.5 * torch.sin(b) * (1 - torch.cos(a))) + + @pytest.mark.parametrize("operation", [qml.U3, qml.U3.decomposition]) + @pytest.mark.parametrize("diff_method", ["backprop", "finite-diff"]) + def test_torch_interface_gradient(self, operation, diff_method, tol): + """Tests that the gradient of an arbitrary U3 gate is correct + using the PyTorch interface, using a variety of differentiation methods.""" + dev = qml.device("default.qubit.torch", wires=1) + + @qml.qnode(dev, diff_method=diff_method, interface="torch") + def circuit(x, weights, w): + """In this example, a mixture of scalar + arguments, array arguments, and keyword arguments are used.""" + qml.QubitStateVector(1j * torch.tensor([1, -1]) / math.sqrt(2), wires=w) + operation(x, weights[0], weights[1], wires=w) + return qml.expval(qml.PauliX(w)) + + # Check that the correct QNode type is being used. + if diff_method == "backprop": + assert circuit.diff_options["method"] == "backprop" + elif diff_method == "finite-diff": + assert circuit.diff_options["method"] == "numeric" + + def cost(params): + """Perform some classical processing""" + return circuit(params[0], params[1:], w=0) ** 2 + + theta = torch.tensor(0.543, dtype=torch.float64) + phi = torch.tensor(-0.234, dtype=torch.float64) + lam = torch.tensor(0.654, dtype=torch.float64) + + params = torch.tensor([theta, phi, lam], dtype=torch.float64, requires_grad=True) + + res = cost(params) + res.backward() + + # check that the result is correct + expected_cost = ( + torch.sin(lam) * torch.sin(phi) - torch.cos(theta) * torch.cos(lam) * torch.cos(phi) + ) ** 2 + assert torch.allclose(res, expected_cost, atol=tol, rtol=0) + + # check that the gradient is correct + expected_grad = ( + torch.tensor( + [ + torch.sin(theta) * torch.cos(lam) * torch.cos(phi), + torch.cos(theta) * torch.cos(lam) * torch.sin(phi) + + torch.sin(lam) * torch.cos(phi), + torch.cos(theta) * torch.sin(lam) * torch.cos(phi) + + torch.cos(lam) * torch.sin(phi), + ] + ) + * 2 + * (torch.sin(lam) * torch.sin(phi) - torch.cos(theta) * torch.cos(lam) * torch.cos(phi)) + ) + assert torch.allclose(params.grad, expected_grad, atol=tol, rtol=0) + + def test_inverse_operation_jacobian_backprop(self, tol): + """Test that inverse operations work in backprop + mode""" + dev = qml.device("default.qubit.torch", wires=1) + + @qml.qnode(dev, diff_method="backprop", interface="torch") + def circuit(param): + qml.RY(param, wires=0).inv() + return qml.expval(qml.PauliX(0)) + + x = torch.tensor(0.3, requires_grad=True, dtype=torch.float64) + + res = circuit(x) + res.backward() + + assert torch.allclose(res, -torch.sin(x), atol=tol, rtol=0) + + grad = x.grad + assert torch.allclose(grad, -torch.cos(x), atol=tol, rtol=0) + + @pytest.mark.parametrize("interface", ["autograd", "torch"]) + def test_error_backprop_wrong_interface(self, interface, tol): + """Tests that an error is raised if diff_method='backprop' but not using + the torch interface""" + dev = qml.device("default.qubit.torch", wires=1) + + def circuit(x, w=None): + qml.RZ(x, wires=w) + return qml.expval(qml.PauliX(w)) + + with pytest.raises(Exception) as e: + assert qml.qnode(dev, diff_method="autograd", interface=interface)(circuit) + assert ( + str(e.value) + == "Differentiation method autograd not recognized. Allowed options are ('best', 'parameter-shift', 'backprop', 'finite-diff', 'device', 'reversible', 'adjoint')." + ) + + +class TestSamples: + """Tests for sampling outputs""" + + def test_sample_observables(self): + """Test that the device allows for sampling from observables.""" + shots = 100 + dev = qml.device("default.qubit.torch", wires=2, shots=shots) + + @qml.qnode(dev, diff_method="backprop", interface="torch") + def circuit(a): + qml.RX(a, wires=0) + return qml.sample(qml.PauliZ(0)) + + a = torch.tensor(0.54) + res = circuit(a) + + assert torch.is_tensor(res) + assert res.shape == (shots,) + assert torch.allclose(torch.unique(res), torch.tensor([-1, 1], dtype=torch.int64)) + + def test_estimating_marginal_probability(self, tol): + """Test that the probability of a subset of wires is accurately estimated.""" + dev = qml.device("default.qubit.torch", wires=2, shots=1000) + + @qml.qnode(dev, diff_method="backprop", interface="torch") + def circuit(): + qml.PauliX(0) + return qml.probs(wires=[0]) + + res = circuit() + + assert torch.is_tensor(res) + + expected = torch.tensor([0, 1], dtype=torch.float64) + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_estimating_full_probability(self, tol): + """Test that the probability of a subset of wires is accurately estimated.""" + dev = qml.device("default.qubit.torch", wires=2, shots=1000) + + @qml.qnode(dev, diff_method="backprop", interface="torch") + def circuit(): + qml.PauliX(0) + qml.PauliX(1) + return qml.probs(wires=[0, 1]) + + res = circuit() + + assert torch.is_tensor(res) + + expected = torch.tensor([0, 0, 0, 1], dtype=torch.float64) + assert torch.allclose(res, expected, atol=tol, rtol=0) + + def test_estimating_expectation_values(self, tol): + """Test that estimating expectation values using a finite number + of shots produces a numeric tensor""" + dev = qml.device("default.qubit.torch", wires=3, shots=1000) + + @qml.qnode(dev, diff_method="backprop", interface="torch") + def circuit(a, b): + qml.RX(a, wires=[0]) + qml.RX(b, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + + a = torch.tensor(0.543) + b = torch.tensor(0.43) + + res = circuit(a, b) + assert torch.is_tensor(res) + + # We don't check the expected value due to stochasticity, but + # leave it here for completeness. + # expected = [torch.cos(a), torch.cos(a) * torch.cos(b)] + # assert np.allclose(res, expected, atol=tol, rtol=0) + + def test_estimating_expectation_values_not_differentiable(self, tol): + """Test that finite shots results in non-differentiable QNodes""" + + dev = qml.device("default.qubit.torch", wires=3, shots=1000) + + @qml.qnode(dev, diff_method="backprop", interface="torch") + def circuit(a, b): + qml.RX(a, wires=[0]) + qml.RX(b, wires=[1]) + qml.CNOT(wires=[0, 1]) + return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1)) + + a = torch.tensor(0.543) + b = torch.tensor(0.43) + + res = circuit(a, b) + + with pytest.raises(RuntimeError): + res.backward() + + +class TestHighLevelIntegration: + """Tests for integration with higher level components of PennyLane.""" + + def test_qnode_collection_integration(self): + """Test that a PassthruQNode default.qubit.torch works with QNodeCollections.""" + dev = qml.device("default.qubit.torch", wires=2) + + obs_list = [qml.PauliX(0) @ qml.PauliY(1), qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliZ(1)] + qnodes = qml.map(qml.templates.StronglyEntanglingLayers, obs_list, dev, interface="torch") + + assert qnodes.interface == "torch" + + torch.manual_seed(42) + weights = torch.rand( + qml.templates.StronglyEntanglingLayers.shape(n_wires=2, n_layers=2), requires_grad=True + ) + + def cost(weights): + return torch.sum(qnodes(weights)) + + res = cost(weights) + res.backward() + + grad = weights.grad + + assert torch.is_tensor(res) + assert grad.shape == weights.shape + + def test_sampling_analytic_mode(self): + """Test that when sampling with shots=None, dev uses 1000 shots and + raises an error. + """ + dev = qml.device("default.qubit.torch", wires=1, shots=None) + + @qml.qnode(dev, interface="torch", diff_method="backprop") + def circuit(): + return qml.sample(qml.PauliZ(wires=0)) + + with pytest.raises( + qml.QuantumFunctionError, + match="The number of shots has to be explicitly set on the device", + ): + res = circuit() diff --git a/tests/gate_data.py b/tests/gate_data.py index 9000f975742..6d60072abce 100644 --- a/tests/gate_data.py +++ b/tests/gate_data.py @@ -8,6 +8,7 @@ # ======================================================== I = np.eye(2) + # Pauli matrices X = np.array([[0, 1], [1, 0]]) #: Pauli-X matrix Y = np.array([[0, -1j], [1j, 0]]) #: Pauli-Y matrix @@ -15,6 +16,10 @@ H = np.array([[1, 1], [1, -1]]) / math.sqrt(2) #: Hadamard gate +II = np.eye(4, dtype=np.complex128) +XX = np.array(np.kron(X, X), dtype=np.complex128) +YY = np.array(np.kron(Y, Y), dtype=np.complex128) + # Single-qubit projectors StateZeroProjector = np.array([[1, 0], [0, 0]]) StateOneProjector = np.array([[0, 0], [0, 1]]) @@ -241,6 +246,62 @@ def MultiRZ2(theta): ) +def IsingXX(phi): + r"""Ising XX coupling gate + + .. math:: XX(\phi) = \begin{bmatrix} + \cos(\phi / 2) & 0 & 0 & -i \sin(\phi / 2) \\ + 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ + 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ + -i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) + \end{bmatrix}. + + Args: + phi (float): rotation angle :math:`\phi` + Returns: + array[complex]: unitary 4x4 rotation matrix + """ + return np.cos(phi / 2) * II - 1j * np.sin(phi / 2) * XX + + +def IsingYY(phi): + r"""Ising YY coupling gate. + + .. math:: YY(\phi) = \begin{bmatrix} + \cos(\phi / 2) & 0 & 0 & i \sin(\phi / 2) \\ + 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ + 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ + i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) + \end{bmatrix}. + + Args: + phi (float): rotation angle :math:`\phi` + Returns: + array[complex]: unitary 4x4 rotation matrix + """ + return np.cos(phi / 2) * II - 1j * np.sin(phi / 2) * YY + + +def IsingZZ(phi): + r"""Ising ZZ coupling gate + + .. math:: ZZ(\phi) = \begin{bmatrix} + e^{-i \phi / 2} & 0 & 0 & 0 \\ + 0 & e^{i \phi / 2} & 0 & 0 \\ + 0 & 0 & e^{i \phi / 2} & 0 \\ + 0 & 0 & 0 & e^{-i \phi / 2} + \end{bmatrix}. + + Args: + phi (float): rotation angle :math:`\phi` + Returns: + array[complex]: unitary 4x4 rotation matrix + """ + e_m = np.exp(-1j * phi / 2) + e = np.exp(1j * phi / 2) + return np.array([[e_m, 0, 0, 0], [0, e, 0, 0], [0, 0, e, 0], [0, 0, 0, e_m]]) + + def ControlledPhaseShift(phi): r"""Controlled phase shift. diff --git a/tests/interfaces/test_qnode_torch.py b/tests/interfaces/test_qnode_torch.py index e227f9e4124..f25fa3ac8e3 100644 --- a/tests/interfaces/test_qnode_torch.py +++ b/tests/interfaces/test_qnode_torch.py @@ -213,7 +213,7 @@ def test_jacobian_dtype(self, dev_name, diff_method, tol): dev = qml.device(dev_name, wires=2) - @qnode(dev) + @qnode(dev, interface="torch", diff_method=diff_method) def circuit(a, b): qml.RY(a, wires=0) qml.RX(b, wires=1) diff --git a/tests/interfaces/test_tape_torch.py b/tests/interfaces/test_tape_torch.py index e854aba3f17..d418734f29b 100644 --- a/tests/interfaces/test_tape_torch.py +++ b/tests/interfaces/test_tape_torch.py @@ -14,7 +14,7 @@ """Unit tests for the torch interface""" import pytest -torch = pytest.importorskip("torch", minversion="1.3") +torch = pytest.importorskip("torch", minversion="1.8.0") import numpy as np @@ -446,13 +446,13 @@ def test_sampling(self): assert isinstance(res, torch.Tensor) def test_complex_min_version(self, monkeypatch): - """Test if an error is raised when a version of torch before 1.6.0 is used as the dtype + """Test if an error is raised when a version of torch before 1.8.0 is used as the dtype in the apply() method""" with monkeypatch.context() as m: m.setattr(qml.interfaces.torch, "COMPLEX_SUPPORT", False) with pytest.raises( - qml.QuantumFunctionError, match=r"Version 1\.6\.0 or above of PyTorch" + qml.QuantumFunctionError, match=r"Version 1\.8\.0 or above of PyTorch" ): TorchInterface.apply(JacobianTape(), dtype=torch.complex128)