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)