Repository: PennyLaneAI/pennylane
Pull request #1642 (2021-09-10 14:50:32)

Integrate batch execution into a QNode
**Context:** With most of the low-level batch-execution work done, there is now a `execute([tapes], device, gradient_transform)` function that is differentiable, and works on all interfaces 💯 .

However, this is still very low-level! This needs to be packaged up and integrated with PL's QNode abstraction.

I began a prototype PR modifying the existing QNode, but this was problematic:

- It leads to a very difficult to review PR
- it is difficult to change the internal QNode logic whilst ensuring 100% feature parity and stability

Ideally, we want to be able to provide a beta QNode with this new logical pipeline that users and devs can 'opt in' to, while keeping the existing stable QNode as default. This will allow us a way to beta test, drive down bugs, and ensure stability before swapping over to the new logic by default.

**Description of the Change:**

- A new QNode, `pennylane.beta.QNode`, has been added. This QNode has --- as close as possible --- the same behaviour and UI as the existing QNode, but uses the new batch-execution pipeline internally. Aside from this, minor changes due to the new logic include:

  - The ability to pass a gradient transform directly to `diff_method`.
  - The ability to pass the `mode` (whether gradients are accumulated on the forward or backward pass)
  - The ability to configure the cache
  - The removal of interface logic (since this is now handled by `execute()`)

- A new decorator `pennylane.beta.qnode` has been added. It simply wraps `qml.beta.QNode`, avoiding docstring and signature duplication.

- Unit tests have been added.

**Benefits:**

- QNode execution uses the new batch-execution pipeline.

- Due to better handling of temporary shot value by `qml.execute()`, the beta QNode also has better temporary shot handling. Calling a QNode with `shots=` now works for TF and Torch gradients.

- You can define and pass custom gradient transforms when creating QNodes

- When changing the interface of a QNode, and `diff_method="best"`, the QNode will re-compute the 'best' gradient method.

- The beta QNode is simpler, due to dropping all decomposition and interface logic.

**Possible Drawbacks:**

- Does not yet support `qml.specs`

- Does not yet support JAX; this will be enabled when #1573 is merged

- Integration tests are in #1646 

- Decomposition are not yet supported, this will be added in a future PR.

- The `max_diff` option is set to 1 by default. This is a _breaking_ change compared to the existing QNode. It means that if you want second order derivatives **using parameter-shift or finite-differences**, you need to manually instantiate the QNode with `max_diff=2`. This is because higher-order derivatives come with an unavoidable overhead, due to using the autodiff frameworks to compute parameter-shift rules rather than vanilla NumPy.

  Note that `backprop` mode (the default) is not supported.

- The beta QNode supports all differentiation methods as the main QNode, _except_ `reversible`. This is because the `reversible` method was not ported to a gradient transform, and does not exist as a device method (unlike `adjoint`).

**Related GitHub Issues:** n/a


github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [.github/CHANGELOG.md](/PennyLaneAI/pennylane/blob/master/.github/CHANGELOG.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1642](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (bb6626c) into [batch-metric-tensor](https://codecov.io/gh/PennyLaneAI/pennylane/commit/523b765903c54f433b150a842c72ed2fa78c3c65?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (523b765) will **decrease** coverage by `0.02%`.
> The diff coverage is `97.28%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@                   Coverage Diff                   @@
##           batch-metric-tensor    #1642      +/-   ##
=======================================================
- Coverage                99.13%   99.11%   -0.03%     
=======================================================
  Files                      196      197       +1     
  Lines                    14272    14451     +179     
=======================================================
+ Hits                     14149    14323     +174     
- Misses                     123      128       +5     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/gradients/parameter\_shift.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2dyYWRpZW50cy9wYXJhbWV0ZXJfc2hpZnQucHk=) | `98.88% <84.61%> (-1.12%)` | :arrow_down: |
| [pennylane/new\_qnode.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL25ld19xbm9kZS5weQ==) | `98.22% <98.22%> (ø)` | |
| [pennylane/transforms/batch\_transform.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvYmF0Y2hfdHJhbnNmb3JtLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/gradients/gradient\_transform.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2dyYWRpZW50cy9ncmFkaWVudF90cmFuc2Zvcm0ucHk=) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [523b765...bb6626c](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1642?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

josh146: [ch8958]

Patch:
diff --git a/doc/code/qml_beta.rst b/doc/code/qml_beta.rst
index 0449d2137..2e2391687 100644
--- a/doc/code/qml_beta.rst
+++ b/doc/code/qml_beta.rst
@@ -6,6 +6,10 @@ and features.
 
 .. currentmodule:: pennylane.beta
 
+.. automodapi:: pennylane.beta
+    :include-all-objects:
+    :no-inheritance-diagram:
+
 .. automodapi:: pennylane.beta.devices
     :include-all-objects:
     :no-inheritance-diagram:
diff --git a/doc/introduction/interfaces.rst b/doc/introduction/interfaces.rst
index bbfded4f1..e25a3bc8c 100644
--- a/doc/introduction/interfaces.rst
+++ b/doc/introduction/interfaces.rst
@@ -100,7 +100,7 @@ Currently, there are four built-in interfaces: :doc:`NumPy <interfaces/numpy>`,
 interfaces make each of these libraries quantum-aware, allowing quantum circuits to be treated just
 like any other operation.
 
-In PennyLane, an interface is declared when creating a :class:`~.QNode`, e.g.,
+In PennyLane, an interface is declared when creating a :class:`QNode <pennylane.QNode>`, e.g.,
 
 .. code-block:: python
 
diff --git a/doc/introduction/interfaces/numpy.rst b/doc/introduction/interfaces/numpy.rst
index 64f5ef6f8..d8ae3b222 100644
--- a/doc/introduction/interfaces/numpy.rst
+++ b/doc/introduction/interfaces/numpy.rst
@@ -3,7 +3,7 @@
 NumPy interface
 ===============
 
-.. note:: This interface is the default interface supported by PennyLane's :class:`~.QNode`.
+.. note:: This interface is the default interface supported by PennyLane's :class:`QNode <pennylane.QNode>`.
 
 
 Using the NumPy interface
diff --git a/doc/introduction/interfaces/tf.rst b/doc/introduction/interfaces/tf.rst
index 9ea01ea92..c8bd99681 100644
--- a/doc/introduction/interfaces/tf.rst
+++ b/doc/introduction/interfaces/tf.rst
@@ -30,7 +30,7 @@ Construction via keyword
 ------------------------
 
 The :ref:`QNode decorator <intro_vcirc_decorator>` is the recommended way for creating
-:class:`~.QNode` objects in PennyLane. The only change required to construct a TensorFlow-capable
+:class:`QNode <pennylane.QNode>` objects in PennyLane. The only change required to construct a TensorFlow-capable
 QNode is to specify the ``interface='tf'`` keyword argument:
 
 .. code-block:: python
diff --git a/doc/introduction/interfaces/torch.rst b/doc/introduction/interfaces/torch.rst
index 53cc25a76..dc130bce6 100644
--- a/doc/introduction/interfaces/torch.rst
+++ b/doc/introduction/interfaces/torch.rst
@@ -30,7 +30,7 @@ Construction via keyword
 ------------------------
 
 The :ref:`QNode decorator <intro_vcirc_decorator>` is the recommended way for creating
-:class:`~.QNode` objects in PennyLane. The only change required to construct a PyTorch-capable
+:class:`QNode <pennylane.QNode>` objects in PennyLane. The only change required to construct a PyTorch-capable
 QNode is to specify the ``interface='torch'`` keyword argument:
 
 .. code-block:: python
diff --git a/doc/introduction/templates.rst b/doc/introduction/templates.rst
index 364350c83..6e2ac1b72 100644
--- a/doc/introduction/templates.rst
+++ b/doc/introduction/templates.rst
@@ -14,7 +14,7 @@ literature, such architectures are commonly known as an *ansatz*.
 
     Templates are constructed out of **structured combinations** of the quantum operations
     provided by PennyLane. This means that **template functions can only be used within a
-    valid** :class:`~.QNode`.
+    valid** :class:`QNode <pennylane.QNode>`.
 
 PennyLane conceptually distinguishes different types of templates, such as :ref:`Embeddings <intro_ref_temp_emb>`,
 :ref:`Layers <intro_ref_temp_lay>`, :ref:`State preparations <intro_ref_temp_stateprep>` and
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index f89b319c5..80dc3059b 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -76,6 +76,48 @@
   For more usage details, please see the
   [classical Jacobian docstring](https://pennylane.readthedocs.io/en/latest/code/api/pennylane.transforms.classical_jacobian.html).
 
+* A new, experimental QNode has been added, that adds support for batch execution of circuits,
+  custom quantum gradient support, and arbitrary order derivatives. This QNode is available via
+  `qml.beta.QNode`, and `@qml.beta.qnode`.
+  [(#1642)](https://github.com/PennyLaneAI/pennylane/pull/1642)
+
+  It differs from the standard QNode in several ways:
+
+  - Custom gradient transforms can be specified as the differentiation method:
+  
+    ```python
+    @qml.gradients.gradient_transform
+    def my_gradient_transform(tape):
+        ...
+        return tapes, processing_fn
+
+    @qml.beta.qnode(dev, diff_method=my_gradient_transform)
+    def circuit():
+    ```
+
+  - Arbitrary :math:`n`-th order derivatives are supported on hardware using
+    gradient transforms such as the parameter-shift rule. To specify that an :math:`n`-th
+    order derivative of a QNode will be computed, the `max_diff` argument should be set.
+    By default, this is set to 1 (first-order derivatives only).
+
+  - Internally, if multiple circuits are generated for execution simultaneously, they
+    will be packaged into a single job for execution on the device. This can lead to
+    significant performance improvement when executing the QNode on remote
+    quantum hardware.
+
+  In an upcoming release, this QNode will replace the existing one. If you come across any bugs
+  while using this QNode, please let us know via a [bug
+  report](https://github.com/PennyLaneAI/pennylane/issues/new?assignees=&labels=bug+%3Abug%3A&template=bug_report.yml&title=%5BBUG%5D)
+  on our GitHub bug tracker.
+
+  Currently, this beta QNode does not support the following features:
+
+  - Circuit decompositions
+  - Non-mutability via the `mutable` keyword argument
+  - Viewing specifications with `qml.specs`
+
+  It is also not tested with the `qml.qnn` module.
+
 <h3>Improvements</h3>
 
 * The `qml.metric_tensor` transform has been improved with regards to
diff --git a/pennylane/beta/__init__.py b/pennylane/beta/__init__.py
index ab03b010a..aba358dd2 100644
--- a/pennylane/beta/__init__.py
+++ b/pennylane/beta/__init__.py
@@ -14,3 +14,4 @@
 """
 This module contains experimental, contributed, and beta code.
 """
+from .qnode import QNode, qnode
diff --git a/pennylane/beta/qnode.py b/pennylane/beta/qnode.py
new file mode 100644
index 000000000..43cec0195
--- /dev/null
+++ b/pennylane/beta/qnode.py
@@ -0,0 +1,569 @@
+# 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 the QNode class and qnode decorator.
+"""
+# pylint: disable=too-many-instance-attributes,too-many-arguments,protected-access
+from collections.abc import Sequence
+import functools
+import inspect
+import warnings
+
+import pennylane as qml
+from pennylane import Device
+from pennylane.interfaces.batch import set_shots, SUPPORTED_INTERFACES
+
+
+class QNode:
+    """Represents a quantum node in the hybrid computational graph.
+
+    .. warning::
+
+        This QNode is a beta feature. It differs from the standard :class:`~.pennylane.QNode`
+        in several ways:
+
+        - Custom gradient transforms can be specified as the differentiation method.
+
+        - Arbitrary :math:`n`-th order derivatives are supported on hardware using
+          gradient transforms such as the parameter-shift rule. To specify that an :math:`n`-th
+          order derivative of a QNode will be computed, the ``max_diff`` argument should be set.
+          By default, this is set to 1 (first-order derivatives only).
+
+        - Internally, if multiple circuits are generated for execution simultaneously, they
+          will be packaged into a single job for execution on the device. This can lead to
+          significant performance improvement when executing the QNode on remote
+          quantum hardware.
+
+        In an upcoming release, this QNode will replace the existing one. If you come across any
+        bugs while using this QNode, please let us know via a `bug report <https://github.com/PennyLaneAI/pennylane/issues/new?assignees=&labels=bug+%3Abug%3A&template=bug_report.yml&title=%5BBUG%5D>`__
+        on our GitHub bug tracker.
+
+        Currently, this beta QNode does not support the following features:
+
+        - Circuit decompositions
+        - Non-mutability via the ``mutable`` keyword argument
+        - Viewing specifications with ``qml.specs``
+
+        It is also not tested with the :mod:`~.qnn` module.
+
+    A *quantum node* contains a :ref:`quantum function <intro_vcirc_qfunc>`
+    (corresponding to a :ref:`variational circuit <glossary_variational_circuit>`)
+    and the computational device it is executed on.
+
+    The QNode calls the quantum function to construct a :class:`~.QuantumTape` instance representing
+    the quantum circuit.
+
+    Args:
+        func (callable): a quantum function
+        device (~.Device): a PennyLane-compatible device
+        interface (str): The interface that will be used for classical backpropagation.
+            This affects the types of objects that can be passed to/returned from the QNode:
+
+            * ``"autograd"``: Allows autograd to backpropagate
+              through the QNode. The QNode accepts default Python types
+              (floats, ints, lists) as well as NumPy array arguments,
+              and returns NumPy arrays.
+
+            * ``"torch"``: Allows PyTorch to backpropogate
+              through the QNode. The QNode accepts and returns Torch tensors.
+
+            * ``"tf"``: Allows TensorFlow in eager mode to backpropogate
+              through the QNode. The QNode accepts and returns
+              TensorFlow ``tf.Variable`` and ``tf.tensor`` objects.
+
+            * ``"jax"``: Allows JAX to backpropogate
+              through the QNode. The QNode accepts and returns
+              JAX ``DeviceArray`` objects.
+
+            * ``None``: The QNode accepts default Python types
+              (floats, ints, lists) as well as NumPy array arguments,
+              and returns NumPy arrays. It does not connect to any
+              machine learning library automatically for backpropagation.
+
+        diff_method (str or .gradient_transform): The method of differentiation to use in the created QNode.
+            Can either be a :class:`~.gradient_transform`, which includes all quantum gradient
+            transforms in the :mod:`qml.gradients <.gradients>` module, or a string. The following
+            strings are allowed:
+
+            * ``"best"``: Best available method. Uses classical backpropagation or the
+              device directly to compute the gradient if supported, otherwise will use
+              the analytic parameter-shift rule where possible with finite-difference as a fallback.
+
+            * ``"device"``: Queries the device directly for the gradient.
+              Only allowed on devices that provide their own gradient computation.
+
+            * ``"backprop"``: Use classical backpropagation. Only allowed on simulator
+              devices that are classically end-to-end differentiable, for example
+              :class:`default.tensor.tf <~.DefaultTensorTF>`. Note that the returned
+              QNode can only be used with the machine-learning framework supported
+              by the device.
+
+            * ``"adjoint"``: Uses an `adjoint method <https://arxiv.org/abs/2009.02823>`__ that
+              reverses through the circuit after a forward pass by iteratively applying the inverse
+              (adjoint) gate. Only allowed on supported simulator devices such as
+              :class:`default.qubit <~.DefaultQubit>`.
+
+            * ``"parameter-shift"``: Use the analytic parameter-shift
+              rule for all supported quantum operation arguments, with finite-difference
+              as a fallback.
+
+            * ``"finite-diff"``: Uses numerical finite-differences for all quantum operation
+              arguments.
+
+            * ``None``: QNode cannot be differentiated. Works the same as ``interface=None``.
+
+        max_expansion (int): The number of times the internal circuit should be expanded when
+            executed on a device. Expansion occurs when an operation or measurement is not
+            supported, and results in a gate decomposition. If any operations in the decomposition
+            remain unsupported by the device, another expansion occurs.
+        mode (str): Whether the gradients should be computed on the forward
+            pass (``forward``) or the backward pass (``backward``). Only applies
+            if the device is queried for the gradient; gradient transform
+            functions available in ``qml.gradients`` are only supported on the backward
+            pass.
+        cache (bool or dict or Cache): Whether to cache evaluations. This can result in
+            a significant reduction in quantum evaluations during gradient computations.
+            If ``True``, a cache with corresponding ``cachesize`` is created for each batch
+            execution. If ``False``, no caching is used. You may also pass your own cache
+            to be used; this can be any object that implements the special methods
+            ``__getitem__()``, ``__setitem__()``, and ``__delitem__()``, such as a dictionary.
+        cachesize (int): The size of any auto-created caches. Only applies when ``cache=True``.
+        max_diff (int): If ``diff_method`` is a gradient transform, this option specifies
+            the maximum number of derivatives to support. Increasing this value allows
+            for higher order derivatives to be extracted, at the cost of additional
+            (classical) computational overhead during the backwards pass.
+
+    Keyword Args:
+        **kwargs: Any additional keyword arguments provided are passed to the differentiation
+            method. Please refer to the :mod:`qml.gradients <.gradients>` module for details
+            on supported options for your chosen gradient transform.
+
+    **Example**
+
+    QNodes can be created by decorating a quantum function:
+
+    >>> dev = qml.device("default.qubit", wires=1)
+    >>> @qml.beta.qnode(dev)
+    ... def circuit(x):
+    ...     qml.RX(x, wires=0)
+    ...     return expval(qml.PauliZ(0))
+
+    or by instantiating the class directly:
+
+    >>> def circuit(x):
+    ...     qml.RX(x, wires=0)
+    ...     return expval(qml.PauliZ(0))
+    >>> dev = qml.device("default.qubit", wires=1)
+    >>> qnode = qml.beta.QNode(circuit, dev)
+    """
+
+    def __init__(
+        self,
+        func,
+        device,
+        interface="autograd",
+        diff_method="best",
+        max_expansion=10,
+        mode="best",
+        cache=True,
+        cachesize=10000,
+        max_diff=1,
+        **gradient_kwargs,
+    ):
+        if interface not in SUPPORTED_INTERFACES:
+            raise qml.QuantumFunctionError(
+                f"Unknown interface {interface}. Interface must be "
+                f"one of {SUPPORTED_INTERFACES}."
+            )
+
+        if not isinstance(device, Device):
+            raise qml.QuantumFunctionError(
+                "Invalid device. Device must be a valid PennyLane device."
+            )
+
+        if "shots" in inspect.signature(func).parameters:
+            warnings.warn(
+                "Detected 'shots' as an argument to the given quantum function. "
+                "The 'shots' argument name is reserved for overriding the number of shots "
+                "taken by the device. Its use outside of this context should be avoided.",
+                UserWarning,
+            )
+            self._qfunc_uses_shots_arg = True
+        else:
+            self._qfunc_uses_shots_arg = False
+
+        # input arguments
+        self.func = func
+        self.device = device
+        self._interface = interface
+        self.diff_method = diff_method
+        self.max_expansion = max_expansion
+
+        # execution keyword arguments
+        self.execute_kwargs = {
+            "mode": mode,
+            "cache": cache,
+            "cachesize": cachesize,
+            "max_diff": max_diff,
+        }
+
+        # internal data attributes
+        self._tape = None
+        self._qfunc_output = None
+        self._user_gradient_kwargs = gradient_kwargs
+        self._original_device = device
+        self.gradient_fn = None
+        self.gradient_kwargs = None
+
+        self._update_gradient_fn()
+        functools.update_wrapper(self, func)
+
+    def __repr__(self):
+        """String representation."""
+        detail = "<QNode: wires={}, device='{}', interface='{}', diff_method='{}'>"
+        return detail.format(
+            self.device.num_wires,
+            self.device.short_name,
+            self.interface,
+            self.diff_method,
+        )
+
+    @property
+    def interface(self):
+        """The interface used by the QNode"""
+        return self._interface
+
+    @interface.setter
+    def interface(self, value):
+        if value not in SUPPORTED_INTERFACES:
+            raise qml.QuantumFunctionError(
+                f"Unknown interface {value}. Interface must be one of {SUPPORTED_INTERFACES}."
+            )
+
+        self._interface = value
+        self._update_gradient_fn()
+
+    def _update_gradient_fn(self):
+        if self.diff_method is None:
+            self._interface = None
+            self.gradient_fn = None
+            self.gradient_kwargs = {}
+            return
+
+        self.gradient_fn, self.gradient_kwargs, self.device = self.get_gradient_fn(
+            self._original_device, self.interface, self.diff_method
+        )
+        self.gradient_kwargs.update(self._user_gradient_kwargs or {})
+
+    def _update_original_device(self):
+        # FIX: If the qnode swapped the device, increase the num_execution value on the original device.
+        # In the long run, we should make sure that the user's device is the one
+        # actually run so she has full control. This could be done by changing the class
+        # of the user's device before and after executing the tape.
+
+        if self.device is not self._original_device:
+            self._original_device._num_executions += 1  # pylint: disable=protected-access
+
+            # Update for state vector simulators that have the _pre_rotated_state attribute
+            if hasattr(self._original_device, "_pre_rotated_state"):
+                self._original_device._pre_rotated_state = self.device._pre_rotated_state
+
+            # Update for state vector simulators that have the _state attribute
+            if hasattr(self._original_device, "_state"):
+                self._original_device._state = self.device._state
+
+    # pylint: disable=too-many-return-statements
+    @staticmethod
+    def get_gradient_fn(device, interface, diff_method="best"):
+        """Determine the best differentiation method, interface, and device
+        for a requested device, interface, and diff method.
+
+        Args:
+            device (.Device): PennyLane device
+            interface (str): name of the requested interface
+            diff_method (str or .gradient_transform): The requested method of differentiation.
+                If a string, allowed options are ``"best"``, ``"backprop"``, ``"adjoint"``, ``"device"``,
+                ``"parameter-shift"``, or ``"finite-diff"``. A gradient transform may
+                also be passed here.
+
+        Returns:
+            tuple[str or .gradient_transform, dict, .Device: Tuple containing the ``gradient_fn``,
+            ``gradient_kwargs``, and the device to use when calling the execute function.
+        """
+
+        if diff_method == "best":
+            return QNode.get_best_method(device, interface)
+
+        if diff_method == "backprop":
+            return QNode._validate_backprop_method(device, interface)
+
+        if diff_method == "adjoint":
+            return QNode._validate_adjoint_method(device)
+
+        if diff_method == "device":
+            return QNode._validate_device_method(device)
+
+        if diff_method == "parameter-shift":
+            return QNode._validate_parameter_shift(device)
+
+        if diff_method == "finite-diff":
+            return qml.gradients.finite_diff, {}, device
+
+        if isinstance(diff_method, str):
+            raise qml.QuantumFunctionError(
+                f"Differentiation method {diff_method} not recognized. Allowed "
+                "options are ('best', 'parameter-shift', 'backprop', 'finite-diff', 'device', 'reversible', 'adjoint')."
+            )
+
+        if isinstance(diff_method, qml.gradients.gradient_transform):
+            return diff_method, {}, device
+
+        raise qml.QuantumFunctionError(
+            f"Differentiation method {diff_method} must be a gradient transform or a string."
+        )
+
+    @staticmethod
+    def get_best_method(device, interface):
+        """Returns the 'best' differentiation method
+        for a particular device and interface combination.
+
+        This method attempts to determine support for differentiation
+        methods using the following order:
+
+        * ``"device"``
+        * ``"backprop"``
+        * ``"parameter-shift"``
+        * ``"finite-diff"``
+
+        The first differentiation method that is supported (going from
+        top to bottom) will be returned.
+
+        Args:
+            device (.Device): PennyLane device
+            interface (str): name of the requested interface
+
+        Returns:
+            tuple[str or .gradient_transform, dict, .Device: Tuple containing the ``gradient_fn``,
+            ``gradient_kwargs``, and the device to use when calling the execute function.
+        """
+        try:
+            return QNode._validate_device_method(device)
+        except qml.QuantumFunctionError:
+            try:
+                return QNode._validate_backprop_method(device, interface)
+            except qml.QuantumFunctionError:
+                try:
+                    return QNode._validate_parameter_shift(device)
+                except qml.QuantumFunctionError:
+                    return qml.gradients.finite_diff, {}, device
+
+    @staticmethod
+    def _validate_backprop_method(device, interface):
+        # determine if the device supports backpropagation
+        backprop_interface = device.capabilities().get("passthru_interface", None)
+
+        # determine if the device has any child devices that support backpropagation
+        backprop_devices = device.capabilities().get("passthru_devices", None)
+
+        if backprop_interface is not None:
+            # device supports backpropagation natively
+
+            if interface == backprop_interface:
+                return "backprop", {}, device
+
+            raise qml.QuantumFunctionError(
+                f"Device {device.short_name} only supports diff_method='backprop' when using the "
+                f"{backprop_interface} interface."
+            )
+
+        if device.shots is None and backprop_devices is not None:
+
+            # device is analytic and has child devices that support backpropagation natively
+
+            if interface in backprop_devices:
+                # TODO: need a better way of passing existing device init options
+                # to a new device?
+                device = qml.device(
+                    backprop_devices[interface],
+                    wires=device.wires,
+                    shots=device.shots,
+                )
+                return "backprop", {}, device
+
+            raise qml.QuantumFunctionError(
+                f"Device {device.short_name} only supports diff_method='backprop' when using the "
+                f"{list(backprop_devices.keys())} interfaces."
+            )
+
+        raise qml.QuantumFunctionError(
+            f"The {device.short_name} device does not support native computations with "
+            "autodifferentiation frameworks."
+        )
+
+    @staticmethod
+    def _validate_adjoint_method(device):
+        # The conditions below provide a minimal set of requirements that we can likely improve upon in
+        # future, or alternatively summarize within a single device capability. Moreover, we also
+        # need to inspect the circuit measurements to ensure only expectation values are taken. This
+        # cannot be done here since we don't yet know the composition of the circuit.
+
+        supported_device = hasattr(device, "_apply_operation")
+        supported_device = supported_device and hasattr(device, "_apply_unitary")
+        supported_device = supported_device and device.capabilities().get("returns_state")
+        supported_device = supported_device and hasattr(device, "adjoint_jacobian")
+
+        if not supported_device:
+            raise ValueError(
+                f"The {device.short_name} device does not support adjoint differentiation."
+            )
+
+        if device.shots is not None:
+            warnings.warn(
+                "Requested adjoint differentiation to be computed with finite shots."
+                " Adjoint differentiation always calculated exactly.",
+                UserWarning,
+            )
+
+        return "device", {"use_device_state": True, "method": "adjoint_jacobian"}, device
+
+    @staticmethod
+    def _validate_device_method(device):
+        # determine if the device provides its own jacobian method
+        provides_jacobian = device.capabilities().get("provides_jacobian", False)
+
+        if not provides_jacobian:
+            raise qml.QuantumFunctionError(
+                f"The {device.short_name} device does not provide a native "
+                "method for computing the jacobian."
+            )
+
+        return "device", {}, device
+
+    @staticmethod
+    def _validate_parameter_shift(device):
+        model = device.capabilities().get("model", None)
+
+        if model == "qubit":
+            return qml.gradients.param_shift, {}, device
+
+        if model == "cv":
+            return qml.gradients.param_shift_cv, {"dev": device}, device
+
+        raise qml.QuantumFunctionError(
+            f"Device {device.short_name} uses an unknown model ('{model}') "
+            "that does not support the parameter-shift rule."
+        )
+
+    @property
+    def tape(self):
+        """The quantum tape"""
+        return self._tape
+
+    qtape = tape  # for backwards compatibility
+
+    def construct(self, args, kwargs):
+        """Call the quantum function with a tape context, ensuring the operations get queued."""
+
+        if self.interface == "autograd":
+            # HOTFIX: to maintain backwards compatibility existing PennyLane code and demos, here we treat
+            # all inputs that do not explicitly specify `requires_grad=False`
+            # as trainable. This should be removed at some point, forcing users
+            # to specify `requires_grad=True` for trainable parameters.
+            args = [
+                qml.numpy.array(a, requires_grad=True) if not hasattr(a, "requires_grad") else a
+                for a in args
+            ]
+
+        self._tape = qml.tape.JacobianTape()
+
+        with self.tape:
+            self._qfunc_output = self.func(*args, **kwargs)
+
+        params = self.tape.get_parameters(trainable_only=False)
+        self.tape.trainable_params = qml.math.get_trainable_indices(params)
+
+        if not isinstance(self._qfunc_output, Sequence):
+            measurement_processes = (self._qfunc_output,)
+        else:
+            measurement_processes = self._qfunc_output
+
+        if not all(isinstance(m, qml.measure.MeasurementProcess) for m in measurement_processes):
+            raise qml.QuantumFunctionError(
+                "A quantum function must return either a single measurement, "
+                "or a nonempty sequence of measurements."
+            )
+
+        if not all(ret == m for ret, m in zip(measurement_processes, self.tape.measurements)):
+            raise qml.QuantumFunctionError(
+                "All measurements must be returned in the order they are measured."
+            )
+
+        for obj in self.tape.operations + self.tape.observables:
+
+            if getattr(obj, "num_wires", None) is qml.operation.WiresEnum.AllWires:
+                # check here only if enough wires
+                if len(obj.wires) != self.device.num_wires:
+                    raise qml.QuantumFunctionError(
+                        "Operator {} must act on all wires".format(obj.name)
+                    )
+
+    def __call__(self, *args, **kwargs):
+        override_shots = False
+
+        if not self._qfunc_uses_shots_arg:
+            # If shots specified in call but not in qfunc signature,
+            # interpret it as device shots value for this call.
+            override_shots = kwargs.pop("shots", False)
+
+            if override_shots is not False:
+                # Since shots has changed, we need to update the preferred gradient function.
+                # This is because the gradient function chosen at initialization may
+                # no longer be applicable.
+
+                # store the initialization gradient function
+                original_grad_fn = [self.gradient_fn, self.gradient_kwargs, self.device]
+
+                # update the gradient function
+                set_shots(self._original_device, override_shots)(self._update_gradient_fn)()
+
+        # construct the tape
+        self.construct(args, kwargs)
+
+        res = qml.execute(
+            [self.tape],
+            device=self.device,
+            gradient_fn=self.gradient_fn,
+            interface=self.interface,
+            gradient_kwargs=self.gradient_kwargs,
+            override_shots=override_shots,
+            **self.execute_kwargs,
+        )[0]
+
+        if override_shots is not False:
+            # restore the initialization gradient function
+            self.gradient_fn, self.gradient_kwargs, self.device = original_grad_fn
+
+        self._update_original_device()
+
+        if isinstance(self._qfunc_output, Sequence) or (
+            self.tape.is_sampled and self.device._has_partitioned_shots()
+        ):
+            return res
+
+        return qml.math.squeeze(res)
+
+
+qnode = lambda dev, **kwargs: functools.partial(QNode, device=dev, **kwargs)
+qnode.__doc__ = QNode.__doc__
+qnode.__signature__ = inspect.signature(QNode)
diff --git a/pennylane/gradients/__init__.py b/pennylane/gradients/__init__.py
index 14d8e9a74..af06bad16 100644
--- a/pennylane/gradients/__init__.py
+++ b/pennylane/gradients/__init__.py
@@ -31,7 +31,7 @@ These quantum gradient transforms can be used in two ways:
 
 - Transforming quantum circuits directly
 - Registering a quantum gradient strategy for use when performing autodifferentiation
-  with a :class:`~.QNode`.
+  with a :class:`QNode <pennylane.QNode>`.
 
 Overview
 --------
@@ -71,7 +71,7 @@ Registering autodifferentiation gradients
 -----------------------------------------
 
 All PennyLane QNodes are automatically differentiable, and can be included
-seamlessly within an autodiff pipeline. When creating a :class:`~.QNode`, the
+seamlessly within an autodiff pipeline. When creating a :class:`QNode <pennylane.QNode>`, the
 strategy for determining the optimal differentiation strategy is *automated*,
 and takes into account the circuit, device, autodiff framework, and metadata
 (such as whether a finite number of shots are used).
diff --git a/pennylane/gradients/finite_difference.py b/pennylane/gradients/finite_difference.py
index 3a0b5b2b9..a0888f194 100644
--- a/pennylane/gradients/finite_difference.py
+++ b/pennylane/gradients/finite_difference.py
@@ -187,7 +187,7 @@ def finite_diff(tape, argnum=None, h=1e-7, approx_order=1, n=1, strategy="forwar
     parameters with respect to its inputs.
 
     Args:
-        qnode (.QNode or .QuantumTape): quantum tape or QNode to differentiate
+        qnode (pennylane.QNode or .QuantumTape): quantum tape or QNode to differentiate
         argnum (int or list[int] or None): Trainable parameter indices to differentiate
             with respect to. If not provided, the derivatives with respect to all
             trainable parameters are returned.
@@ -237,7 +237,7 @@ def finite_diff(tape, argnum=None, h=1e-7, approx_order=1, n=1, strategy="forwar
 
     .. UsageDetails::
 
-        This gradient transform can also be applied directly to :class:`~.QNode` objects:
+        This gradient transform can also be applied directly to :class:`QNode <pennylane.QNode>` objects:
 
         >>> @qml.qnode(dev)
         ... def circuit(params):
diff --git a/pennylane/gradients/parameter_shift.py b/pennylane/gradients/parameter_shift.py
index d76554206..10c859206 100644
--- a/pennylane/gradients/parameter_shift.py
+++ b/pennylane/gradients/parameter_shift.py
@@ -371,7 +371,7 @@ def param_shift(
     parameters with respect to its inputs.
 
     Args:
-        qnode (.QNode or .QuantumTape): quantum tape or QNode to differentiate
+        qnode (pennylane.QNode or .QuantumTape): quantum tape or QNode to differentiate
         argnum (int or list[int] or None): Trainable parameter indices to differentiate
             with respect to. If not provided, the derivative with respect to all
             trainable indices are returned.
@@ -478,7 +478,7 @@ def param_shift(
 
     .. UsageDetails::
 
-        This gradient transform can be applied directly to :class:`~.QNode` objects:
+        This gradient transform can be applied directly to :class:`QNode <pennylane.QNode>` objects:
 
         >>> @qml.qnode(dev)
         ... def circuit(params):
diff --git a/pennylane/gradients/parameter_shift_cv.py b/pennylane/gradients/parameter_shift_cv.py
index ec38d11a3..39b9fab0c 100644
--- a/pennylane/gradients/parameter_shift_cv.py
+++ b/pennylane/gradients/parameter_shift_cv.py
@@ -595,7 +595,7 @@ def param_shift_cv(
 
     .. UsageDetails::
 
-        This gradient transform can be applied directly to :class:`~.QNode` objects:
+        This gradient transform can be applied directly to :class:`QNode <pennylane.QNode>` objects:
 
         >>> @qml.qnode(dev)
         ... def circuit(params):
diff --git a/pennylane/templates/layers/simplified_two_design.py b/pennylane/templates/layers/simplified_two_design.py
index 8ca767b22..01558e652 100644
--- a/pennylane/templates/layers/simplified_two_design.py
+++ b/pennylane/templates/layers/simplified_two_design.py
@@ -60,7 +60,7 @@ class SimplifiedTwoDesign(Operation):
 
     .. UsageDetails::
 
-        template - here shown for two layers - is used inside a :class:`~.QNode`:
+        template - here shown for two layers - is used inside a :class:`QNode <pennylane.QNode>`:
 
         .. code-block:: python
 
diff --git a/pennylane/transforms/batch_transform.py b/pennylane/transforms/batch_transform.py
index 5c7999ce0..7d62d8804 100644
--- a/pennylane/transforms/batch_transform.py
+++ b/pennylane/transforms/batch_transform.py
@@ -17,6 +17,7 @@ import functools
 import types
 
 import pennylane as qml
+from pennylane.beta import QNode
 
 
 class batch_transform:
@@ -257,7 +258,7 @@ class batch_transform:
             # tapes, fn = some_transform(tape, *transform_args)
             return self.construct(qnode, *targs, **tkwargs)
 
-        if isinstance(qnode, (qml.QNode, qml.ExpvalCost)):
+        if isinstance(qnode, (qml.QNode, QNode, qml.ExpvalCost)):
             # Input is a QNode:
             # result = some_transform(qnode, *transform_args)(*qnode_args)
             wrapper = self.qnode_wrapper(qnode, targs, tkwargs)
diff --git a/pennylane/transforms/classical_jacobian.py b/pennylane/transforms/classical_jacobian.py
index 0e853d547..03b4b1154 100644
--- a/pennylane/transforms/classical_jacobian.py
+++ b/pennylane/transforms/classical_jacobian.py
@@ -27,7 +27,7 @@ def classical_jacobian(qnode, argnum=None):
     arguments and the quantum gate arguments to be extracted.
 
     Args:
-        qnode (.QNode): QNode to compute the (classical) Jacobian of
+        qnode (pennylane.QNode): QNode to compute the (classical) Jacobian of
         argnum (int or Sequence[int]): indices of QNode arguments with respect to which
             the (classical) Jacobian is computed
 
diff --git a/pennylane/transforms/metric_tensor.py b/pennylane/transforms/metric_tensor.py
index 0f4ca9b21..aef150680 100644
--- a/pennylane/transforms/metric_tensor.py
+++ b/pennylane/transforms/metric_tensor.py
@@ -54,7 +54,7 @@ def metric_tensor(tape, diag_approx=False):
         All other parametrized gates will be decomposed if possible.
 
     Args:
-        qnode (.QNode or .QuantumTape): quantum tape or QNode to find the metric tensor of
+        qnode (pennylane.QNode or .QuantumTape): quantum tape or QNode to find the metric tensor of
         diag_approx (bool): if True, use the diagonal approximation. If ``False``, a
         block diagonal approximation of the metric tensor is computed.
         hybrid (bool): Specifies whether classical processing inside a QNode


Test patch:
diff --git a/tests/beta/test_beta_qnode.py b/tests/beta/test_beta_qnode.py
new file mode 100644
index 000000000..a2d1952de
--- /dev/null
+++ b/tests/beta/test_beta_qnode.py
@@ -0,0 +1,900 @@
+# 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 for the QNode"""
+from collections import defaultdict
+import pytest
+import numpy as np
+
+import pennylane as qml
+from pennylane import numpy as pnp
+from pennylane.beta import qnode, QNode
+from pennylane.transforms import draw
+from pennylane.tape import JacobianTape
+
+
+def dummyfunc():
+    return None
+
+
+class TestValidation:
+    """Tests for QNode creation and validation"""
+
+    def test_invalid_interface(self):
+        """Test that an exception is raised for an invalid interface"""
+        dev = qml.device("default.qubit", wires=1)
+        test_interface = "something"
+        expected_error = (
+            fr"Unknown interface {test_interface}\. Interface must be "
+            r"one of \[None, 'autograd', 'numpy', 'torch', 'pytorch', 'tf', 'tensorflow'\]\."
+        )
+
+        with pytest.raises(qml.QuantumFunctionError, match=expected_error):
+            QNode(dummyfunc, dev, interface="something")
+
+    def test_changing_invalid_interface(self):
+        """Test that an exception is raised for an invalid interface
+        on a pre-existing QNode"""
+        dev = qml.device("default.qubit", wires=1)
+        test_interface = "something"
+
+        @qnode(dev)
+        def circuit(x):
+            qml.RX(wires=0)
+            return qml.probs(wires=0)
+
+        expected_error = (
+            fr"Unknown interface {test_interface}\. Interface must be "
+            r"one of \[None, 'autograd', 'numpy', 'torch', 'pytorch', 'tf', 'tensorflow'\]\."
+        )
+
+        with pytest.raises(qml.QuantumFunctionError, match=expected_error):
+            circuit.interface = test_interface
+
+    def test_valid_interface(self):
+        """Test that changing to a valid interface works as expected, and the
+        diff method is updated as required."""
+        torch = pytest.importorskip("torch")
+        dev = qml.device("default.qubit", wires=1)
+
+        @qnode(dev, interface="autograd", diff_method="best")
+        def circuit(x):
+            qml.RX(wires=0)
+            return qml.probs(wires=0)
+
+        assert circuit.device.short_name == "default.qubit.autograd"
+        assert circuit.gradient_fn == "backprop"
+
+        circuit.interface = "torch"
+        assert circuit.device.short_name == "default.qubit.torch"
+        assert circuit.gradient_fn == "backprop"
+
+    def test_invalid_device(self):
+        """Test that an exception is raised for an invalid device"""
+        with pytest.raises(qml.QuantumFunctionError, match="Invalid device"):
+            QNode(dummyfunc, None)
+
+    def test_validate_device_method(self, monkeypatch):
+        """Test that the method for validating the device diff method
+        tape works as expected"""
+        dev = qml.device("default.qubit", wires=1)
+
+        with pytest.raises(
+            qml.QuantumFunctionError,
+            match="does not provide a native method for computing the jacobian",
+        ):
+            QNode._validate_device_method(dev)
+
+        monkeypatch.setitem(dev._capabilities, "provides_jacobian", True)
+        method, diff_options, device = QNode._validate_device_method(dev)
+
+        assert method == "device"
+        assert device is dev
+
+    def test_validate_backprop_method_invalid_device(self):
+        """Test that the method for validating the backprop diff method
+        tape raises an exception if the device does not support backprop."""
+        dev = qml.device("default.gaussian", wires=1)
+
+        with pytest.raises(qml.QuantumFunctionError, match="does not support native computations"):
+            QNode._validate_backprop_method(dev, None)
+
+    def test_validate_backprop_method_invalid_interface(self, monkeypatch):
+        """Test that the method for validating the backprop diff method
+        tape raises an exception if the wrong interface is provided"""
+        dev = qml.device("default.qubit", wires=1)
+        test_interface = "something"
+
+        monkeypatch.setitem(dev._capabilities, "passthru_interface", test_interface)
+
+        with pytest.raises(qml.QuantumFunctionError, match=f"when using the {test_interface}"):
+            QNode._validate_backprop_method(dev, None)
+
+    def test_validate_backprop_method(self, monkeypatch):
+        """Test that the method for validating the backprop diff method
+        tape works as expected"""
+        dev = qml.device("default.qubit", wires=1)
+        test_interface = "something"
+        monkeypatch.setitem(dev._capabilities, "passthru_interface", test_interface)
+
+        method, diff_options, device = QNode._validate_backprop_method(dev, "something")
+
+        assert method == "backprop"
+        assert device is dev
+
+    def test_validate_backprop_child_method(self, monkeypatch):
+        """Test that the method for validating the backprop diff method
+        tape works as expected if a child device supports backprop"""
+        dev = qml.device("default.qubit", wires=1)
+        test_interface = "something"
+
+        orig_capabilities = dev.capabilities().copy()
+        orig_capabilities["passthru_devices"] = {test_interface: "default.gaussian"}
+        monkeypatch.setattr(dev, "capabilities", lambda: orig_capabilities)
+
+        method, diff_options, device = QNode._validate_backprop_method(dev, test_interface)
+
+        assert method == "backprop"
+        assert isinstance(device, qml.devices.DefaultGaussian)
+
+    def test_validate_backprop_child_method_wrong_interface(self, monkeypatch):
+        """Test that the method for validating the backprop diff method
+        tape raises an error if a child device supports backprop but using a different interface"""
+        dev = qml.device("default.qubit", wires=1)
+        test_interface = "something"
+
+        orig_capabilities = dev.capabilities().copy()
+        orig_capabilities["passthru_devices"] = {test_interface: "default.gaussian"}
+        monkeypatch.setattr(dev, "capabilities", lambda: orig_capabilities)
+
+        with pytest.raises(
+            qml.QuantumFunctionError, match=r"when using the \['something'\] interface"
+        ):
+            QNode._validate_backprop_method(dev, "another_interface")
+
+    def test_parameter_shift_qubit_device(self):
+        """Test that the _validate_parameter_shift method
+        returns the correct gradient transform for qubit devices."""
+        dev = qml.device("default.qubit", wires=1)
+        gradient_fn = QNode._validate_parameter_shift(dev)
+        assert gradient_fn[0] is qml.gradients.param_shift
+
+    def test_parameter_shift_cv_device(self):
+        """Test that the _validate_parameter_shift method
+        returns the correct gradient transform for cv devices."""
+        dev = qml.device("default.gaussian", wires=1)
+        gradient_fn = QNode._validate_parameter_shift(dev)
+        assert gradient_fn[0] is qml.gradients.param_shift_cv
+        assert gradient_fn[1] == {"dev": dev}
+
+    def test_parameter_shift_tape_unknown_model(self, monkeypatch):
+        """Test that an unknown model raises an exception"""
+
+        def capabilities(cls):
+            capabilities = cls._capabilities
+            capabilities.update(model="None")
+            return capabilities
+
+        monkeypatch.setattr(qml.devices.DefaultQubit, "capabilities", capabilities)
+        dev = qml.device("default.qubit", wires=1)
+
+        with pytest.raises(
+            qml.QuantumFunctionError, match="does not support the parameter-shift rule"
+        ):
+            QNode._validate_parameter_shift(dev)
+
+    def test_best_method(self, monkeypatch):
+        """Test that the method for determining the best diff method
+        for a given device and interface works correctly"""
+        dev = qml.device("default.qubit", wires=1)
+        monkeypatch.setitem(dev._capabilities, "passthru_interface", "some_interface")
+        monkeypatch.setitem(dev._capabilities, "provides_jacobian", True)
+
+        # device is top priority
+        res = QNode.get_best_method(dev, "another_interface")
+        assert res == ("device", {}, dev)
+
+        # backprop is next priority
+        monkeypatch.setitem(dev._capabilities, "provides_jacobian", False)
+        res = QNode.get_best_method(dev, "some_interface")
+        assert res == ("backprop", {}, dev)
+
+        # The next fallback is parameter-shift.
+        res = QNode.get_best_method(dev, "another_interface")
+        assert res == (qml.gradients.param_shift, {}, dev)
+
+        # finally, if both fail, finite differences is the fallback
+        def capabilities(cls):
+            capabilities = cls._capabilities
+            capabilities.update(model="None")
+            return capabilities
+
+        monkeypatch.setattr(qml.devices.DefaultQubit, "capabilities", capabilities)
+        res = QNode.get_best_method(dev, "another_interface")
+        assert res == (qml.gradients.finite_diff, {}, dev)
+
+    def test_diff_method(self, mocker):
+        """Test that a user-supplied diff method correctly returns the right
+        diff method."""
+        dev = qml.device("default.qubit", wires=1)
+
+        mock_best = mocker.patch("pennylane.beta.QNode.get_best_method")
+        mock_best.return_value = ("best", {}, dev)
+
+        mock_backprop = mocker.patch("pennylane.beta.QNode._validate_backprop_method")
+        mock_backprop.return_value = ("backprop", {}, dev)
+
+        mock_device = mocker.patch("pennylane.beta.QNode._validate_device_method")
+        mock_device.return_value = ("device", {}, dev)
+
+        qn = QNode(dummyfunc, dev, diff_method="best")
+        assert qn.diff_method == "best"
+        assert qn.gradient_fn == "best"
+
+        qn = QNode(dummyfunc, dev, diff_method="backprop")
+        assert qn.diff_method == "backprop"
+        assert qn.gradient_fn == "backprop"
+        mock_backprop.assert_called_once()
+
+        qn = QNode(dummyfunc, dev, diff_method="device")
+        assert qn.diff_method == "device"
+        assert qn.gradient_fn == "device"
+        mock_device.assert_called_once()
+
+        qn = QNode(dummyfunc, dev, diff_method="finite-diff")
+        assert qn.diff_method == "finite-diff"
+        assert qn.gradient_fn is qml.gradients.finite_diff
+
+        qn = QNode(dummyfunc, dev, diff_method="parameter-shift")
+        assert qn.diff_method == "parameter-shift"
+        assert qn.gradient_fn is qml.gradients.param_shift
+
+        # check that get_best_method was only ever called once
+        mock_best.assert_called_once()
+
+    def test_gradient_transform(self, mocker):
+        """Test passing a gradient transform directly to a QNode"""
+        dev = qml.device("default.qubit", wires=1)
+        spy = mocker.spy(qml.gradients.finite_difference, "finite_diff_coeffs")
+
+        @qnode(dev, diff_method=qml.gradients.finite_diff)
+        def circuit(x):
+            qml.RX(x, wires=0)
+            return qml.expval(qml.PauliZ(0))
+
+        assert circuit.gradient_fn is qml.gradients.finite_diff
+
+        qml.grad(circuit)(0.5)
+        spy.assert_called()
+
+    def test_unknown_diff_method_string(self):
+        """Test that an exception is raised for an unknown differentiation method string"""
+        dev = qml.device("default.qubit", wires=1)
+
+        with pytest.raises(
+            qml.QuantumFunctionError, match="Differentiation method hello not recognized"
+        ):
+            QNode(dummyfunc, dev, diff_method="hello")
+
+    def test_unknown_diff_method_type(self):
+        """Test that an exception is raised for an unknown differentiation method type"""
+        dev = qml.device("default.qubit", wires=1)
+
+        with pytest.raises(
+            qml.QuantumFunctionError,
+            match="Differentiation method 5 must be a gradient transform or a string",
+        ):
+            QNode(dummyfunc, dev, diff_method=5)
+
+    def test_validate_adjoint_invalid_device(self):
+        """Test if a ValueError is raised when an invalid device is provided to
+        _validate_adjoint_method"""
+
+        dev = qml.device("default.gaussian", wires=1)
+
+        with pytest.raises(ValueError, match="The default.gaussian device does not"):
+            QNode._validate_adjoint_method(dev)
+
+    def test_validate_adjoint_finite_shots(self):
+        """Test that a UserWarning is raised when device has finite shots"""
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(
+            UserWarning, match="Requested adjoint differentiation to be computed with finite shots."
+        ):
+            QNode._validate_adjoint_method(dev)
+
+    def test_adjoint_finite_shots(self):
+        """Tests that UserWarning is raised with the adjoint differentiation method
+        on QNode construction when the device has finite shots
+        """
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(
+            UserWarning, match="Requested adjoint differentiation to be computed with finite shots."
+        ):
+
+            @qnode(dev, diff_method="adjoint")
+            def circ():
+                return qml.expval(qml.PauliZ(0))
+
+    def test_qnode_print(self):
+        """Test that printing a QNode object yields the right information."""
+        dev = qml.device("default.qubit", wires=1)
+
+        def func(x):
+            qml.RX(x, wires=0)
+            return qml.expval(qml.PauliZ(0))
+
+        qn = QNode(func, dev)
+
+        assert (
+            qn.__repr__()
+            == "<QNode: wires=1, device='default.qubit.autograd', interface='autograd', diff_method='best'>"
+        )
+
+    def test_diff_method_none(self, tol):
+        """Test that diff_method=None creates a QNode with no interface, and no
+        device swapping."""
+        dev = qml.device("default.qubit", wires=1)
+
+        @qnode(dev, diff_method=None)
+        def circuit(x):
+            qml.RX(x, wires=0)
+            return qml.expval(qml.PauliZ(0))
+
+        assert circuit.interface is None
+        assert circuit.gradient_fn is None
+        assert circuit.device is dev
+
+        # QNode can still be executed
+        assert np.allclose(circuit(0.5), np.cos(0.5), atol=tol, rtol=0)
+
+        with pytest.warns(UserWarning, match="Output seems independent of input"):
+            grad = qml.grad(circuit)(0.5)
+
+        assert np.allclose(grad, 0)
+
+
+class TestTapeConstruction:
+    """Tests for the tape construction"""
+
+    def test_basic_tape_construction(self, tol):
+        """Test that a quantum tape is properly constructed"""
+        dev = qml.device("default.qubit", wires=2)
+
+        def func(x, y):
+            qml.RX(x, wires=0)
+            qml.RY(y, wires=1)
+            qml.CNOT(wires=[0, 1])
+            return qml.expval(qml.PauliZ(0))
+
+        qn = QNode(func, dev)
+
+        x = 0.12
+        y = 0.54
+
+        res = qn(x, y)
+
+        assert isinstance(qn.qtape, JacobianTape)
+        assert len(qn.qtape.operations) == 3
+        assert len(qn.qtape.observables) == 1
+        assert qn.qtape.num_params == 2
+
+        expected = qn.qtape.execute(dev)
+        assert np.allclose(res, expected, atol=tol, rtol=0)
+
+        # when called, a new quantum tape is constructed
+        old_tape = qn.qtape
+        res2 = qn(x, y)
+
+        assert np.allclose(res, res2, atol=tol, rtol=0)
+        assert qn.qtape is not old_tape
+
+    def test_jacobian(self, tol):
+        """Test the jacobian computation"""
+        dev = qml.device("default.qubit", wires=2)
+
+        def func(x, y):
+            qml.RX(x, wires=0)
+            qml.RY(y, wires=1)
+            qml.CNOT(wires=[0, 1])
+            return qml.probs(wires=0), qml.probs(wires=1)
+
+        qn = QNode(func, dev, diff_method="finite-diff", h=1e-8, approx_order=2)
+        assert qn.gradient_kwargs["h"] == 1e-8
+        assert qn.gradient_kwargs["approx_order"] == 2
+
+        jac = qn.gradient_fn(qn)(0.45, 0.1)
+        assert jac.shape == (2, 2, 2)
+
+    def test_returning_non_measurements(self):
+        """Test that an exception is raised if a non-measurement
+        is returned from the QNode."""
+        dev = qml.device("default.qubit", wires=2)
+
+        def func(x, y):
+            qml.RX(x, wires=0)
+            qml.RY(y, wires=1)
+            qml.CNOT(wires=[0, 1])
+            return 5
+
+        qn = QNode(func, dev)
+
+        with pytest.raises(
+            qml.QuantumFunctionError, match="must return either a single measurement"
+        ):
+            qn(5, 1)
+
+        def func(x, y):
+            qml.RX(x, wires=0)
+            qml.RY(y, wires=1)
+            qml.CNOT(wires=[0, 1])
+            return qml.expval(qml.PauliZ(0)), 5
+
+        qn = QNode(func, dev)
+
+        with pytest.raises(
+            qml.QuantumFunctionError, match="must return either a single measurement"
+        ):
+            qn(5, 1)
+
+    def test_inconsistent_measurement_order(self):
+        """Test that an exception is raised if measurements are returned in an
+        order different to how they were queued on the tape"""
+        dev = qml.device("default.qubit", wires=2)
+
+        def func(x, y):
+            qml.RX(x, wires=0)
+            qml.RY(y, wires=1)
+            qml.CNOT(wires=[0, 1])
+            m = qml.expval(qml.PauliZ(0))
+            return qml.expval(qml.PauliX(1)), m
+
+        qn = QNode(func, dev)
+
+        with pytest.raises(
+            qml.QuantumFunctionError,
+            match="measurements must be returned in the order they are measured",
+        ):
+            qn(5, 1)
+
+    def test_consistent_measurement_order(self):
+        """Test evaluation proceeds as expected if measurements are returned in the
+        same order to how they were queued on the tape"""
+        dev = qml.device("default.qubit", wires=2)
+
+        def func(x, y):
+            global op1, op2, op3, m1, m2
+            op1 = qml.RX(x, wires=0)
+            op2 = qml.RY(y, wires=1)
+            op3 = qml.CNOT(wires=[0, 1])
+            m1 = qml.expval(qml.PauliZ(0))
+            m2 = qml.expval(qml.PauliX(1))
+            return [m1, m2]
+
+        qn = QNode(func, dev)
+        qn(5, 1)  # evaluate the QNode
+        assert qn.qtape.operations == [op1, op2, op3]
+        assert qn.qtape.measurements == [m1, m2]
+
+    @pytest.mark.xfail
+    def test_multiple_observables_same_wire_expval(self, mocker):
+        """Test that the QNode supports returning expectation values of observables that are on the
+        same wire (provided that they are Pauli words and qubit-wise commuting)"""
+        dev = qml.device("default.qubit", wires=3)
+
+        w = np.random.random((2, 3, 3))
+
+        @qnode(dev)
+        def f(w):
+            qml.templates.StronglyEntanglingLayers(w, wires=range(3))
+            return (
+                qml.expval(qml.PauliX(0)),
+                qml.expval(qml.PauliX(0) @ qml.PauliZ(1)),
+                qml.expval(qml.PauliX(2)),
+            )
+
+        spy = mocker.spy(qml.devices.DefaultQubit, "apply")
+        res = f(w)
+        spy.assert_called_once()
+
+        obs = [qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(1), qml.PauliX(2)]
+        qnodes = qml.map(qml.templates.StronglyEntanglingLayers, obs, dev)
+        res_2 = qnodes(w)
+
+        assert np.allclose(res, res_2)
+
+    @pytest.mark.xfail
+    def test_multiple_observables_same_wire_mixed(self, mocker):
+        """Test that the QNode supports returning observables that are on the
+        same wire but with different return types (provided that the observables are Pauli words and
+        qubit-wise commuting)"""
+        dev = qml.device("default.qubit", wires=3)
+
+        w = np.random.random((2, 3, 3))
+
+        @qnode(dev)
+        def f(w):
+            qml.templates.StronglyEntanglingLayers(w, wires=range(3))
+            return qml.expval(qml.PauliX(0)), qml.var(qml.PauliX(0) @ qml.PauliZ(1))
+
+        spy = mocker.spy(qml.devices.DefaultQubit, "apply")
+        res = f(w)
+        spy.assert_called_once()
+
+        q1 = qml.map(qml.templates.StronglyEntanglingLayers, [qml.PauliX(0)], dev, measure="expval")
+        q2 = qml.map(
+            qml.templates.StronglyEntanglingLayers,
+            [qml.PauliX(0) @ qml.PauliZ(1)],
+            dev,
+            measure="var",
+        )
+
+        res_2 = np.array([q1(w), q2(w)]).squeeze()
+
+        assert np.allclose(res, res_2)
+
+    def test_operator_all_wires(self, monkeypatch, tol):
+        """Test that an operator that must act on all wires
+        does, or raises an error."""
+        monkeypatch.setattr(qml.RX, "num_wires", qml.operation.AllWires)
+
+        def circuit(x):
+            qml.RX(x, wires=0)
+            return qml.expval(qml.PauliZ(0))
+
+        dev = qml.device("default.qubit", wires=2)
+        qnode = QNode(circuit, dev)
+
+        with pytest.raises(qml.QuantumFunctionError, match="Operator RX must act on all wires"):
+            qnode(0.5)
+
+        dev = qml.device("default.qubit", wires=1)
+        qnode = QNode(circuit, dev)
+        assert np.allclose(qnode(0.5), np.cos(0.5), atol=tol, rtol=0)
+
+
+class TestDecorator:
+    """Unit tests for the decorator"""
+
+    def test_decorator(self, tol):
+        """Test that the decorator correctly creates a QNode."""
+        dev = qml.device("default.qubit", wires=2)
+
+        @qnode(dev)
+        def func(x, y):
+            """My function docstring"""
+            qml.RX(x, wires=0)
+            qml.RY(y, wires=1)
+            qml.CNOT(wires=[0, 1])
+            return qml.expval(qml.PauliZ(0))
+
+        assert isinstance(func, QNode)
+        assert func.__doc__ == "My function docstring"
+
+        x = 0.12
+        y = 0.54
+
+        res = func(x, y)
+
+        assert isinstance(func.qtape, JacobianTape)
+        assert len(func.qtape.operations) == 3
+        assert len(func.qtape.observables) == 1
+        assert func.qtape.num_params == 2
+
+        expected = func.qtape.execute(dev)
+        assert np.allclose(res, expected, atol=tol, rtol=0)
+
+        # when called, a new quantum tape is constructed
+        old_tape = func.qtape
+        res2 = func(x, y)
+
+        assert np.allclose(res, res2, atol=tol, rtol=0)
+        assert func.qtape is not old_tape
+
+
+class TestIntegration:
+    """Integration tests."""
+
+    def test_correct_number_of_executions_autograd(self):
+        """Test that number of executions are tracked in the autograd interface."""
+
+        def func():
+            qml.Hadamard(wires=0)
+            qml.CNOT(wires=[0, 1])
+            return qml.expval(qml.PauliZ(0))
+
+        dev = qml.device("default.qubit", wires=2)
+        qn = QNode(func, dev, interface="autograd")
+
+        for i in range(2):
+            qn()
+
+        assert dev.num_executions == 2
+
+        qn2 = QNode(func, dev, interface="autograd")
+        for i in range(3):
+            qn2()
+
+        assert dev.num_executions == 5
+
+    def test_correct_number_of_executions_tf(self):
+        """Test that number of executions are tracked in the tf interface."""
+        tf = pytest.importorskip("tf")
+
+        def func():
+            qml.Hadamard(wires=0)
+            qml.CNOT(wires=[0, 1])
+            return qml.expval(qml.PauliZ(0))
+
+        dev = qml.device("default.qubit", wires=2)
+        qn = QNode(func, dev, interface="tf")
+        for i in range(2):
+            qn()
+
+        assert dev.num_executions == 2
+
+        qn2 = QNode(func, dev, interface="tf")
+        for i in range(3):
+            qn2()
+
+        assert dev.num_executions == 5
+
+        # qubit of different interface
+        qn3 = QNode(func, dev, interface="autograd")
+        qn3()
+
+        assert dev.num_executions == 6
+
+    def test_correct_number_of_executions_torch(self):
+        """Test that number of executions are tracked in the torch interface."""
+        torch = pytest.importorskip("torch")
+
+        def func():
+            qml.Hadamard(wires=0)
+            qml.CNOT(wires=[0, 1])
+            return qml.expval(qml.PauliZ(0))
+
+        dev = qml.device("default.qubit", wires=2)
+        qn = QNode(func, dev, interface="torch")
+        for i in range(2):
+            qn()
+
+        assert dev.num_executions == 2
+
+        qn2 = QNode(func, dev, interface="torch")
+        for i in range(3):
+            qn2()
+
+        assert dev.num_executions == 5
+
+        # qubit of different interface
+        qn3 = QNode(func, dev, interface="autograd")
+        qn3()
+
+        assert dev.num_executions == 6
+
+    @pytest.mark.parametrize("diff_method", ["parameter-shift", "finite-diff"])
+    def test_single_expectation_value_with_argnum_one(self, diff_method, tol):
+        """Tests correct output shape and evaluation for a QNode
+        with a single expval output where only one parameter is chosen to
+        estimate the jacobian.
+
+        This test relies on the fact that exactly one term of the estimated
+        jacobian will match the expected analytical value.
+        """
+        from pennylane import numpy as anp
+
+        dev = qml.device("default.qubit", wires=2)
+
+        x = anp.array(0.543, requires_grad=True)
+        y = anp.array(-0.654, requires_grad=True)
+
+        @qnode(
+            dev, diff_method=diff_method, argnum=[1]
+        )  # <--- we only choose one trainable parameter
+        def circuit(x, y):
+            qml.RX(x, wires=[0])
+            qml.RY(y, wires=[1])
+            qml.CNOT(wires=[0, 1])
+            return qml.expval(qml.PauliZ(0) @ qml.PauliX(1))
+
+        res = qml.grad(circuit)(x, y)
+        assert len(res) == 2
+
+        expected = (0, np.cos(y) * np.cos(x))
+        res = res
+        expected = expected
+
+        assert np.allclose(res, expected, atol=tol, rtol=0)
+
+
+class TestShots:
+    """Unit tests for specifying shots per call."""
+
+    def test_specify_shots_per_call_sample(self):
+        """Tests that shots can be set per call for a sample return type."""
+        dev = qml.device("default.qubit", wires=1, shots=10)
+
+        @qnode(dev)
+        def circuit(a):
+            qml.RX(a, wires=0)
+            return qml.sample(qml.PauliZ(wires=0))
+
+        assert len(circuit(0.8)) == 10
+        assert len(circuit(0.8, shots=2)) == 2
+        assert len(circuit(0.8, shots=3178)) == 3178
+        assert len(circuit(0.8)) == 10
+
+    def test_specify_shots_per_call_expval(self):
+        """Tests that shots can be set per call for an expectation value.
+        Note: this test has a vanishingly small probability to fail."""
+        dev = qml.device("default.qubit", wires=1, shots=None)
+
+        @qnode(dev)
+        def circuit():
+            qml.Hadamard(wires=0)
+            return qml.expval(qml.PauliZ(wires=0))
+
+        # check that the circuit is analytic
+        res1 = [circuit() for _ in range(100)]
+        assert np.std(res1) == 0.0
+        assert circuit.device._shots is None
+
+        # check that the circuit is temporary non-analytic
+        res1 = [circuit(shots=1) for _ in range(100)]
+        assert np.std(res1) != 0.0
+
+        # check that the circuit is analytic again
+        res1 = [circuit() for _ in range(100)]
+        assert np.std(res1) == 0.0
+        assert circuit.device._shots is None
+
+    def test_no_shots_per_call_if_user_has_shots_qfunc_kwarg(self):
+        """Tests that the per-call shots overwriting is suspended if user
+        has a shots keyword argument, but a warning is raised."""
+
+        dev = qml.device("default.qubit", wires=2, shots=10)
+
+        def circuit(a, shots=0):
+            qml.RX(a, wires=shots)
+            return qml.sample(qml.PauliZ(wires=0))
+
+        with pytest.warns(
+            UserWarning, match="The 'shots' argument name is reserved for overriding"
+        ):
+            circuit = QNode(circuit, dev)
+
+        assert len(circuit(0.8)) == 10
+        assert circuit.qtape.operations[0].wires.labels == (0,)
+
+        assert len(circuit(0.8, shots=1)) == 10
+        assert circuit.qtape.operations[0].wires.labels == (1,)
+
+        assert len(circuit(0.8, shots=0)) == 10
+        assert circuit.qtape.operations[0].wires.labels == (0,)
+
+    def test_no_shots_per_call_if_user_has_shots_qfunc_arg(self):
+        """Tests that the per-call shots overwriting is suspended
+        if user has a shots argument, but a warning is raised."""
+
+        # Todo: use standard creation of qnode below for both asserts once we do not parse args to tensors any more
+        dev = qml.device("default.qubit", wires=[qml.numpy.array(0), qml.numpy.array(1)], shots=10)
+
+        def circuit(a, shots):
+            qml.RX(a, wires=shots)
+            return qml.sample(qml.PauliZ(wires=qml.numpy.array(0)))
+
+        # assert that warning is still raised
+        with pytest.warns(
+            UserWarning, match="The 'shots' argument name is reserved for overriding"
+        ):
+            circuit = QNode(circuit, dev)
+
+        assert len(circuit(0.8, 1)) == 10
+        assert circuit.qtape.operations[0].wires.labels == (1,)
+
+        dev = qml.device("default.qubit", wires=2, shots=10)
+
+        with pytest.warns(
+            UserWarning, match="The 'shots' argument name is reserved for overriding"
+        ):
+
+            @qnode(dev)
+            def circuit(a, shots):
+                qml.RX(a, wires=shots)
+                return qml.sample(qml.PauliZ(wires=0))
+
+        assert len(circuit(0.8, shots=0)) == 10
+        assert circuit.qtape.operations[0].wires.labels == (0,)
+
+    @pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift"])
+    def test_shots_setting_does_not_mutate_device(self, diff_method):
+        """Tests that per-call shots setting does not change the number of shots in the device."""
+
+        dev = qml.device("default.qubit", wires=1, shots=3)
+
+        @qnode(dev)
+        def circuit(a):
+            qml.RX(a, wires=0)
+            return qml.sample(qml.PauliZ(wires=0))
+
+        assert dev.shots == 3
+        res = circuit(0.8, shots=2)
+        assert len(res) == 2
+        assert dev.shots == 3
+
+
+@pytest.mark.xfail
+class TestSpecs:
+    """Tests for the qnode property specs"""
+
+    def test_specs_error(self):
+        """Tests an error is raised if the tape is not constructed."""
+
+        dev = qml.device("default.qubit", wires=4)
+
+        @qnode(dev)
+        def circuit():
+            return qml.expval(qml.PauliZ(0))
+
+        with pytest.raises(qml.QuantumFunctionError, match=r"The QNode specifications"):
+            circuit.specs
+
+    @pytest.mark.parametrize(
+        "diff_method, len_info", [("backprop", 10), ("parameter-shift", 12), ("adjoint", 11)]
+    )
+    def test_specs(self, diff_method, len_info):
+        """Tests the specs property with backprop, parameter-shift and adjoint diff_method"""
+
+        dev = qml.device("default.qubit", wires=4)
+
+        @qnode(dev, diff_method=diff_method)
+        def circuit(x, y):
+            qml.RX(x[0], wires=0)
+            qml.Toffoli(wires=(0, 1, 2))
+            qml.CRY(x[1], wires=(0, 1))
+            qml.Rot(x[2], x[3], y, wires=2)
+            return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(1))
+
+        x = pnp.array([0.05, 0.1, 0.2, 0.3], requires_grad=True)
+        y = pnp.array(0.1, requires_grad=False)
+
+        res = circuit(x, y)
+
+        info = circuit.specs
+
+        assert len(info) == len_info
+
+        assert info["gate_sizes"] == defaultdict(int, {1: 2, 3: 1, 2: 1})
+        assert info["gate_types"] == defaultdict(int, {"RX": 1, "Toffoli": 1, "CRY": 1, "Rot": 1})
+        assert info["num_operations"] == 4
+        assert info["num_observables"] == 2
+        assert info["num_diagonalizing_gates"] == 1
+        assert info["num_used_wires"] == 3
+        assert info["depth"] == 3
+        assert info["num_device_wires"] == 4
+
+        assert info["diff_method"] == diff_method
+
+        if diff_method == "parameter-shift":
+            assert info["num_parameter_shift_executions"] == 7
+
+        if diff_method != "backprop":
+            assert info["device_name"] == "default.qubit"
+            assert info["num_trainable_params"] == 4
+        else:
+            assert info["device_name"] == "default.qubit.autograd"