Repository: PennyLaneAI/pennylane
Pull request #2178 (2022-02-08 00:01:18)

Merge v0.21.0 rc into master
Merge the content of the release candidate branch after the v0.21.0 release.

josh146: @antalszava I have merged the latest `v0.21.0-rc0` back into this branch :)
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#2178](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (ed4cac1) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/e4f33ccfe9ca6b122339717a5ba2bf039464f7d3?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (e4f33cc) will **decrease** coverage by `2.80%`.
> The diff coverage is `99.08%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/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/2178?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@            Coverage Diff             @@
##           master    #2178      +/-   ##
==========================================
- Coverage   99.20%   96.40%   -2.81%     
==========================================
  Files         231      231              
  Lines       17807    17784      -23     
==========================================
- Hits        17666    17144     -522     
- Misses        141      640     +499     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/drawer/tape\_text.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RyYXdlci90YXBlX3RleHQucHk=) | `100.00% <ø> (ø)` | |
| [pennylane/hf/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2hmL19faW5pdF9fLnB5) | `100.00% <ø> (ø)` | |
| [pennylane/templates/embeddings/basis.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RlbXBsYXRlcy9lbWJlZGRpbmdzL2Jhc2lzLnB5) | `100.00% <ø> (ø)` | |
| [pennylane/templates/state\_preparations/basis.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RlbXBsYXRlcy9zdGF0ZV9wcmVwYXJhdGlvbnMvYmFzaXMucHk=) | `100.00% <ø> (ø)` | |
| [pennylane/templates/subroutines/interferometer.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RlbXBsYXRlcy9zdWJyb3V0aW5lcy9pbnRlcmZlcm9tZXRlci5weQ==) | `100.00% <ø> (ø)` | |
| [pennylane/gradients/finite\_difference.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2dyYWRpZW50cy9maW5pdGVfZGlmZmVyZW5jZS5weQ==) | `98.96% <83.33%> (-1.04%)` | :arrow_down: |
| [pennylane/\_grad.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19ncmFkLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/\_qubit\_device.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19xdWJpdF9kZXZpY2UucHk=) | `98.40% <100.00%> (-0.32%)` | :arrow_down: |
| [pennylane/gradients/gradient\_transform.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/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% <100.00%> (ø)` | |
| [pennylane/gradients/hessian\_transform.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2dyYWRpZW50cy9oZXNzaWFuX3RyYW5zZm9ybS5weQ==) | `100.00% <100.00%> (ø)` | |
| ... and [36 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178?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/2178?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [e4f33cc...ed4cac1](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2178?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).


Patch:
diff --git a/doc/development/release_notes.md b/doc/development/release_notes.md
index 0ebe8a142..6d812caaf 100644
--- a/doc/development/release_notes.md
+++ b/doc/development/release_notes.md
@@ -6,6 +6,8 @@ This page contains the release notes for PennyLane.
 
 .. mdinclude:: ../releases/changelog-dev.md
 
+.. mdinclude:: ../releases/changelog-0.21.0.md
+
 .. mdinclude:: ../releases/changelog-0.20.0.md
 
 .. mdinclude:: ../releases/changelog-0.19.1.md
diff --git a/doc/releases/changelog-0.21.0.md b/doc/releases/changelog-0.21.0.md
index 4da315e00..aa219c160 100644
--- a/doc/releases/changelog-0.21.0.md
+++ b/doc/releases/changelog-0.21.0.md
@@ -4,64 +4,138 @@
 
 <h3>New features since last release</h3>
 
-* Added a modified version of the `simplify` function to the `hf` module.
-  [(#2103)](https://github.com/PennyLaneAI/pennylane/pull/2103)
+<h4>Reduce qubit requirements of simulating Hamiltonians ⚛️</h4>
 
-  This function combines redundant terms in a Hamiltonian and eliminates terms with a coefficient
-  smaller than a cutoff value. The new function makes construction of molecular Hamiltonians more
-  efficient. For LiH, as an example, the time to construct the Hamiltonian is reduced roughly by a
-  factor of 20.
+* Functions for tapering qubits based on molecular symmetries have been added,
+  following results from [Setia et al](https://arxiv.org/abs/1910.14644).
+  [(#1966)](https://github.com/PennyLaneAI/pennylane/pull/1966)
+  [(#1974)](https://github.com/PennyLaneAI/pennylane/pull/1974)
+  [(#2041)](https://github.com/PennyLaneAI/pennylane/pull/2041)
+  [(#2042)](https://github.com/PennyLaneAI/pennylane/pull/2042)
 
-* The JAX interface now supports evaluating vector-valued QNodes. Vector-valued
-  QNodes include those with:
-  * `qml.probs`;
-  * `qml.state`;
-  * `qml.sample` or
-  * multiple `qml.expval` / `qml.var` measurements.
+  With this functionality, a molecular Hamiltonian and the corresponding Hartree-Fock (HF) state can be transformed to a new Hamiltonian and HF state that acts on a reduced number of qubits, respectively.
+  ```python
+  # molecular geometry
+  symbols = ["He", "H"]
+  geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4588684632]])
+  mol = qml.hf.Molecule(symbols, geometry, charge=1)
+
+  # generate the qubit Hamiltonian
+  H = qml.hf.generate_hamiltonian(mol)(geometry)
+
+  # determine Hamiltonian symmetries
+  generators, paulix_ops = qml.hf.generate_symmetries(H, len(H.wires))
+  opt_sector = qml.hf.optimal_sector(H, generators, mol.n_electrons)
+
+  # taper the Hamiltonian
+  H_tapered = qml.hf.transform_hamiltonian(H, generators, paulix_ops, opt_sector)
+  ```
+
+  We can compare the number of qubits required by the original Hamiltonian
+  and the tapered Hamiltonian:
+
+  ```pycon
+  >>> len(H.wires)
+  4
+  >>> len(H_tapered.wires)
+  2
+  ```
+
+  For quantum chemistry algorithms, the Hartree-Fock state can also be tapered:
 
-  Consider a QNode that returns basis-state probabilities:
   ```python
-  dev = qml.device('default.qubit', wires=2)
-  x = jnp.array(0.543)
-  y = jnp.array(-0.654)
+  n_elec = mol.n_electrons
+  n_qubits = mol.n_orbitals * 2
 
-  @qml.qnode(dev, diff_method="parameter-shift", interface="jax")
-  def circuit(x, y):
-      qml.RX(x, wires=[0])
-      qml.RY(y, wires=[1])
-      qml.CNOT(wires=[0, 1])
-      return qml.probs(wires=[1])
+  hf_tapered = qml.hf.transform_hf(
+      generators, paulix_ops, opt_sector, n_elec, n_qubits
+  )
   ```
-  The QNode can be evaluated and its jacobian can be computed:
   ```pycon
-  >>> circuit(x, y)
-  DeviceArray([0.8397495 , 0.16025047], dtype=float32)
-  >>> jax.jacobian(circuit, argnums=[0, 1])(x, y)
-  (DeviceArray([-0.2050439,  0.2050439], dtype=float32, weak_type=True),
-   DeviceArray([ 0.26043, -0.26043], dtype=float32, weak_type=True))
+  >>> hf_tapered
+  tensor([1, 1], requires_grad=True)
   ```
-  Note that `jax.jit` is not supported for vector-valued QNodes.
-  [(#2110)](https://github.com/PennyLaneAI/pennylane/pull/2110)
 
-* For subclasses of `Operator` where it is known before instantiation, the `num_params` is reverted back to being a
-  static property. This allows to programmatically know the number of parameters before an operator is
-  instantiated without changing the user interface.
-  [(#2099)](https://github.com/PennyLaneAI/pennylane/issues/2099)
+<h4>New tensor network templates 🪢</h4>
 
-* Development of a circuit-cutting compiler has begun:
-  A `WireCut` operator has been added for manual wire cut placement
-  when constructing a QNode.
-  [(#2093)](https://github.com/PennyLaneAI/pennylane/pull/2093)
+* Quantum circuits with the shape
+  of a matrix product state tensor network can now be easily implemented
+  using the new `qml.MPS` template, based on the work
+  [arXiv:1803.11537](https://arxiv.org/abs/1803.11537).
+  [(#1871)](https://github.com/PennyLaneAI/pennylane/pull/1871)
+
+  ```python
+  def block(weights, wires):
+      qml.CNOT(wires=[wires[0], wires[1]])
+      qml.RY(weights[0], wires=wires[0])
+      qml.RY(weights[1], wires=wires[1])
+
+  n_wires = 4
+  n_block_wires = 2
+  n_params_block = 2
+  template_weights = np.array([[0.1, -0.3], [0.4, 0.2], [-0.15, 0.5]], requires_grad=True)
+
+  dev = qml.device("default.qubit", wires=range(n_wires))
+
+  @qml.qnode(dev)
+  def circuit(weights):
+      qml.MPS(range(n_wires), n_block_wires, block, n_params_block, weights)
+      return qml.expval(qml.PauliZ(wires=n_wires - 1))
+  ```
+
+  The resulting circuit is:
+  ```pycon
+  >>> print(qml.draw(circuit, expansion_strategy="device")(template_weights))
+  0: ──╭C──RY(0.1)───────────────────────────────┤
+  1: ──╰X──RY(-0.3)──╭C──RY(0.4)─────────────────┤
+  2: ────────────────╰X──RY(0.2)──╭C──RY(-0.15)──┤
+  3: ─────────────────────────────╰X──RY(0.5)────┤ ⟨Z⟩
+  ```
+
+* Added a template for tree tensor networks, `qml.TTN`.
+  [(#2043)](https://github.com/PennyLaneAI/pennylane/pull/2043)
+  ```python
+  def block(weights, wires):
+      qml.CNOT(wires=[wires[0], wires[1]])
+      qml.RY(weights[0], wires=wires[0])
+      qml.RY(weights[1], wires=wires[1])
+
+  n_wires = 4
+  n_block_wires = 2
+  n_params_block = 2
+  n_blocks = qml.MPS.get_n_blocks(range(n_wires), n_block_wires)
+  template_weights = [[0.1, -0.3]] * n_blocks
+
+  dev = qml.device("default.qubit", wires=range(n_wires))
+
+  @qml.qnode(dev)
+  def circuit(template_weights):
+      qml.TTN(range(n_wires), n_block_wires, block, n_params_block, template_weights)
+      return qml.expval(qml.PauliZ(wires=n_wires - 1))
+  ```
+  The resulting circuit is:
+  ```pycon
+  >>> print(qml.draw(circuit, expansion_strategy="device")(template_weights))
+  0: ──╭C──RY(0.1)─────────────────┤
+  1: ──╰X──RY(-0.3)──╭C──RY(0.1)───┤
+  2: ──╭C──RY(0.1)───│─────────────┤
+  3: ──╰X──RY(-0.3)──╰X──RY(-0.3)──┤ ⟨Z⟩
+  ```
+
+<h4>Generalized RotosolveOptmizer 📉</h4>
 
 * The `RotosolveOptimizer` has been generalized to arbitrary frequency spectra
   in the cost function. Also note the changes in behaviour listed under *Breaking
   changes*.
   [(#2081)](https://github.com/PennyLaneAI/pennylane/pull/2081)
 
-  Previously, the `RotsolveOptimizer` was available for cost functions with
-  frequency spectra that only contained integers, and the maximal frequency
-  (instead of the number of frequencies) determined the cost of the optimization.
-  Now arbitrary frequencies are supported.
+  Previously, the RotosolveOptimizer only supported variational circuits using
+  special gates such as single-qubit Pauli rotations. Now, circuits with
+  arbitrary gates are supported natively without decomposition, as long as the
+  frequencies of the gate parameters are known. This new generalization extends
+  the Rotosolve optimization method to a larger class of circuits, and can
+  reduce the cost of the optimization compared to decomposing all gates to
+  single-qubit rotations.
 
   Consider the QNode
   ```python
@@ -110,124 +184,55 @@
   ...     (x, Y), history = opt.step(qnode, x, Y, spectra=spectra, full_output=True)
   ...     print(f"New cost: {np.round(qnode(x, Y), 3)} reached via substeps {np.round(history, 3)}")
   New cost: 0.0 reached via substeps [-0.  0.  0.]
-  New cost: -1.0 reached via substeps [-0.276 -0.276 -1.   ]
+  New cost: -1.0 reached via substeps [-1. -1. -1.]
   ```
   However, note that these intermediate minimal values are evaluations of the
   *reconstructions* that Rotosolve creates and uses internally for the optimization,
   and not of the original objective function. For noisy cost functions, these intermediate
   evaluations may differ significantly from evaluations of the original cost function.
 
-* A tensor network templates module has been added. Quantum circuits with the shape
-  of a matrix product state tensor network can now be easily implemented.
-  Motivation and theory can be found in [arXiv:1803.11537](https://arxiv.org/abs/1803.11537).
-  [(#1871)](https://github.com/PennyLaneAI/pennylane/pull/1871)
-
-  An example circuit that uses the `MPS` template is:
-  ```python
-  import pennylane as qml
-  import numpy as np
-
-  def block(weights, wires):
-      qml.CNOT(wires=[wires[0],wires[1]])
-      qml.RY(weights[0], wires=wires[0])
-      qml.RY(weights[1], wires=wires[1])
+<h4>Improved JAX support 💻</h4>
 
-  n_wires = 4
-  n_block_wires = 2
-  n_params_block = 2
-  template_weights = [[0.1,-0.3],[0.4,0.2],[-0.15,0.5]]
+* The JAX interface now supports evaluating vector-valued QNodes.
+  [(#2110)](https://github.com/PennyLaneAI/pennylane/pull/2110)
 
-  dev= qml.device('default.qubit',wires=range(n_wires))
-  @qml.qnode(dev)
-  def circuit(weights):
-      qml.MPS(range(n_wires),n_block_wires,block, n_params_block, weights)
-      return qml.expval(qml.PauliZ(wires=n_wires-1))
-  ```
+  Vector-valued QNodes include those with:
+  * `qml.probs`;
+  * `qml.state`;
+  * `qml.sample` or
+  * multiple `qml.expval` / `qml.var` measurements.
 
-  The resulting circuit is:
-  ```pycon
-  >>> print(qml.draw(circuit,expansion_strategy='device')(template_weights))
-  0: ──╭C──RY(0.1)───────────────────────────────┤
-  1: ──╰X──RY(-0.3)──╭C──RY(0.4)─────────────────┤
-  2: ────────────────╰X──RY(0.2)──╭C──RY(-0.15)──┤
-  3: ─────────────────────────────╰X──RY(0.5)────┤ ⟨Z⟩
-  ```
-* Added a template for tree tensor networks (TTN).
-  [(#2043)](https://github.com/PennyLaneAI/pennylane/pull/2043)
-  An example circuit that uses the `TTN` template is:
+  Consider a QNode that returns basis-state probabilities:
   ```python
-  import pennylane as qml
-  import numpy as np
-
-  def block(weights, wires):
-      qml.CNOT(wires=[wires[0],wires[1]])
-      qml.RY(weights[0], wires=wires[0])
-      qml.RY(weights[1], wires=wires[1])
-
-  n_wires = 4
-  n_block_wires = 2
-  n_params_block = 2
-  n_blocks = qml.MPS.get_n_blocks(range(n_wires),n_block_wires)
-  template_weights = [[0.1,-0.3]]*n_blocks
+  dev = qml.device('default.qubit', wires=2)
+  x = jnp.array(0.543)
+  y = jnp.array(-0.654)
 
-  dev= qml.device('default.qubit',wires=range(n_wires))
-  @qml.qnode(dev)
-  def circuit(template_weights):
-      qml.TTN(range(n_wires), n_block_wires, block, n_params_block, template_weights)
-      return qml.expval(qml.PauliZ(wires=n_wires-1))
+  @qml.qnode(dev, diff_method="parameter-shift", interface="jax")
+  def circuit(x, y):
+      qml.RX(x, wires=[0])
+      qml.RY(y, wires=[1])
+      qml.CNOT(wires=[0, 1])
+      return qml.probs(wires=[1])
   ```
-  The resulting circuit is:
+  The QNode can be evaluated and its jacobian can be computed:
   ```pycon
-  >>> print(qml.draw(circuit,expansion_strategy='device')(template_weights))
-  0: ──╭C──RY(0.1)─────────────────┤
-  1: ──╰X──RY(-0.3)──╭C──RY(0.1)───┤
-  2: ──╭C──RY(0.1)───│─────────────┤
-  3: ──╰X──RY(-0.3)──╰X──RY(-0.3)──┤ ⟨Z⟩
+  >>> circuit(x, y)
+  DeviceArray([0.8397495 , 0.16025047], dtype=float32)
+  >>> jax.jacobian(circuit, argnums=[0, 1])(x, y)
+  (DeviceArray([-0.2050439,  0.2050439], dtype=float32, weak_type=True),
+   DeviceArray([ 0.26043, -0.26043], dtype=float32, weak_type=True))
   ```
+  Note that `jax.jit` is not yet supported for vector-valued QNodes.
 
-* Functions for tapering qubits based on molecular symmetries have been added.
-  [(#1966)](https://github.com/PennyLaneAI/pennylane/pull/1966)
-  [(#1974)](https://github.com/PennyLaneAI/pennylane/pull/1974)
-  [(#2041)](https://github.com/PennyLaneAI/pennylane/pull/2041)
-
-  With this functionality, a molecular Hamiltonian and the corresponding Hartree-Fock (HF) state can be transformed to a new Hamiltonian and HF state that acts on a reduced number of qubits, respectively.
+<h4>Speedier quantum natural gradient ⚡</h4>
 
-  ```python
-  from pennylane import hf
-  from pennylane import numpy as np
-
-  symbols = ["He", "H"]
-  geometry = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4588684632]])
-  mol = hf.Molecule(symbols, geometry, charge=1)
-  H = hf.generate_hamiltonian(mol)(geometry)
-  n_qubits, n_elec = len(H.wires), mol.n_electrons
-
-  generators, paulix_ops = hf.generate_symmetries(H, n_qubits)
-  opt_sector = hf.optimal_sector(H, generators, n_elec)
-  H_tapered = hf.transform_hamiltonian(H, generators, paulix_ops, opt_sector)
-  hf_tapered = hf.transform_hf(generators, paulix_ops, paulix_sector,
-                                    n_elec, n_qubits)
-  ```
-  ```pycon
-  >>> print(H_tapered)
-    ((-1.7997297644914574+0j)) [I0]
-  + ((-0.10492941956079854+0j)) [X0]
-  + ((0.10492941956079856+0j)) [X1]
-  + ((0.5675134088336165+0j)) [Z1]
-  + ((0.5675134088336168+0j)) [Z0]
-  + ((-0.14563730440190722+0j)) [Y0 Y1]
-  + ((-0.10492941933657857+0j)) [X0 Z1]
-  + ((0.09337410512815508+0j)) [Z0 Z1]
-  + ((0.10492941933657857+0j)) [Z0 X1]
-  >>> print(hf_tapered)
-  tensor([1, 1], requires_grad=True)
-  ```
-  
-
-* Added the adjoint method for the metric tensor.
+* A new function for computing the metric tensor on simulators,
+  `qml.adjoint_metric_tensor`, has been added, that uses classically
+  efficient methods to massively improve performance.
   [(#1992)](https://github.com/PennyLaneAI/pennylane/pull/1992)
 
-  This method, detailed in [Jones 2020](https://arxiv.org/abs/2011.02991),
+  This method, detailed in [Jones (2020)](https://arxiv.org/abs/2011.02991),
   computes the metric tensor using four copies of the state vector and
   a number of operations that scales quadratically in the number of trainable
   parameters.
@@ -254,6 +259,7 @@
       qml.RY(y[0], wires=0)
       qml.RY(y[1], wires=1)
       qml.RY(y[0], wires=2)
+      return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1)), qml.expval(qml.PauliY(1))
 
   x = np.array([[0.2, 0.4, -0.1], [-2.1, 0.5, -0.2], [0.1, 0.7, -0.6]], requires_grad=False)
   y = np.array([1.3, 0.2], requires_grad=True)
@@ -277,18 +283,23 @@
   The adjoint method requires memory for 4 independent state vectors, which corresponds roughly
   to storing a state vector of a system with 2 additional qubits.
 
-* A new method `qml.gradients.param_shift_hessian` has been added to directly compute the Hessian
-  (2nd order partial derivative matrix) of QNodes and QuantumTapes. The method generates
-  parameter-shifted tapes which allow the Hessian to be computed analytically on hardware and
-  software devices. Compared to using an auto-differentiation framework to compute the Hessian
-  via parameter shifts, this method will use fewer device invocations and can be used to inspect
-  the parameter-shifted "Hessian tapes" directly. The method remains fully differentiable on all
-  supported PennyLane interfaces.
+<h4>Compute the Hessian on hardware ⬆️</h4>
 
-  Additionally, the parameter-shift Hessian comes with a new batch transform decorator
-  `@qml.gradients.hessian_transform`, which can be used to create custom Hessian methods.
+* A new gradient transform `qml.gradients.param_shift_hessian` has been added
+  to directly compute the Hessian (2nd order partial derivative matrix) of
+  QNodes on hardware.
   [(#1884)](https://github.com/PennyLaneAI/pennylane/pull/1884)
 
+  The function generates parameter-shifted tapes which allow the Hessian to be
+  computed analytically on hardware and software devices. Compared to using an
+  auto-differentiation framework to compute the Hessian via parameter shifts,
+  this function will use fewer device invocations and can be used to inspect
+  the parameter-shifted "Hessian tapes" directly. The function remains fully
+  differentiable on all supported PennyLane interfaces.
+
+  Additionally, the parameter-shift Hessian comes with a new batch transform decorator
+  `@qml.gradients.hessian_transform`, which can be used to create custom Hessian functions.
+
   The following code demonstrates how to use the parameter-shift Hessian:
 
   ```python
@@ -312,6 +323,40 @@
 
 <h3>Improvements</h3>
 
+* The `qml.transforms.insert` transform now supports adding operation after or
+  before certain specific gates.
+  [(#1980)](https://github.com/PennyLaneAI/pennylane/pull/1980)
+
+* Added a modified version of the `simplify` function to the `hf` module.
+  [(#2103)](https://github.com/PennyLaneAI/pennylane/pull/2103)
+
+  This function combines redundant terms in a Hamiltonian and eliminates terms
+  with a coefficient smaller than a cutoff value. The new function makes
+  construction of molecular Hamiltonians more efficient. For LiH, as an
+  example, the time to construct the Hamiltonian is reduced roughly by a factor
+  of 20.
+
+* The QAOA module now accepts both NetworkX and RetworkX graphs as function inputs.
+  [(#1791)](https://github.com/PennyLaneAI/pennylane/pull/1791)
+
+* The `CircuitGraph`, used to represent circuits via directed acyclic graphs, now
+  uses RetworkX for its internal representation. This results in significant speedup
+  for algorithms that rely on a directed acyclic graph representation.
+  [(#1791)](https://github.com/PennyLaneAI/pennylane/pull/1791)
+
+* For subclasses of `Operator` where the number of parameters
+  is known before instantiation, the
+  `num_params` is reverted back to being a static property. This allows to
+  programmatically know the number of parameters before an operator is
+  instantiated without changing the user interface. A test was added to ensure
+  that different ways of defining `num_params` work as expected.
+  [(#2101)](https://github.com/PennyLaneAI/pennylane/pull/2101)
+  [(#2135)](https://github.com/PennyLaneAI/pennylane/pull/2135)
+
+* A `WireCut` operator has been added for manual wire cut placement
+  when constructing a QNode.
+  [(#2093)](https://github.com/PennyLaneAI/pennylane/pull/2093)
+
 * The new function `qml.drawer.tape_text` produces a string drawing of a tape. This function
   differs in implementation and minor stylistic details from the old string circuit drawing
   infrastructure.
@@ -356,15 +401,8 @@
   params = list(range(1, 3))
   ```
 
-  The produced state is
-
-  ```pycon
-  >>> circuit_wrong(params)
-  [ 0.47415988+0.j          0.        -0.73846026j  0.        -0.25903472j
-   -0.40342268+0.j        ]
-  ```
-
-  but if we apply the `adjoint` correctly, we get
+  Evaluating `circuit_wrong(params)` now raises a `ValueError` and if we apply
+  `qml.adjoint` correctly, we get
 
   ```pycon
   >>> circuit_correct(params)
@@ -376,20 +414,9 @@
   to control the precision of parameters.
   [(#2071)](https://github.com/PennyLaneAI/pennylane/pull/2071)
 
-* `insert` transform now supports adding operation after or before certain specific gates.
-  [(#1980)](https://github.com/PennyLaneAI/pennylane/pull/1980)
-
 * Interferometer now has a `shape` method.
   [(#1946)](https://github.com/PennyLaneAI/pennylane/pull/1946)
 
-* The `CircuitGraph`, used to represent circuits via directed acyclic graphs, now
-  uses RetworkX for its internal representation. This results in significant speedup
-  for algorithms that rely on a directed acyclic graph representation.
-  [(#1791)](https://github.com/PennyLaneAI/pennylane/pull/1791)
-
-* The QAOA module now accepts both NetworkX and RetworkX graphs as function inputs.
-  [(#1791)](https://github.com/PennyLaneAI/pennylane/pull/1791)
-
 * The Barrier and Identity operations now support the `adjoint` method.
   [(#2062)](https://github.com/PennyLaneAI/pennylane/pull/2062)
   [(#2063)](https://github.com/PennyLaneAI/pennylane/pull/2063)
@@ -400,7 +427,6 @@
 * Added a new `multi_dispatch` decorator that helps ease the definition of new functions
   inside PennyLane. The decorator is used throughout the math module, demonstrating use cases.
   [(#2082)](https://github.com/PennyLaneAI/pennylane/pull/2084)
-
   [(#2096)](https://github.com/PennyLaneAI/pennylane/pull/2096)
 
   We can decorate a function, indicating the arguments that are
@@ -431,13 +457,40 @@
 
 <h3>Breaking changes</h3>
 
-* `qml.metric_tensor`, `qml.adjoint_metric_tensor` and `qml.transforms.classical_jacobian`
-  now follow a different convention regarding their output shape when being used
-  with the Autograd interface
-  [(#2059)](https://github.com/PennyLaneAI/pennylane/pull/2059)
+* QNode arguments will no longer be considered trainable by default when using
+  the Autograd interface. In order to obtain derivatives with respect to a parameter,
+  it should be instantiated via PennyLane's NumPy wrapper using the `requires_grad=True`
+  attribute. The previous behaviour was deprecated in version v0.19.0 of PennyLane.
+  [(#2116)](https://github.com/PennyLaneAI/pennylane/pull/2116)
+  [(#2125)](https://github.com/PennyLaneAI/pennylane/pull/2125)
+  [(#2139)](https://github.com/PennyLaneAI/pennylane/pull/2139)
+  [(#2148)](https://github.com/PennyLaneAI/pennylane/pull/2148)
+  [(#2156)](https://github.com/PennyLaneAI/pennylane/pull/2156)
 
-  See the previous entry for details. This breaking change immediately follows from
-  the change in `qml.jacobian` whenever `hybrid=True` is used in the above methods.
+  ```python
+  from pennylane import numpy as np
+
+  @qml.qnode(qml.device("default.qubit", wires=2))
+  def circuit(x):
+    ...
+
+  x = np.array([0.1, 0.2], requires_grad=True)
+  qml.grad(circuit)(x)
+  ```
+
+  For the `qml.grad` and `qml.jacobian` functions, trainability can alternatively be
+  indicated via the `argnum` keyword:
+
+  ```python
+  import numpy as np
+
+  @qml.qnode(qml.device("default.qubit", wires=2))
+  def circuit(hyperparam, param):
+    ...
+
+  x = np.array([0.1, 0.2])
+  qml.grad(circuit, argnum=1)(0.5, x)
+  ```
 
 * `qml.jacobian` now follows a different convention regarding its output shape.
   [(#2059)](https://github.com/PennyLaneAI/pennylane/pull/2059)
@@ -464,6 +517,14 @@
   will no longer support higher-order derivatives. In such cases, combining multiple
   arguments into a single array is recommended.
 
+* `qml.metric_tensor`, `qml.adjoint_metric_tensor` and `qml.transforms.classical_jacobian`
+  now follow a different convention regarding their output shape when being used
+  with the Autograd interface
+  [(#2059)](https://github.com/PennyLaneAI/pennylane/pull/2059)
+
+  See the previous entry for details. This breaking change immediately follows from
+  the change in `qml.jacobian` whenever `hybrid=True` is used in the above methods.
+
 * The behaviour of `RotosolveOptimizer` has been changed regarding
   its keyword arguments.
   [(#2081)](https://github.com/PennyLaneAI/pennylane/pull/2081)
@@ -486,40 +547,39 @@
   For more details, see the
   [RotosolveOptimizer documentation](https://pennylane.readthedocs.io/en/stable/code/api/pennylane.RotosolveOptimizer.html).
 
-* QNode arguments will no longer be considered trainable by default when using
-  the Autograd interface. In order to obtain derivatives with respect to a parameter,
-  it should be instantiated via PennyLane's NumPy wrapper using the `requires_grad=True`
-  attribute. The previous behaviour was deprecated in version v0.19.0 of PennyLane.
-  [(#2116)](https://github.com/PennyLaneAI/pennylane/pull/2116)
-  [(#2125)](https://github.com/PennyLaneAI/pennylane/pull/2125)
-
-  ```python
-  from pennylane import numpy as np
-
-  @qml.qnode(qml.device("default.qubit", wires=2))
-  def circuit(x):
-    ...
-
-  x = np.array([0.1, 0.2], requires_grad=True)
-  qml.grad(circuit)(x)
-  ```
+<h3>Deprecations</h3>
 
-  For the `qml.grad` and `qml.jacobian` functions, trainability can alternatively be
-  indicated via the `argnum` keyword:
+* Deprecates the caching ability provided by `QubitDevice`.
+  [(#2154)](https://github.com/PennyLaneAI/pennylane/pull/2154)
 
+  Going forward, the preferred way is to use the caching abilities of the
+  QNode:
   ```python
-  import numpy as np
+  dev = qml.device("default.qubit", wires=2)
 
-  @qml.qnode(qml.device("default.qubit", wires=2))
-  def circuit(hyperparam, param):
-    ...
+  cache = {}
 
-  x = np.array([0.1, 0.2])
-  qml.grad(circuit, argnum=1)(0.5, x)
+  @qml.qnode(dev, diff_method='parameter-shift', cache=cache)
+  def circuit():
+      qml.RY(0.345, wires=0)
+      return qml.expval(qml.PauliZ(0))
+  ```
+  ```pycon
+  >>> for _ in range(10):
+  ...    circuit()
+  >>> dev.num_executions
+  1
   ```
 
 <h3>Bug fixes</h3>
 
+* Fixes a bug where an incorrect number of executions are recorded by
+  a QNode using a custom cache with `diff_method="backprop"`.
+  [(#2171)](https://github.com/PennyLaneAI/pennylane/pull/2171)
+
+* Fixes a bug where the `default.qubit.jax` device can't be used with `diff_method=None` and jitting.
+  [(#2136)](https://github.com/PennyLaneAI/pennylane/pull/2136)
+
 * Fixes a bug where the Torch interface was not properly unwrapping Torch tensors
   to NumPy arrays before executing gradient tapes on devices.
   [(#2117)](https://github.com/PennyLaneAI/pennylane/pull/2117)
@@ -532,7 +592,7 @@
   provided by the underlying device.
   [(#2111)](https://github.com/PennyLaneAI/pennylane/pull/2111)
 
-* An error is raised during QNode creation if backpropagation is requested on a device with
+* An error is now raised during QNode creation if backpropagation is requested on a device with
   finite shots specified.
   [(#2114)](https://github.com/PennyLaneAI/pennylane/pull/2114)
 
@@ -577,12 +637,21 @@
   vector.
   [(#2028)](https://github.com/PennyLaneAI/pennylane/pull/2028)
 
+* Updated the `adjoint()` method for non-parametric qubit operations to
+  solve a bug where repeated `adjoint()` calls don't return the correct
+  operator.
+  [(#2133)](https://github.com/PennyLaneAI/pennylane/pull/2133)
+
+* Fixed a bug in `insert()` which prevented operations that inherited
+  from multiple classes to be inserted.
+  [(#2172)](https://github.com/PennyLaneAI/pennylane/pull/2172)
+
 <h3>Documentation</h3>
 
 * Fixes an error in the signs of equations in the `DoubleExcitation` page.
   [(#2072)](https://github.com/PennyLaneAI/pennylane/pull/2072)
 
-* Extended the interfaces description page to explicitly mention device
+* Extends the interfaces description page to explicitly mention device
   compatibility.
   [(#2031)](https://github.com/PennyLaneAI/pennylane/pull/2031)
 
@@ -590,6 +659,8 @@
 
 This release contains contributions from (in alphabetical order):
 
-Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Esther Cruz, Christian Gogolin, Christina Lee, Olivia Di Matteo, Diego Guala,
-Anthony Hayes, Josh Izaac, Soran Jahangiri, Edward Jiang, Ankit Khandelwal, Korbinian Kottmann, Jay Soni, Antal Száva,
-David Wierichs, Shaoming Zhang
+Juan Miguel Arrazola, Ali Asadi, Utkarsh Azad, Sam Banning, Thomas Bromley,
+Esther Cruz, Christian Gogolin, Nathan Killoran, Christina Lee, Olivia Di
+Matteo, Diego Guala, Anthony Hayes, David Ittah, Josh Izaac, Soran Jahangiri,
+Edward Jiang, Ankit Khandelwal, Korbinian Kottmann, Romain Moyard, Lee James
+O'Riordan, Maria Schuld, Jay Soni, Antal Száva, David Wierichs, Shaoming Zhang.
diff --git a/pennylane/_grad.py b/pennylane/_grad.py
index d770cdf33..a24ac613e 100644
--- a/pennylane/_grad.py
+++ b/pennylane/_grad.py
@@ -14,6 +14,7 @@
 """
 This module contains the autograd wrappers :class:`grad` and :func:`jacobian`
 """
+import warnings
 from functools import partial
 
 import numpy as onp
@@ -78,7 +79,7 @@ class grad:
           as differentiable.
         """
         if self._grad_fn is not None:
-            return self._grad_fn
+            return self._grad_fn, self._argnum
 
         # Inspect the arguments for differentiability, and
         # compute the autograd gradient function with required argnums
@@ -93,16 +94,25 @@ class grad:
         if len(argnum) == 1:
             argnum = argnum[0]
 
-        return self._grad_with_forward(
-            self._fun,
-            argnum=argnum,
-        )
+        return self._grad_with_forward(self._fun, argnum=argnum), argnum
 
     def __call__(self, *args, **kwargs):
         """Evaluates the gradient function, and saves the function value
         calculated during the forward pass in :attr:`.forward`."""
-        grad_value, ans = self._get_grad_fn(args)(*args, **kwargs)
+        grad_fn, argnum = self._get_grad_fn(args)
+
+        if not isinstance(argnum, int) and not argnum:
+            warnings.warn(
+                "Attempted to differentiate a function with no trainable parameters. "
+                "If this is unintended, please add trainable parameters via the "
+                "'requires_grad' attribute or 'argnum' keyword."
+            )
+            self._forward = self._fun(*args, **kwargs)
+            return ()
+
+        grad_value, ans = grad_fn(*args, **kwargs)
         self._forward = ans
+
         return grad_value
 
     @property
@@ -306,6 +316,13 @@ def jacobian(func, argnum=None):
             unpack = isinstance(argnum, int)
             _argnum = [argnum] if unpack else argnum
 
+        if not _argnum:
+            warnings.warn(
+                "Attempted to differentiate a function with no trainable parameters. "
+                "If this is unintended, please add trainable parameters via the "
+                "'requires_grad' attribute or 'argnum' keyword."
+            )
+
         jac = tuple(_jacobian(func, arg)(*args, **kwargs) for arg in _argnum)
 
         return jac[0] if unpack else jac
diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py
index 7831b930c..72e2f85e8 100644
--- a/pennylane/_qubit_device.py
+++ b/pennylane/_qubit_device.py
@@ -128,6 +128,13 @@ class QubitDevice(Device):
         """None or array[int]: stores the samples generated by the device
         *after* rotation to diagonalize the observables."""
 
+        if cache > 0:
+            warnings.warn(
+                "The caching ability of QubitDevice is being deprecated, passing a "
+                "dictionary as the cache argument to the QNode is preferred.",
+                UserWarning,
+            )
+
         self._cache = cache
         """int: Number of device executions to store in a cache to speed up subsequent
         executions. If set to zero, no caching occurs."""
diff --git a/pennylane/drawer/tape_text.py b/pennylane/drawer/tape_text.py
index 96d81e973..ee6a76f0d 100644
--- a/pennylane/drawer/tape_text.py
+++ b/pennylane/drawer/tape_text.py
@@ -104,15 +104,15 @@ def tape_text(
     .. code-block:: python
 
         with qml.tape.QuantumTape() as tape:
-            qml.QFT(wires=(0,1,2))
+            qml.QFT(wires=(0, 1, 2))
             qml.RX(1.234, wires=0)
             qml.RY(1.234, wires=1)
             qml.RZ(1.234, wires=2)
-            qml.Toffoli(wires=(0,1,"aux"))
+            qml.Toffoli(wires=(0, 1, "aux"))
 
             qml.expval(qml.PauliZ("aux"))
             qml.var(qml.PauliZ(0) @ qml.PauliZ(1))
-            qml.probs(wires=(0,1,2,"aux"))
+            qml.probs(wires=(0, 1, 2, "aux"))
 
     >>> print(tape_text(tape))
       0: ─╭QFT──RX─╭C─┤ ╭Var[Z@Z] ╭Probs
@@ -169,7 +169,7 @@ def tape_text(
 
     If the wire order contains empty wires, they are only shown if the ``show_all_wires=True``.
 
-    >>> print(tape_text(tape, wire_order=["a", "b", "aux", 0,1,2], show_all_wires=True))
+    >>> print(tape_text(tape, wire_order=["a", "b", "aux", 0, 1, 2], show_all_wires=True))
       a: ─────────────┤
       b: ─────────────┤
     aux: ──────────╭X─┤  <Z>      ╭Probs
diff --git a/pennylane/gradients/finite_difference.py b/pennylane/gradients/finite_difference.py
index 46334da6c..08b8582ae 100644
--- a/pennylane/gradients/finite_difference.py
+++ b/pennylane/gradients/finite_difference.py
@@ -17,6 +17,7 @@ of a quantum tape.
 """
 # pylint: disable=protected-access,too-many-arguments
 import functools
+import warnings
 
 import numpy as np
 from scipy.special import factorial
@@ -291,6 +292,14 @@ def finite_diff(
         [[-0.38751721 -0.18884787 -0.38355704]
          [ 0.69916862  0.34072424  0.69202359]]
     """
+    if argnum is None and not tape.trainable_params:
+        warnings.warn(
+            "Attempted to compute the gradient of a tape with no trainable parameters. "
+            "If this is unintended, please mark trainable parameters in accordance with the "
+            "chosen auto differentiation framework, or via the 'tape.trainable_params' property."
+        )
+        return [], lambda _: np.zeros([tape.output_dim, len(tape.trainable_params)])
+
     # TODO: replace the JacobianTape._grad_method_validation
     # functionality before deprecation.
     if validate_params:
@@ -298,10 +307,8 @@ def finite_diff(
     else:
         diff_methods = ["F" for i in tape.trainable_params]
 
-    if not tape.trainable_params or all(g == "0" for g in diff_methods):
-        # Either all parameters have grad method 0, or there are no trainable
-        # parameters.
-        return [], lambda x: np.zeros([tape.output_dim, len(tape.trainable_params)])
+    if all(g == "0" for g in diff_methods):
+        return [], lambda _: np.zeros([tape.output_dim, len(tape.trainable_params)])
 
     gradient_tapes = []
     shapes = []
diff --git a/pennylane/gradients/gradient_transform.py b/pennylane/gradients/gradient_transform.py
index 66c286d77..3f06cc385 100644
--- a/pennylane/gradients/gradient_transform.py
+++ b/pennylane/gradients/gradient_transform.py
@@ -14,6 +14,8 @@
 """This module contains utilities for defining custom gradient transforms,
 including a decorator for specifying gradient expansions."""
 # pylint: disable=too-few-public-methods
+import warnings
+
 import pennylane as qml
 from pennylane.transforms.tape_expand import expand_invalid_trainable
 
@@ -117,6 +119,14 @@ class gradient_transform(qml.batch_transform):
         cjac_fn = qml.transforms.classical_jacobian(qnode, expand_fn=expand_invalid_trainable)
 
         def jacobian_wrapper(*args, **kwargs):
+            if not qml.math.get_trainable_indices(args):
+                warnings.warn(
+                    "Attempted to compute the gradient of a QNode with no trainable parameters. "
+                    "If this is unintended, please add trainable parameters in accordance with "
+                    "the chosen auto differentiation framework."
+                )
+                return ()
+
             qjac = _wrapper(*args, **kwargs)
 
             if any(m.return_type is qml.operation.Probability for m in qnode.qtape.measurements):
diff --git a/pennylane/gradients/hessian_transform.py b/pennylane/gradients/hessian_transform.py
index 8110a915c..b81b05924 100644
--- a/pennylane/gradients/hessian_transform.py
+++ b/pennylane/gradients/hessian_transform.py
@@ -13,8 +13,9 @@
 # limitations under the License.
 """This module contains utilities for defining custom Hessian transforms,
 including a decorator for specifying Hessian expansions."""
-import pennylane as qml
+import warnings
 
+import pennylane as qml
 from pennylane.transforms.tape_expand import expand_invalid_trainable
 
 
@@ -113,6 +114,14 @@ class hessian_transform(qml.batch_transform):
         cjac_fn = qml.transforms.classical_jacobian(qnode, expand_fn=self.expand_fn)
 
         def hessian_wrapper(*args, **kwargs):
+            if not qml.math.get_trainable_indices(args):
+                warnings.warn(
+                    "Attempted to compute the hessian of a QNode with no trainable parameters. "
+                    "If this is unintended, please add trainable parameters in accordance with "
+                    "the chosen auto differentiation framework."
+                )
+                return ()
+
             qhess = _wrapper(*args, **kwargs)
 
             if any(m.return_type is qml.operation.Probability for m in qnode.qtape.measurements):
diff --git a/pennylane/gradients/param_shift_hessian.py b/pennylane/gradients/param_shift_hessian.py
index e544fa0f0..7bf8bbd6d 100644
--- a/pennylane/gradients/param_shift_hessian.py
+++ b/pennylane/gradients/param_shift_hessian.py
@@ -15,6 +15,7 @@
 This module contains functions for computing the parameter-shift hessian
 of a qubit-based quantum tape.
 """
+import warnings
 import numpy as np
 
 import pennylane as qml
@@ -302,7 +303,12 @@ def param_shift_hessian(tape, f0=None):
         )
 
     if not tape.trainable_params:
-        return [], lambda _: []
+        warnings.warn(
+            "Attempted to compute the hessian of a tape with no trainable parameters. "
+            "If this is unintended, please mark trainable parameters in accordance with the "
+            "chosen auto differentiation framework, or via the 'tape.trainable_params' property."
+        )
+        return [], lambda _: ()
 
     # The parameter-shift Hessian implementation currently only supports
     # the two-term parameter-shift rule. Raise an error for unsupported operations.
diff --git a/pennylane/gradients/parameter_shift.py b/pennylane/gradients/parameter_shift.py
index 11d6241ee..d00954c2b 100644
--- a/pennylane/gradients/parameter_shift.py
+++ b/pennylane/gradients/parameter_shift.py
@@ -16,6 +16,7 @@ This module contains functions for computing the parameter-shift gradient
 of a qubit-based quantum tape.
 """
 # pylint: disable=protected-access,too-many-arguments,too-many-statements
+import warnings
 import numpy as np
 
 import pennylane as qml
@@ -546,6 +547,11 @@ def param_shift(
     gradient_tapes = []
 
     if argnum is None and not tape.trainable_params:
+        warnings.warn(
+            "Attempted to compute the gradient of a tape with no trainable parameters. "
+            "If this is unintended, please mark trainable parameters in accordance with the "
+            "chosen auto differentiation framework, or via the 'tape.trainable_params' property."
+        )
         return gradient_tapes, lambda _: np.zeros([tape.output_dim, len(tape.trainable_params)])
 
     # TODO: replace the JacobianTape._grad_method_validation
diff --git a/pennylane/gradients/parameter_shift_cv.py b/pennylane/gradients/parameter_shift_cv.py
index 39b9fab0c..f06fecf9f 100644
--- a/pennylane/gradients/parameter_shift_cv.py
+++ b/pennylane/gradients/parameter_shift_cv.py
@@ -646,6 +646,14 @@ def param_shift_cv(
     shapes = []
     fns = []
 
+    if argnum is None and not tape.trainable_params:
+        warnings.warn(
+            "Attempted to compute the gradient of a tape with no trainable parameters. "
+            "If this is unintended, please mark trainable parameters in accordance with the "
+            "chosen auto differentiation framework, or via the 'tape.trainable_params' property."
+        )
+        return gradient_tapes, lambda _: np.zeros([tape.output_dim, len(tape.trainable_params)])
+
     def _update(data):
         """Utility function to update the list of gradient tapes,
         the corresponding number of gradient tapes, and the processing functions"""
@@ -657,8 +665,7 @@ def param_shift_cv(
     # functionality before deprecation.
     diff_methods = tape._grad_method_validation("analytic" if fallback_fn is None else "best")
     all_params_grad_method_zero = all(g == "0" for g in diff_methods)
-
-    if not tape.trainable_params or all_params_grad_method_zero:
+    if all_params_grad_method_zero:
         return gradient_tapes, lambda _: np.zeros([tape.output_dim, len(tape.trainable_params)])
 
     # TODO: replace the JacobianTape._choose_params_with_methods
diff --git a/pennylane/hf/__init__.py b/pennylane/hf/__init__.py
index 5bf4a451c..fb4827a5b 100644
--- a/pennylane/hf/__init__.py
+++ b/pennylane/hf/__init__.py
@@ -12,7 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """
-This subpackage provides the functionality to perform differentiable Hartree-Fock calculations.
+This subpackage provides the functionality to perform differentiable
+Hartree-Fock calculations.
 """
 from .basis_data import STO3G, atomic_numbers
 from .basis_set import BasisFunction, atom_basis_data, mol_basis_data
@@ -20,6 +21,7 @@ from .hamiltonian import (
     generate_electron_integrals,
     generate_fermionic_hamiltonian,
     generate_hamiltonian,
+    simplify,
 )
 from .hartree_fock import generate_scf, hf_energy, nuclear_energy
 from .integrals import (
diff --git a/pennylane/ops/channel.py b/pennylane/ops/channel.py
index c8b56e573..3db47a99e 100644
--- a/pennylane/ops/channel.py
+++ b/pennylane/ops/channel.py
@@ -51,13 +51,10 @@ class AmplitudeDamping(Channel):
         gamma (float): amplitude damping probability
         wires (Sequence[int] or int): the wire the channel acts on
     """
+    num_params = 1
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 1
-
     @classmethod
     def _kraus_matrices(cls, *params):
         gamma = params[0]
@@ -114,13 +111,10 @@ class GeneralizedAmplitudeDamping(Channel):
         p (float): excitation probability
         wires (Sequence[int] or int): the wire the channel acts on
     """
+    num_params = 2
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 2
-
     @classmethod
     def _kraus_matrices(cls, *params):
         gamma, p = params
@@ -169,13 +163,10 @@ class PhaseDamping(Channel):
         gamma (float): phase damping probability
         wires (Sequence[int] or int): the wire the channel acts on
     """
+    num_params = 1
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 1
-
     @classmethod
     def _kraus_matrices(cls, *params):
         gamma = params[0]
@@ -230,14 +221,11 @@ class DepolarizingChannel(Channel):
         p (float): Each Pauli gate is applied with probability :math:`\frac{p}{3}`
         wires (Sequence[int] or int): the wire the channel acts on
     """
+    num_params = 1
     num_wires = 1
     grad_method = "A"
     grad_recipe = ([[1, 0, 1], [-1, 0, 0]],)
 
-    @property
-    def num_params(self):
-        return 1
-
     @classmethod
     def _kraus_matrices(cls, *params):
         p = params[0]
@@ -281,14 +269,11 @@ class BitFlip(Channel):
         p (float): The probability that a bit flip error occurs.
         wires (Sequence[int] or int): the wire the channel acts on
     """
+    num_params = 1
     num_wires = 1
     grad_method = "A"
     grad_recipe = ([[1, 0, 1], [-1, 0, 0]],)
 
-    @property
-    def num_params(self):
-        return 1
-
     @classmethod
     def _kraus_matrices(cls, *params):
         p = params[0]
@@ -350,13 +335,10 @@ class ResetError(Channel):
         p_1 (float): The probability that a reset to 1 error occurs.
         wires (Sequence[int] or int): the wire the channel acts on
     """
+    num_params = 2
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 2
-
     @classmethod
     def _kraus_matrices(cls, *params):
         p_0, p_1 = params[0], params[1]
@@ -503,14 +485,11 @@ class PhaseFlip(Channel):
         p (float): The probability that a phase flip error occurs.
         wires (Sequence[int] or int): the wire the channel acts on
     """
+    num_params = 1
     num_wires = 1
     grad_method = "A"
     grad_recipe = ([[1, 0, 1], [-1, 0, 0]],)
 
-    @property
-    def num_params(self):
-        return 1
-
     @classmethod
     def _kraus_matrices(cls, *params):
         p = params[0]
@@ -540,6 +519,7 @@ class QubitChannel(Channel):
         K_list (list[array[complex]]): List of Kraus matrices
         wires (Union[Wires, Sequence[int], or int]): the wire(s) the operation acts on
     """
+    num_params = 1
     num_wires = AnyWires
     grad_method = None
 
@@ -569,10 +549,6 @@ class QubitChannel(Channel):
         if not np.allclose(Kraus_sum, np.eye(K_list[0].shape[0])):
             raise ValueError("Only trace preserving channels can be applied.")
 
-    @property
-    def num_params(self):
-        return 1
-
     @classmethod
     def _kraus_matrices(cls, *params):
         K_list = params[0]
@@ -656,13 +632,10 @@ class ThermalRelaxationError(Channel):
         tg (float): the gate time for relaxation error.
         wires (Sequence[int] or int): the wire the channel acts on
     """
+    num_params = 4
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 4
-
     @classmethod
     def _kraus_matrices(cls, *params):
         pe = params[0]
diff --git a/pennylane/ops/cv.py b/pennylane/ops/cv.py
index 948f94aaf..ee2957f12 100644
--- a/pennylane/ops/cv.py
+++ b/pennylane/ops/cv.py
@@ -96,13 +96,10 @@ class Rotation(CVOperation):
     Args:
         phi (float): the rotation angle
     """
+    num_params = 1
     num_wires = 1
     grad_method = "A"
 
-    @property
-    def num_params(self):
-        return 1
-
     @staticmethod
     def _heisenberg_rep(p):
         return _rotation(p[0])
@@ -143,6 +140,7 @@ class Squeezing(CVOperation):
         phi (float): squeezing phase angle :math:`\phi`
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 2
     num_wires = 1
     grad_method = "A"
 
@@ -151,10 +149,6 @@ class Squeezing(CVOperation):
     a = 1
     grad_recipe = ([[multiplier, a, shift], [-multiplier, a, -shift]], None)
 
-    @property
-    def num_params(self):
-        return 2
-
     @staticmethod
     def _heisenberg_rep(p):
         R = _rotation(p[1] / 2)
@@ -197,6 +191,7 @@ class Displacement(CVOperation):
         phi (float): phase angle :math:`\phi`
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 2
     num_wires = 1
     grad_method = "A"
 
@@ -205,10 +200,6 @@ class Displacement(CVOperation):
     a = 1
     grad_recipe = ([[multiplier, a, shift], [-multiplier, a, -shift]], None)
 
-    @property
-    def num_params(self):
-        return 2
-
     @staticmethod
     def _heisenberg_rep(p):
         c = math.cos(p[1])
@@ -257,13 +248,10 @@ class Beamsplitter(CVOperation):
             The value :math:`\phi = \pi/2` gives the symmetric beamsplitter.
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 2
     num_wires = 2
     grad_method = "A"
 
-    @property
-    def num_params(self):
-        return 2
-
     # For the beamsplitter, both parameters are rotation-like
     @staticmethod
     def _heisenberg_rep(p):
@@ -317,6 +305,7 @@ class TwoModeSqueezing(CVOperation):
         phi (float): squeezing phase angle :math:`\phi`
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 2
     num_wires = 2
 
     grad_method = "A"
@@ -326,10 +315,6 @@ class TwoModeSqueezing(CVOperation):
     a = 1
     grad_recipe = ([[multiplier, a, shift], [-multiplier, a, -shift]], None)
 
-    @property
-    def num_params(self):
-        return 2
-
     @staticmethod
     def _heisenberg_rep(p):
         R = _rotation(p[1], bare=True)
@@ -378,6 +363,7 @@ class QuadraticPhase(CVOperation):
         s (float): parameter
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 1
     num_wires = 1
 
     grad_method = "A"
@@ -387,10 +373,6 @@ class QuadraticPhase(CVOperation):
     a = 1
     grad_recipe = ([[multiplier, a, shift], [-multiplier, a, -shift]],)
 
-    @property
-    def num_params(self):
-        return 1
-
     @staticmethod
     def _heisenberg_rep(p):
         U = np.identity(3)
@@ -431,6 +413,7 @@ class ControlledAddition(CVOperation):
         s (float): addition multiplier
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 1
     num_wires = 2
     grad_method = "A"
 
@@ -439,10 +422,6 @@ class ControlledAddition(CVOperation):
     a = 1
     grad_recipe = ([[multiplier, a, shift], [-multiplier, a, -shift]],)
 
-    @property
-    def num_params(self):
-        return 1
-
     @staticmethod
     def _heisenberg_rep(p):
         U = np.identity(5)
@@ -487,6 +466,7 @@ class ControlledPhase(CVOperation):
         s (float):  phase shift multiplier
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 1
     num_wires = 2
     grad_method = "A"
 
@@ -495,10 +475,6 @@ class ControlledPhase(CVOperation):
     a = 1
     grad_recipe = ([[multiplier, a, shift], [-multiplier, a, -shift]],)
 
-    @property
-    def num_params(self):
-        return 1
-
     @staticmethod
     def _heisenberg_rep(p):
         U = np.identity(5)
@@ -530,13 +506,10 @@ class Kerr(CVOperation):
         kappa (float): parameter
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 1
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 1
-
     def adjoint(self, do_queue=True):
         return Kerr(-self.parameters[0], wires=self.wires, do_queue=do_queue)
 
@@ -558,13 +531,10 @@ class CrossKerr(CVOperation):
         kappa (float): parameter
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 1
     num_wires = 2
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 1
-
     def adjoint(self, do_queue=True):
         return CrossKerr(-self.parameters[0], wires=self.wires, do_queue=do_queue)
 
@@ -586,13 +556,10 @@ class CubicPhase(CVOperation):
         gamma (float): parameter
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 1
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 1
-
     def adjoint(self, do_queue=True):
         return CubicPhase(-self.parameters[0], wires=self.wires, do_queue=do_queue)
 
@@ -634,14 +601,11 @@ class InterferometerUnitary(CVOperation):
         U (array): A shape ``(len(wires), len(wires))`` complex unitary matrix
         wires (Sequence[int] or int): the wires the operation acts on
     """
+    num_params = 1
     num_wires = AnyWires
     grad_method = None
     grad_recipe = None
 
-    @property
-    def num_params(self):
-        return 1
-
     @staticmethod
     def _heisenberg_rep(p):
         N = len(p[0])
@@ -687,13 +651,10 @@ class CoherentState(CVOperation):
         phi (float): phase angle :math:`\phi`
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 2
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 2
-
 
 class SqueezedState(CVOperation):
     r"""pennylane.SqueezedState(r, phi, wires)
@@ -710,13 +671,10 @@ class SqueezedState(CVOperation):
         phi (float): squeezing angle :math:`\phi`
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 2
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 2
-
 
 class DisplacedSqueezedState(CVOperation):
     r"""pennylane.DisplacedSqueezedState(a, phi_a, r, phi_r, wires)
@@ -743,13 +701,10 @@ class DisplacedSqueezedState(CVOperation):
         phi_r (float): squeezing angle :math:`\phi_r`
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 4
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 4
-
 
 class ThermalState(CVOperation):
     r"""pennylane.ThermalState(nbar, wires)
@@ -765,13 +720,10 @@ class ThermalState(CVOperation):
         nbar (float): mean thermal population of the mode
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 1
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 1
-
     def label(self, decimals=None, base_label=None):
         return super().label(decimals=decimals, base_label=base_label or "Thermal")
 
@@ -792,13 +744,10 @@ class GaussianState(CVOperation):
             form :math:`(\x_0,\dots,\x_{N-1},\p_0,\dots,\p_{N-1})`
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 2
     num_wires = AnyWires
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 2
-
     def label(self, decimals=None, base_label=None):
         return super().label(decimals=decimals, base_label=base_label or "Gaussian")
 
@@ -817,13 +766,10 @@ class FockState(CVOperation):
         n (int): Fock state to prepare
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 1
     num_wires = 1
     grad_method = None
 
-    @property
-    def num_params(self):
-        return 1
-
     def label(self, decimals=None, base_label=None):
         r"""A customizable string representation of the operator.
 
@@ -903,13 +849,10 @@ class FockStateVector(CVOperation):
                 return qml.expval(qml.NumberOperator(wires=0))
 
     """
+    num_params = 1
     num_wires = AnyWires
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 1
-
     def label(self, decimals=None, base_label=None):
         r"""A customizable string representation of the operator.
 
@@ -947,13 +890,10 @@ class FockDensityMatrix(CVOperation):
         state (array): a single mode matrix :math:`\rho_{ij}`, or
             a multimode tensor :math:`\rho_{ij,kl,\dots,mn}`, with two indices per mode
     """
+    num_params = 1
     num_wires = AnyWires
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 1
-
 
 class CatState(CVOperation):
     r"""pennylane.CatState(a, phi, p, wires)
@@ -981,13 +921,10 @@ class CatState(CVOperation):
             cat state, and :math:`p=1` an odd cat state.
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 3
     num_wires = 1
     grad_method = "F"
 
-    @property
-    def num_params(self):
-        return 3
-
 
 # =============================================================================
 # Observables
@@ -1020,14 +957,11 @@ class NumberOperator(CVObservable):
     Args:
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 0
     num_wires = 1
 
     ev_order = 2
 
-    @property
-    def num_params(self):
-        return 0
-
     @staticmethod
     def _heisenberg_rep(p):
         hbar = 2
@@ -1074,6 +1008,7 @@ class TensorN(CVObservable):
         >>> cv_obs.ev_order
         2
     """
+    num_params = 0
     num_wires = AnyWires
     ev_order = None
 
@@ -1089,10 +1024,6 @@ class TensorN(CVObservable):
             return NumberOperator(*params, wires=wires, do_queue=do_queue)
         return super().__new__(cls)
 
-    @property
-    def num_params(self):
-        return 0
-
     def label(self, decimals=None, base_label=None):
         if base_label is not None:
             return base_label
@@ -1119,14 +1050,11 @@ class X(CVObservable):
     Args:
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 0
     num_wires = 1
 
     ev_order = 1
 
-    @property
-    def num_params(self):
-        return 0
-
     @staticmethod
     def _heisenberg_rep(p):
         return np.array([0, 1, 0])
@@ -1152,14 +1080,11 @@ class P(CVObservable):
     Args:
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 0
     num_wires = 1
 
     ev_order = 1
 
-    @property
-    def num_params(self):
-        return 0
-
     @staticmethod
     def _heisenberg_rep(p):
         return np.array([0, 0, 1])
@@ -1187,15 +1112,12 @@ class QuadOperator(CVObservable):
             the generalized quadrature observable
         wires (Sequence[int] or int): the wire the operation acts on
     """
+    num_params = 1
     num_wires = 1
 
     grad_method = "A"
     ev_order = 1
 
-    @property
-    def num_params(self):
-        return 1
-
     @staticmethod
     def _heisenberg_rep(p):
         phi = p[0]
@@ -1261,15 +1183,12 @@ class PolyXP(CVObservable):
         q (array[float]): expansion coefficients
 
     """
+    num_params = 1
     num_wires = AnyWires
 
     grad_method = "F"
     ev_order = 2
 
-    @property
-    def num_params(self):
-        return 1
-
     @staticmethod
     def _heisenberg_rep(p):
         return p[0]
@@ -1317,15 +1236,12 @@ class FockStateProjector(CVObservable):
             Note that ``len(n)==len(wires)``, and that ``len(n)`` cannot exceed the
             total number of wires in the QNode.
     """
+    num_params = 1
     num_wires = AnyWires
 
     grad_method = None
     ev_order = None
 
-    @property
-    def num_params(self):
-        return 1
-
     def label(self, decimals=None, base_label=None):
         r"""A customizable string representation of the operator.
 
diff --git a/pennylane/ops/identity.py b/pennylane/ops/identity.py
index b5d66a2e7..d68f86c2e 100644
--- a/pennylane/ops/identity.py
+++ b/pennylane/ops/identity.py
@@ -32,16 +32,13 @@ class Identity(CVObservable, Operation):
     corresponds to the trace of the quantum state, which in exact
     simulators should always be equal to 1.
     """
+    num_params = 0
     num_wires = 1
     grad_method = None
 
     ev_order = 1
     eigvals = np.array([1, 1])
 
-    @property
-    def num_params(self):
-        return 0
-
     def label(self, decimals=None, base_label=None):
         return base_label or "I"
 
diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py
index 4a5374b44..385b7eba9 100644
--- a/pennylane/ops/qubit/non_parametric_ops.py
+++ b/pennylane/ops/qubit/non_parametric_ops.py
@@ -314,7 +314,9 @@ class S(Operation):
         return decomp_ops
 
     def adjoint(self):
-        return S(wires=self.wires).inv()
+        op = S(wires=self.wires)
+        op.inverse = not self.inverse
+        return op
 
     def single_qubit_rot_angles(self):
         # S = RZ(\pi/2) RY(0) RZ(0)
@@ -358,7 +360,9 @@ class T(Operation):
         return decomp_ops
 
     def adjoint(self):
-        return T(wires=self.wires).inv()
+        op = T(wires=self.wires)
+        op.inverse = not self.inverse
+        return op
 
     def single_qubit_rot_angles(self):
         # T = RZ(\pi/4) RY(0) RZ(0)
@@ -407,7 +411,9 @@ class SX(Operation):
         return decomp_ops
 
     def adjoint(self):
-        return SX(wires=self.wires).inv()
+        op = SX(wires=self.wires)
+        op.inverse = not self.inverse
+        return op
 
     def single_qubit_rot_angles(self):
         # SX = RZ(-\pi/2) RY(\pi/2) RZ(\pi/2)
@@ -646,7 +652,9 @@ class ISWAP(Operation):
         return decomp_ops
 
     def adjoint(self):
-        return ISWAP(wires=self.wires).inv()
+        op = ISWAP(wires=self.wires)
+        op.inverse = not self.inverse
+        return op
 
 
 class SISWAP(Operation):
@@ -707,7 +715,9 @@ class SISWAP(Operation):
         return decomp_ops
 
     def adjoint(self):
-        return SISWAP(wires=self.wires).inv()
+        op = SISWAP(wires=self.wires)
+        op.inverse = not self.inverse
+        return op
 
 
 SQISW = SISWAP
diff --git a/pennylane/qnode.py b/pennylane/qnode.py
index bdfd1983c..0a0a7da27 100644
--- a/pennylane/qnode.py
+++ b/pennylane/qnode.py
@@ -217,6 +217,7 @@ class QNode:
         self._original_device = device
         self.gradient_fn = None
         self.gradient_kwargs = None
+        self._tape_cached = False
 
         self._update_gradient_fn()
         functools.update_wrapper(self, func)
@@ -265,7 +266,9 @@ class QNode:
         # 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
+
+            if not self._tape_cached:
+                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"):
@@ -546,6 +549,14 @@ class QNode:
         # construct the tape
         self.construct(args, kwargs)
 
+        cache = self.execute_kwargs.get("cache", False)
+        using_custom_cache = (
+            hasattr(cache, "__getitem__")
+            and hasattr(cache, "__setitem__")
+            and hasattr(cache, "__delitem__")
+        )
+        self._tape_cached = using_custom_cache and self.tape.hash in cache
+
         res = qml.execute(
             [self.tape],
             device=self.device,
diff --git a/pennylane/templates/embeddings/basis.py b/pennylane/templates/embeddings/basis.py
index 399175020..8cb223790 100644
--- a/pennylane/templates/embeddings/basis.py
+++ b/pennylane/templates/embeddings/basis.py
@@ -33,7 +33,7 @@ class BasisEmbedding(Operation):
         gradients with respect to the argument cannot be computed by PennyLane.
 
     Args:
-        features (tensor-like): binary input of shape ``(n, )``
+        features (tensor_like): binary input of shape ``(n, )``
         wires (Iterable): wires that the template acts on
 
     Example:
diff --git a/pennylane/templates/state_preparations/basis.py b/pennylane/templates/state_preparations/basis.py
index 20f806342..fca2cd9ec 100644
--- a/pennylane/templates/state_preparations/basis.py
+++ b/pennylane/templates/state_preparations/basis.py
@@ -43,7 +43,7 @@ class BasisStatePreparation(Operation):
         print(circuit(basis_state)) # [ 1. -1. -1.  1.]
 
     Args:
-        basis_state (array): Input array of shape ``(n,)``, where n is the number of wires
+        basis_state (tensor_like): Input of shape ``(n,)``, where n is the number of wires
             the state preparation acts on.
         wires (Iterable): wires that the template acts on
     """
diff --git a/pennylane/templates/subroutines/commuting_evolution.py b/pennylane/templates/subroutines/commuting_evolution.py
index cedc3dba4..889db61c0 100644
--- a/pennylane/templates/subroutines/commuting_evolution.py
+++ b/pennylane/templates/subroutines/commuting_evolution.py
@@ -102,7 +102,6 @@ class CommutingEvolution(Operation):
         0.6536436208636115
     """
 
-    num_params = 3
     num_wires = AnyWires
     par_domain = "R"
     grad_method = None
@@ -125,12 +124,15 @@ class CommutingEvolution(Operation):
             self.grad_method = "A"
 
         self.hamiltonian = hamiltonian
-        self.num_params = len(hamiltonian.data) + 1
         self.frequencies = frequencies
         self.shifts = shifts
 
         super().__init__(time, *hamiltonian.data, wires=hamiltonian.wires, do_queue=do_queue, id=id)
 
+    @property
+    def num_params(self):
+        return len(self.hamiltonian.data) + 1
+
     def expand(self):
         # uses standard PauliRot decomposition through ApproxTimeEvolution.
         hamiltonian = qml.Hamiltonian(self.parameters[1:], self.hamiltonian.ops)
diff --git a/pennylane/templates/subroutines/interferometer.py b/pennylane/templates/subroutines/interferometer.py
index 42e9fc958..dc8633032 100644
--- a/pennylane/templates/subroutines/interferometer.py
+++ b/pennylane/templates/subroutines/interferometer.py
@@ -155,7 +155,7 @@ class Interferometer(CVOperation):
 
         Using these random parameters, the resulting circuit is:
 
-        >>> print(qml.draw(circuit)(params))
+        >>> print(qml.draw(circuit, expansion_strategy="device")(params))
             0: ──╭BS(0.0522, 0.0472)────────────────────╭BS(0.438, 0.222)───R(0.606)────────────────────┤ ⟨I⟩
             1: ──╰BS(0.0522, 0.0472)──╭BS(0.994, 0.59)──╰BS(0.438, 0.222)──╭BS(0.823, 0.623)──R(0.221)──┤
             2: ──╭BS(0.636, 0.298)────╰BS(0.994, 0.59)──╭BS(0.0818, 0.72)──╰BS(0.823, 0.623)──R(0.807)──┤
@@ -177,7 +177,7 @@ class Interferometer(CVOperation):
 
         The resulting circuit in this case is:
 
-        >>> print(qml.draw(circuit)(params))
+        >>> print(qml.draw(circuit, expansion_strategy="device")(params))
             0: ──R(0.713)──────────────────────────────────╭BS(0.213, 0)───R(0.681)──────────────────────────────────────────────────────────┤ ⟨I⟩
             1: ──R(0.00912)─────────────────╭BS(0.239, 0)──╰BS(0.213, 0)───R(0.388)──────╭BS(0.622, 0)──R(0.567)─────────────────────────────┤
             2: ──R(0.43)─────╭BS(0.534, 0)──╰BS(0.239, 0)───R(0.189)──────╭BS(0.809, 0)──╰BS(0.622, 0)──R(0.309)──╭BS(0.00845, 0)──R(0.757)──┤
diff --git a/pennylane/transforms/batch_params.py b/pennylane/transforms/batch_params.py
index b93a30337..95af35b67 100644
--- a/pennylane/transforms/batch_params.py
+++ b/pennylane/transforms/batch_params.py
@@ -110,6 +110,11 @@ def batch_params(tape, all_operations=False):
     -0.30262974103192636
     """
     params = tape.get_parameters(trainable_only=not all_operations)
+    if not params:
+        raise ValueError(
+            "There are no operations to transform. Either add trainable parameters, "
+            "or specify `all_operations=True`."
+        )
     output_tapes = []
 
     try:
diff --git a/pennylane/transforms/insert_ops.py b/pennylane/transforms/insert_ops.py
index 7ec72f172..b207ff8d7 100644
--- a/pennylane/transforms/insert_ops.py
+++ b/pennylane/transforms/insert_ops.py
@@ -35,13 +35,13 @@ def _check_position(position):
         req_ops = position.copy()
         for operation in req_ops:
             try:
-                if operation.__base__ != Operation:
+                if Operation not in operation.__bases__:
                     not_op = True
             except AttributeError:
                 not_op = True
     elif not isinstance(position, list):
         try:
-            if position.__base__ == Operation:
+            if Operation in position.__bases__:
                 req_ops = [position]
             else:
                 not_op = True
diff --git a/pennylane/transforms/metric_tensor.py b/pennylane/transforms/metric_tensor.py
index b9b522cbd..d1b559617 100644
--- a/pennylane/transforms/metric_tensor.py
+++ b/pennylane/transforms/metric_tensor.py
@@ -244,6 +244,14 @@ def metric_tensor(tape, approx=None, allow_nonunitary=True, aux_wire=None, devic
         This means that in total only the tapes for the first terms of the off block-diagonal
         are required in addition to the circuits for the block diagonal.
     """
+    if not tape.trainable_params:
+        warnings.warn(
+            "Attempted to compute the metric tensor of a tape with no trainable parameters. "
+            "If this is unintended, please mark trainable parameters in accordance with the "
+            "chosen auto differentiation framework, or via the 'tape.trainable_params' property."
+        )
+        return [], lambda _: ()
+
     # pylint: disable=too-many-arguments
     if approx in {"diag", "block-diag"}:
         # Only require covariance matrix based transform
@@ -323,6 +331,14 @@ def qnode_execution_wrapper(self, qnode, targs, tkwargs):
     cjac_fn = qml.transforms.classical_jacobian(qnode, expand_fn=_expand_fn)
 
     def wrapper(*args, **kwargs):
+        if not qml.math.get_trainable_indices(args):
+            warnings.warn(
+                "Attempted to compute the metric tensor of a QNode with no trainable parameters. "
+                "If this is unintended, please add trainable parameters in accordance with the "
+                "chosen auto differentiation framework."
+            )
+            return ()
+
         try:
             mt = mt_fn(*args, **kwargs)
         except qml.wires.WireError as e:
diff --git a/setup.py b/setup.py
index cfc227edf..a45389ed7 100644
--- a/setup.py
+++ b/setup.py
@@ -28,7 +28,7 @@ requirements = [
     "semantic_version==2.6",
     "autoray",
     "cachetools",
-    "pennylane-lightning>=0.20",
+    "pennylane-lightning>=0.21",
 ]
 
 info = {


Test patch:
diff --git a/tests/collections/test_qnode_collection.py b/tests/collections/test_qnode_collection.py
index 2f0f50d28..a295f57b8 100644
--- a/tests/collections/test_qnode_collection.py
+++ b/tests/collections/test_qnode_collection.py
@@ -195,7 +195,7 @@ class TestEvalation:
         the Autograd interface"""
         qnode1, qnode2 = qnodes
 
-        params = [0.5643, -0.45]
+        params = np.array([0.5643, -0.45], requires_grad=True)
         qc = qml.QNodeCollection([qnode1, qnode2])
         cost_qc = lambda params: np.sum(qc(params))
         grad_qc = qml.grad(cost_qc)
@@ -206,7 +206,7 @@ class TestEvalation:
         res = grad_qc(params)
         expected = grad_expected(params)
 
-        assert np.all(res == expected)
+        assert len(res) != 0 and np.all(res == expected)
 
     @pytest.mark.parametrize("interface", ["torch"])
     def test_eval_torch(self, qnodes, skip_if_no_torch_support, parallel, interface):
@@ -249,7 +249,11 @@ class TestEvalation:
         qc = qml.QNodeCollection([qnode1, qnode2])
         params = [0.5643, -0.45]
 
-        res = qc(params, parallel=parallel).numpy()
+        if parallel:
+            with pytest.warns(UserWarning, match="Parallel execution of QNodeCollections is an"):
+                res = qc(params, parallel=parallel).numpy()
+        else:
+            res = qc(params, parallel=parallel).numpy()
         expected = onp.vstack([qnode1(params), qnode2(params)])
         assert np.all(res == expected)
 
diff --git a/tests/devices/test_caching.py b/tests/devices/test_caching.py
index cc2b99f23..7ce56954f 100644
--- a/tests/devices/test_caching.py
+++ b/tests/devices/test_caching.py
@@ -29,6 +29,14 @@ def qfunc(x, y):
     return expval(qml.PauliZ(wires=1))
 
 
+def get_device_with_caching(wires, cache):
+    """Get a device that defines caching."""
+    with pytest.warns(UserWarning, match="deprecated"):
+        dev = qml.device("default.qubit", wires=wires, cache=cache)
+
+    return dev
+
+
 class TestCaching:
     """Tests for device caching"""
 
@@ -37,7 +45,7 @@ class TestCaching:
         dev = qml.device("default.qubit", wires=2)
         assert dev.cache == 0
 
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         assert dev.cache == 10
 
     def test_no_caching(self, mocker):
@@ -54,7 +62,7 @@ class TestCaching:
 
     def test_caching(self, mocker):
         """Test that caching occurs when the cache attribute is above zero"""
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev)
 
         qn(0.1, 0.2)
@@ -66,7 +74,7 @@ class TestCaching:
 
     def test_add_to_cache_execute(self):
         """Test that the _cache_execute attribute is added to when the device is executed"""
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev)
 
         result = qn(0.1, 0.2)
@@ -80,7 +88,7 @@ class TestCaching:
     def test_fill_cache(self):
         """Test that the cache is added to until it reaches its maximum size (in this case 10),
         and then maintains that size upon subsequent additions."""
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev)
 
         args = np.arange(20)
@@ -96,7 +104,7 @@ class TestCaching:
     def test_drop_from_cache(self):
         """Test that the first entry of the _cache_execute dictionary is the first to be dropped
         from the dictionary once it becomes full"""
-        dev = qml.device("default.qubit", wires=2, cache=2)
+        dev = get_device_with_caching(wires=2, cache=2)
         qn = QNode(qfunc, dev)
 
         qn(0.1, 0.2)
@@ -110,7 +118,7 @@ class TestCaching:
     def test_caching_multiple_values(self, mocker):
         """Test that multiple device executions with different params are cached and accessed on
         subsequent executions"""
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev)
 
         args = np.arange(10)
@@ -126,7 +134,7 @@ class TestCaching:
 
     def test_backprop_error(self):
         """Test if an error is raised when caching is used with the backprop diff_method"""
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         with pytest.raises(
             qml.QuantumFunctionError, match="Device caching is incompatible with the backprop"
         ):
@@ -135,7 +143,7 @@ class TestCaching:
     def test_gradient_autograd(self, mocker):
         """Test that caching works when calculating the gradient using the autograd
         interface"""
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev, interface="autograd")
         d_qnode = qml.grad(qn)
         args = np.array([0.1, 0.2], requires_grad=True)
@@ -150,7 +158,7 @@ class TestCaching:
         """Test that caching works when calculating the gradient using the TF interface"""
         import tensorflow as tf
 
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev, interface="tf")
         args0 = tf.Variable(0.1)
         args1 = tf.Variable(0.2)
@@ -173,7 +181,7 @@ class TestCaching:
         """Test that caching works when calculating the gradient using the Torch interface"""
         import torch
 
-        dev = qml.device("default.qubit", wires=2, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
         qn = QNode(qfunc, dev, interface="torch")
         args0 = torch.tensor(0.1, requires_grad=True)
         args1 = torch.tensor(0.2)
@@ -191,7 +199,7 @@ class TestCaching:
         """Test that caching is compatible with circuit mutability. Caching should take place if
         the circuit and parameters are the same, and should not take place if the parameters are
         the same but the circuit different."""
-        dev = qml.device("default.qubit", wires=3, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
 
         @qnode(dev)
         def qfunc(x, y, flag=1):
@@ -215,7 +223,7 @@ class TestCaching:
     def test_classical_processing_in_circuit(self, mocker):
         """Test if caching is compatible with QNodes that include classical processing"""
 
-        dev = qml.device("default.qubit", wires=3, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
 
         @qnode(dev)
         def qfunc(x, y):
@@ -236,7 +244,7 @@ class TestCaching:
         """Test that caching is compatible with calculating the gradient in QNodes which contain
         classical processing"""
 
-        dev = qml.device("default.qubit", wires=3, cache=10)
+        dev = get_device_with_caching(wires=2, cache=10)
 
         @qnode(dev)
         def qfunc(x, y):
@@ -265,7 +273,7 @@ class TestCaching:
 
     devs = [
         (qml.device("default.qubit", wires=2)),
-        (qml.device("default.qubit", cache=1, wires=2)),
+        (get_device_with_caching(wires=2, cache=10)),
     ]
 
     @pytest.mark.parametrize("dev", devs)
diff --git a/tests/gradients/test_finite_difference.py b/tests/gradients/test_finite_difference.py
index 9e3008cb2..05c84f8f7 100644
--- a/tests/gradients/test_finite_difference.py
+++ b/tests/gradients/test_finite_difference.py
@@ -180,27 +180,48 @@ class TestFiniteDiff:
         # only called for parameter 0
         assert spy.call_args[0][0:2] == (tape, 0)
 
-    def test_no_trainable_parameters(self, mocker):
-        """Test that if the tape has no trainable parameters, no
-        subroutines are called and the returned Jacobian is empty"""
-        spy = mocker.spy(qml.gradients.finite_difference, "generate_shifted_tapes")
+    @pytest.mark.parametrize("interface", ["autograd", "jax", "torch", "tensorflow"])
+    def test_no_trainable_params_qnode(self, interface):
+        """Test that the correct ouput and warning is generated in the absence of any trainable
+        parameters"""
+        if interface != "autograd":
+            pytest.importorskip(interface)
+        dev = qml.device("default.qubit", wires=2)
 
-        with qml.tape.JacobianTape() as tape:
-            qml.RX(0.543, wires=[0])
-            qml.RY(-0.654, wires=[1])
-            qml.expval(qml.PauliZ(0))
+        @qml.qnode(dev, interface=interface)
+        def circuit(weights):
+            qml.RX(weights[0], wires=0)
+            qml.RY(weights[1], wires=0)
+            return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
+
+        weights = [0.1, 0.2]
+        with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"):
+            res = qml.gradients.finite_diff(circuit)(weights)
+
+        assert res == ()
 
+    def test_no_trainable_params_tape(self):
+        """Test that the correct ouput and warning is generated in the absence of any trainable
+        parameters"""
         dev = qml.device("default.qubit", wires=2)
-        tape.trainable_params = {}
 
-        tapes, fn = finite_diff(tape)
-        res = fn(dev.batch_execute(tapes))
+        weights = [0.1, 0.2]
+        with qml.tape.QuantumTape() as tape:
+            qml.RX(weights[0], wires=0)
+            qml.RY(weights[1], wires=0)
+            qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
+
+        # TODO: remove once #2155 is resolved
+        tape.trainable_params = []
+
+        with pytest.warns(UserWarning, match="gradient of a tape with no trainable parameters"):
+            g_tapes, post_processing = qml.gradients.finite_diff(tape)
+        res = post_processing(qml.execute(g_tapes, dev, None))
+
+        assert g_tapes == []
         assert res.size == 0
         assert np.all(res == np.array([[]]))
 
-        spy.assert_not_called()
-        assert len(tapes) == 0
-
     def test_y0(self, mocker):
         """Test that if first order finite differences is used, then
         the tape is executed only once using the current parameter
diff --git a/tests/gradients/test_gradient_transform.py b/tests/gradients/test_gradient_transform.py
index 40417f947..f69069362 100644
--- a/tests/gradients/test_gradient_transform.py
+++ b/tests/gradients/test_gradient_transform.py
@@ -395,15 +395,15 @@ class TestGradientTransformIntegration:
         """Raise an exception if shots is used within the QNode"""
         dev = qml.device("default.qubit", wires=1, shots=1000)
 
-        @qml.qnode(dev)
         def circuit(x, shots):
             qml.RX(x, wires=0)
             return qml.expval(qml.PauliZ(0))
 
-        with pytest.raises(
-            ValueError, match="'shots' argument name is reserved for overriding the number of shots"
-        ):
-            qml.gradients.param_shift(circuit)(0.2, shots=100)
+        with pytest.warns(UserWarning, match="Detected 'shots' as an argument to the given"):
+            qnode = qml.QNode(circuit, dev)
+
+        with pytest.raises(ValueError, match="Detected 'shots' as an argument of the quantum"):
+            qml.gradients.param_shift(qnode)(0.2, shots=100)
 
 
 class TestInterfaceIntegration:
diff --git a/tests/gradients/test_param_shift_hessian.py b/tests/gradients/test_param_shift_hessian.py
index 55433b909..3867b28dc 100644
--- a/tests/gradients/test_param_shift_hessian.py
+++ b/tests/gradients/test_param_shift_hessian.py
@@ -501,24 +501,46 @@ class TestParameterShiftHessian:
 
         qml.gradients.param_shift_hessian(circuit)(x, y, z)
 
-    def test_no_trainable_parameters(self):
-        """Test that the correct ouput is generated in the absence of any trainable parameters"""
+    @pytest.mark.parametrize("interface", ["autograd", "jax", "torch", "tensorflow"])
+    def test_no_trainable_params_qnode(self, interface):
+        """Test that the correct ouput and warning is generated in the absence of any trainable
+        parameters"""
+        if interface != "autograd":
+            pytest.importorskip(interface)
+        dev = qml.device("default.qubit", wires=2)
+
+        @qml.qnode(dev, interface=interface)
+        def circuit(weights):
+            qml.RX(weights[0], wires=0)
+            qml.RY(weights[1], wires=0)
+            return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
+
+        weights = [0.1, 0.2]
+        with pytest.warns(UserWarning, match="hessian of a QNode with no trainable parameters"):
+            res = qml.gradients.param_shift_hessian(circuit)(weights)
+
+        assert res == ()
 
+    def test_no_trainable_params_tape(self):
+        """Test that the correct ouput and warning is generated in the absence of any trainable
+        parameters"""
         dev = qml.device("default.qubit", wires=2)
 
-        @qml.qnode(dev, diff_method="parameter-shift", max_diff=2)
-        def circuit(x):
-            qml.RX(x[0], wires=0)
-            qml.RY(x[1], wires=0)
-            qml.CRZ(x[2], wires=[0, 1])
-            return qml.probs(wires=1)
+        weights = [0.1, 0.2]
+        with qml.tape.QuantumTape() as tape:
+            qml.RX(weights[0], wires=0)
+            qml.RY(weights[1], wires=0)
+            qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
 
-        x = np.array([0.1, 0.2, 0.3], requires_grad=False)
+        # TODO: remove once #2155 is resolved
+        tape.trainable_params = []
 
-        expected = qml.jacobian(qml.jacobian(circuit))(x)
-        hessian = qml.gradients.param_shift_hessian(circuit)(x)
+        with pytest.warns(UserWarning, match="hessian of a tape with no trainable parameters"):
+            h_tapes, post_processing = qml.gradients.param_shift_hessian(tape)
+        res = post_processing(qml.execute(h_tapes, dev, None))
 
-        assert np.allclose(expected, hessian)
+        assert h_tapes == []
+        assert res == ()
 
     def test_f0_argument(self):
         """Test that we can provide the results of a QNode to save on quantum invocations"""
diff --git a/tests/gradients/test_parameter_shift.py b/tests/gradients/test_parameter_shift.py
index 408804b09..3633037f7 100644
--- a/tests/gradients/test_parameter_shift.py
+++ b/tests/gradients/test_parameter_shift.py
@@ -151,7 +151,8 @@ class TestParamShift:
         with qml.tape.JacobianTape() as tape:
             qml.expval(qml.PauliZ(0))
 
-        tapes, _ = qml.gradients.param_shift(tape)
+        with pytest.warns(UserWarning, match="gradient of a tape with no trainable parameters"):
+            tapes, _ = qml.gradients.param_shift(tape)
         assert not tapes
 
     def test_all_parameters_independent(self):
@@ -196,21 +197,45 @@ class TestParamShift:
         # only called for parameter 0
         assert spy.call_args[0][0:2] == (tape, [0])
 
-    def test_no_trainable_parameters(self):
-        """Test that if the tape has no trainable parameters, no
-        subroutines are called and the returned Jacobian is empty"""
-        with qml.tape.JacobianTape() as tape:
-            qml.RX(0.543, wires=[0])
-            qml.RY(-0.654, wires=[1])
-            qml.expval(qml.PauliZ(0))
+    @pytest.mark.parametrize("interface", ["autograd", "jax", "torch", "tensorflow"])
+    def test_no_trainable_params_qnode(self, interface):
+        """Test that the correct ouput and warning is generated in the absence of any trainable
+        parameters"""
+        if interface != "autograd":
+            pytest.importorskip(interface)
+        dev = qml.device("default.qubit", wires=2)
+
+        @qml.qnode(dev, interface=interface)
+        def circuit(weights):
+            qml.RX(weights[0], wires=0)
+            qml.RY(weights[1], wires=0)
+            return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
 
+        weights = [0.1, 0.2]
+        with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"):
+            res = qml.gradients.param_shift(circuit)(weights)
+
+        assert res == ()
+
+    def test_no_trainable_params_tape(self):
+        """Test that the correct ouput and warning is generated in the absence of any trainable
+        parameters"""
         dev = qml.device("default.qubit", wires=2)
-        tape.trainable_params = {}
 
-        tapes, fn = qml.gradients.param_shift(tape)
-        assert len(tapes) == 0
+        weights = [0.1, 0.2]
+        with qml.tape.QuantumTape() as tape:
+            qml.RX(weights[0], wires=0)
+            qml.RY(weights[1], wires=0)
+            qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
 
-        res = fn(dev.batch_execute(tapes))
+        # TODO: remove once #2155 is resolved
+        tape.trainable_params = []
+
+        with pytest.warns(UserWarning, match="gradient of a tape with no trainable parameters"):
+            g_tapes, post_processing = qml.gradients.param_shift(tape)
+        res = post_processing(qml.execute(g_tapes, dev, None))
+
+        assert g_tapes == []
         assert res.size == 0
         assert np.all(res == np.array([[]]))
 
diff --git a/tests/gradients/test_parameter_shift_cv.py b/tests/gradients/test_parameter_shift_cv.py
index 67c5aed4e..d58054223 100644
--- a/tests/gradients/test_parameter_shift_cv.py
+++ b/tests/gradients/test_parameter_shift_cv.py
@@ -271,6 +271,48 @@ class TestTransformObservable:
 class TestParameterShiftLogic:
     """Test for the dispatching logic of the parameter shift method"""
 
+    @pytest.mark.parametrize("interface", ["autograd", "jax", "torch", "tensorflow"])
+    def test_no_trainable_params_qnode(self, interface):
+        """Test that the correct ouput and warning is generated in the absence of any trainable
+        parameters"""
+        if interface != "autograd":
+            pytest.importorskip(interface)
+        dev = qml.device("default.gaussian", wires=2)
+
+        @qml.qnode(dev, interface=interface)
+        def circuit(weights):
+            qml.Displacement(weights[0], 0.0, wires=[0])
+            qml.Rotation(weights[1], wires=[0])
+            return qml.expval(qml.X(0))
+
+        weights = [0.1, 0.2]
+        with pytest.warns(UserWarning, match="gradient of a QNode with no trainable parameters"):
+            res = qml.gradients.param_shift_cv(circuit)(weights)
+
+        assert res == ()
+
+    def test_no_trainable_params_tape(self):
+        """Test that the correct ouput and warning is generated in the absence of any trainable
+        parameters"""
+        dev = qml.device("default.gaussian", wires=2)
+
+        weights = [0.1, 0.2]
+        with qml.tape.JacobianTape() as tape:
+            qml.Displacement(weights[0], 0.0, wires=[0])
+            qml.Rotation(weights[1], wires=[0])
+            qml.expval(qml.X(0))
+
+        # TODO: remove once #2155 is resolved
+        tape.trainable_params = []
+
+        with pytest.warns(UserWarning, match="gradient of a tape with no trainable parameters"):
+            g_tapes, post_processing = qml.gradients.param_shift_cv(tape, dev)
+        res = post_processing(qml.execute(g_tapes, dev, None))
+
+        assert g_tapes == []
+        assert res.size == 0
+        assert np.all(res == np.array([[]]))
+
     def test_state_non_differentiable_error(self):
         """Test error raised if attempting to differentiate with
         respect to a state"""
diff --git a/tests/interfaces/test_batch_autograd.py b/tests/interfaces/test_batch_autograd.py
index 0fda44306..d40625b36 100644
--- a/tests/interfaces/test_batch_autograd.py
+++ b/tests/interfaces/test_batch_autograd.py
@@ -584,13 +584,14 @@ class TestAutogradExecuteIntegration:
         res = cost(a, b, device=dev)
         assert res.shape == (2,)
 
-        res = qml.jacobian(cost)(a, b, device=dev)
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
+            res = qml.jacobian(cost)(a, b, device=dev)
         assert len(res) == 0
 
         def loss(a, b):
             return np.sum(cost(a, b, device=dev))
 
-        with pytest.warns(UserWarning, match="Output seems independent"):
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
             res = qml.grad(loss)(a, b)
 
         assert np.allclose(res, 0)
diff --git a/tests/interfaces/test_batch_autograd_qnode.py b/tests/interfaces/test_batch_autograd_qnode.py
index f3a8bef32..be00e29c8 100644
--- a/tests/interfaces/test_batch_autograd_qnode.py
+++ b/tests/interfaces/test_batch_autograd_qnode.py
@@ -336,12 +336,13 @@ class TestQNode:
         assert res.shape == (2,)
         assert isinstance(res, np.ndarray)
 
-        assert not qml.jacobian(circuit)(a, b)
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
+            assert not qml.jacobian(circuit)(a, b)
 
         def cost(a, b):
             return np.sum(circuit(a, b))
 
-        with pytest.warns(UserWarning, match="Output seems independent of input"):
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
             grad = qml.grad(cost)(a, b)
 
         assert grad == tuple()
@@ -1230,7 +1231,7 @@ def test_adjoint_reuse_device_state(mocker):
 
     spy = mocker.spy(dev, "adjoint_jacobian")
 
-    grad = qml.grad(circ)(1.0)
+    qml.grad(circ, argnum=0)(1.0)
     assert circ.device.num_executions == 1
 
     spy.assert_called_with(mocker.ANY, use_device_state=True)
diff --git a/tests/interfaces/test_qnode_autograd.py b/tests/interfaces/test_qnode_autograd.py
index 85d5e44e0..9e0b8062c 100644
--- a/tests/interfaces/test_qnode_autograd.py
+++ b/tests/interfaces/test_qnode_autograd.py
@@ -367,12 +367,13 @@ class TestQNode:
         assert res.shape == (2,)
         assert isinstance(res, np.ndarray)
 
-        assert not qml.jacobian(circuit)(a, b)
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
+            assert not qml.jacobian(circuit)(a, b)
 
         def cost(a, b):
             return np.sum(circuit(a, b))
 
-        with pytest.warns(UserWarning, match="Output seems independent of input"):
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
             grad = qml.grad(cost)(a, b)
 
         assert grad == tuple()
diff --git a/tests/interfaces/test_tape_autograd.py b/tests/interfaces/test_tape_autograd.py
index 7f260c48a..0dff33594 100644
--- a/tests/interfaces/test_tape_autograd.py
+++ b/tests/interfaces/test_tape_autograd.py
@@ -226,13 +226,14 @@ class TestAutogradQuantumTape:
         res = cost(a, b, device=dev)
         assert res.shape == (2,)
 
-        res = qml.jacobian(cost)(a, b, device=dev)
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
+            res = qml.jacobian(cost)(a, b, device=dev)
         assert not res
 
         def loss(a, b):
             return np.sum(cost(a, b, device=dev))
 
-        with pytest.warns(UserWarning, match="Output seems independent"):
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
             res = qml.grad(loss)(a, b)
 
         assert not res
@@ -542,13 +543,14 @@ class TestAutogradPassthru:
         assert res.shape == (2,)
         spy.assert_not_called()
 
-        res = qml.jacobian(cost)(a, b, device=dev)
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
+            res = qml.jacobian(cost)(a, b, device=dev)
         assert not res
 
         def loss(a, b):
             return np.sum(cost(a, b, device=dev))
 
-        with pytest.warns(UserWarning, match="Output seems independent"):
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
             res = qml.grad(loss)(a, b)
 
         assert not res
diff --git a/tests/math/test_functions.py b/tests/math/test_functions.py
index c582f7e26..dbbc9a442 100644
--- a/tests/math/test_functions.py
+++ b/tests/math/test_functions.py
@@ -1063,10 +1063,8 @@ class TestRequiresGrad:
 
         t.requires_grad = False
         s.requires_grad = False
-
-        with pytest.warns(UserWarning, match="Output seems independent of input"):
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
             qml.grad(cost_fn)(t, s)
-
         assert res == [False, False]
 
     def test_torch(self):
diff --git a/tests/ops/qubit/test_hamiltonian.py b/tests/ops/qubit/test_hamiltonian.py
index efbe8543b..44ac191ca 100644
--- a/tests/ops/qubit/test_hamiltonian.py
+++ b/tests/ops/qubit/test_hamiltonian.py
@@ -1286,8 +1286,8 @@ class TestHamiltonianDifferentiation:
     def test_nontrainable_coeffs_paramshift(self):
         """Test the parameter-shift method if the coefficients are explicitly set non-trainable
         by not passing them to the qnode."""
-        coeffs = np.array([-0.05, 0.17])
-        param = np.array(1.7)
+        coeffs = pnp.array([-0.05, 0.17], requires_grad=False)
+        param = pnp.array(1.7, requires_grad=True)
 
         # differentiating a circuit with measurement expval(H)
         @qml.qnode(dev, diff_method="parameter-shift")
diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py
index 392697939..ebcbdc7f2 100644
--- a/tests/ops/qubit/test_non_parametric_ops.py
+++ b/tests/ops/qubit/test_non_parametric_ops.py
@@ -915,3 +915,72 @@ def test_control_wires(op, control_wires):
     """Test ``control_wires`` attribute for non-parametrized operations."""
 
     assert op.control_wires == control_wires
+
+
+all_ops = [
+    qml.Identity(0),
+    qml.Hadamard(0),
+    qml.PauliX(0),
+    qml.PauliY(0),
+    qml.PauliZ(0),
+    qml.S(wires=0),
+    qml.T(wires=0),
+    qml.SX(wires=0),
+    qml.CNOT(wires=(0, 1)),
+    qml.CZ(wires=(0, 1)),
+    qml.CY(wires=(0, 1)),
+    qml.SWAP(wires=(0, 1)),
+    qml.ISWAP(wires=(0, 1)),
+    qml.SISWAP(wires=(0, 1)),
+    qml.SQISW(wires=(0, 1)),
+    qml.CSWAP(wires=(0, 1, 2)),
+    qml.Toffoli(wires=(0, 1, 2)),
+    qml.MultiControlledX(control_wires=(0, 1, 2), wires=(3)),
+    qml.Barrier(0),
+    qml.WireCut(wires=0),
+]
+
+involution_ops = [  # ops who are their own inverses
+    qml.Identity,
+    qml.Hadamard,
+    qml.PauliX,
+    qml.PauliY,
+    qml.PauliZ,
+    qml.CNOT,
+    qml.CZ,
+    qml.CY,
+    qml.SWAP,
+    qml.CSWAP,
+    qml.Toffoli,
+    qml.MultiControlledX,
+    qml.Barrier,
+    qml.WireCut,
+]
+
+
+@pytest.mark.parametrize("op", all_ops)
+def test_adjoint_method(op, tol):
+    for num_adjoint_calls in range(1, 4):
+
+        adj_op = copy.copy(op)
+        for i in range(num_adjoint_calls):
+            adj_op = adj_op.adjoint()
+
+        if (type(op) in involution_ops) or (num_adjoint_calls % 2 == 0):
+            expected_adj_op = copy.copy(op)
+
+        else:
+            expected_adj_op = copy.copy(op)
+            expected_adj_op.inverse = not expected_adj_op.inverse
+
+        assert adj_op.name == expected_adj_op.name
+        assert (
+            adj_op.label() == expected_adj_op.label()
+        )  # check that the name and labels are the same
+
+        try:
+            np.testing.assert_allclose(
+                adj_op.matrix, expected_adj_op.matrix, atol=tol
+            )  # compare matrix if its defined
+        except NotImplementedError:
+            pass
diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py
index 7696946a5..d3e8081c3 100644
--- a/tests/ops/qubit/test_parametric_ops.py
+++ b/tests/ops/qubit/test_parametric_ops.py
@@ -553,7 +553,7 @@ class TestGrad:
 
     for phi in phis:
         for device, method in device_methods:
-            configuration.append([device, method, phi])
+            configuration.append([device, method, npp.array(phi, requires_grad=True)])
 
     @pytest.mark.parametrize("dev_name,diff_method,phi", configuration)
     def test_isingxx_autograd_grad(self, tol, dev_name, diff_method, phi):
@@ -1171,7 +1171,7 @@ class TestPauliRot:
             0.5 * (circuit(angle + np.pi / 2) - circuit(angle - np.pi / 2)), abs=tol
         )
 
-    @pytest.mark.parametrize("angle", np.linspace(0, 2 * np.pi, 7))
+    @pytest.mark.parametrize("angle", npp.linspace(0, 2 * np.pi, 7, requires_grad=True))
     def test_decomposition_integration(self, angle, tol):
         """Test that the decompositon of PauliRot yields the same results."""
 
@@ -1376,7 +1376,7 @@ class TestMultiRZ:
             0.5 * (circuit(angle + np.pi / 2) - circuit(angle - np.pi / 2)), abs=tol
         )
 
-    @pytest.mark.parametrize("angle", np.linspace(0, 2 * np.pi, 7))
+    @pytest.mark.parametrize("angle", npp.linspace(0, 2 * np.pi, 7, requires_grad=True))
     def test_decomposition_integration(self, angle, tol):
         """Test that the decompositon of MultiRZ yields the same results."""
 
diff --git a/tests/ops/qubit/test_qchem_ops.py b/tests/ops/qubit/test_qchem_ops.py
index 86ea7af7b..68a39a3fa 100644
--- a/tests/ops/qubit/test_qchem_ops.py
+++ b/tests/ops/qubit/test_qchem_ops.py
@@ -19,6 +19,7 @@ import numpy as np
 from scipy.linalg import expm
 
 import pennylane as qml
+from pennylane import numpy as pnp
 
 from gate_data import (
     X,
@@ -210,9 +211,9 @@ class TestSingleExcitation:
     @pytest.mark.parametrize(
         ("excitation", "phi"),
         [
-            (qml.SingleExcitation, -0.1),
-            (qml.SingleExcitationPlus, 0.2),
-            (qml.SingleExcitationMinus, np.pi / 4),
+            (qml.SingleExcitation, pnp.array(-0.1, requires_grad=True)),
+            (qml.SingleExcitationPlus, pnp.array(0.2, requires_grad=True)),
+            (qml.SingleExcitationMinus, pnp.array(np.pi / 4, requires_grad=True)),
         ],
     )
     def test_autograd_grad(self, diff_method, excitation, phi):
@@ -480,9 +481,9 @@ class TestDoubleExcitation:
     @pytest.mark.parametrize(
         ("excitation", "phi"),
         [
-            (qml.DoubleExcitation, -0.1),
-            (qml.DoubleExcitationPlus, 0.2),
-            (qml.DoubleExcitationMinus, np.pi / 4),
+            (qml.DoubleExcitation, pnp.array(-0.1, requires_grad=True)),
+            (qml.DoubleExcitationPlus, pnp.array(0.2, requires_grad=True)),
+            (qml.DoubleExcitationMinus, pnp.array(np.pi / 4, requires_grad=True)),
         ],
     )
     def test_autograd_grad(self, excitation, phi):
@@ -808,10 +809,10 @@ class TestOrbitalRotation:
 
     @pytest.mark.parametrize("diff_method", ["parameter-shift", "backprop"])
     @pytest.mark.parametrize(
-        ("phi"),
+        "phi",
         [
-            (-0.1),
-            (0.1),
+            pnp.array(-0.1, requires_grad=True),
+            pnp.array(0.1, requires_grad=True),
         ],
     )
     def test_autograd_grad(self, phi, diff_method):
diff --git a/tests/ops/qubit/test_sparse.py b/tests/ops/qubit/test_sparse.py
index 5592aaa0a..8506876ef 100644
--- a/tests/ops/qubit/test_sparse.py
+++ b/tests/ops/qubit/test_sparse.py
@@ -113,7 +113,7 @@ class TestSparse:
             match="SparseHamiltonian observable must be"
             " used with the parameter-shift differentiation method",
         ):
-            qml.grad(circuit)([0.5])
+            qml.grad(circuit, argnum=0)([0.5])
 
     @pytest.mark.parametrize("qubits, hamiltonian, expected_output", [(4, H_hydrogen, -0.18092703)])
     def test_sparse_gradient(self, qubits, hamiltonian, expected_output, tol):
@@ -129,7 +129,7 @@ class TestSparse:
             qml.DoubleExcitation(param, wires=[0, 1, 2, 3])
             return qml.expval(qml.SparseHamiltonian(hamiltonian, wires=range(qubits)))
 
-        assert np.allclose(qml.grad(circuit)(0.0), expected_output, atol=tol, rtol=0)
+        assert np.allclose(qml.grad(circuit, argnum=0)(0.0), expected_output, atol=tol, rtol=0)
 
     @pytest.mark.parametrize(
         "qubits, operations, hamiltonian, expected_output",
diff --git a/tests/ops/test_channel_ops.py b/tests/ops/test_channel_ops.py
index 53c43d089..cd1e5456b 100644
--- a/tests/ops/test_channel_ops.py
+++ b/tests/ops/test_channel_ops.py
@@ -614,7 +614,7 @@ class TestThermalRelaxationError:
         grad recipes are independent of channel parameter"""
 
         dev = qml.device("default.mixed", wires=1)
-        pe = 0.0
+        pe = pnp.array(0.0, requires_grad=True)
 
         @qml.qnode(dev)
         def circuit(pe):
diff --git a/tests/tape/test_qnode_old.py b/tests/tape/test_qnode_old.py
index 70cae66f0..385684718 100644
--- a/tests/tape/test_qnode_old.py
+++ b/tests/tape/test_qnode_old.py
@@ -558,17 +558,23 @@ class TestValidation:
 
         grad = qml.grad(qn)
 
-        # Raise error in cases 1 and 5, as non-trainable parameters do not trigger differentiation
+        # Raise error in cases 1 and 5, as non-trainable parameters do not trigger differentiation.
+        # Raise warning in cases 1-4 as there a no trainable parameters.
         # Case 1: No input
         # Case 2: int input
         # Case 3: float input
         # Case 4: numpy input
         # Case 5: differentiable tensor input
-        if par is None or hasattr(par, "requires_grad"):
+        if par is None:
+            with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
+                with pytest.raises(TypeError) as exp:
+                    grad()
+        elif hasattr(par, "requires_grad"):
             with pytest.raises(TypeError) as exp:
-                grad() if par is None else grad(par)
+                grad(par)
         else:
-            grad(par)
+            with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
+                grad(par)
 
     def test_diff_method_none_no_qnode_param(self):
         """Test if diff_method=None works as intended."""
@@ -584,7 +590,8 @@ class TestValidation:
         grad = qml.grad(qn)
 
         # No differentiation required. No error raised.
-        grad()
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
+            grad()
 
     def test_unrecognized_keyword_arguments_validation(self):
         """Tests that a UserWarning is raised when unrecognized keyword arguments are provided."""
@@ -1228,10 +1235,10 @@ class TestMutability:
         np.testing.assert_allclose(res, np.cos(x), atol=tol, rtol=0)
 
         # test differentiability
-        grad = qml.grad(circuit)(0.5)
+        grad = qml.grad(circuit, argnum=0)(0.5)
         np.testing.assert_allclose(grad, 0, atol=tol, rtol=0)
 
-        grad = qml.grad(circuit)(-0.5)
+        grad = qml.grad(circuit, argnum=0)(-0.5)
         np.testing.assert_allclose(grad, -np.sin(-0.5), atol=tol, rtol=0)
 
     def test_immutable(self, mocker, tol):
@@ -1267,7 +1274,8 @@ class TestMutability:
         np.testing.assert_allclose(res, 1, atol=tol, rtol=0)
 
         # test differentiability. The circuit will assume an RZ gate
-        grad = qml.grad(circuit)(-0.5)
+        with pytest.warns(UserWarning, match="Output seems independent of input"):
+            grad = qml.grad(circuit, argnum=0)(-0.5)
         np.testing.assert_allclose(grad, 0, atol=tol, rtol=0)
 
 
@@ -1455,9 +1463,9 @@ def test_finitediff_float32(tol):
 
     shape = qml.templates.StronglyEntanglingLayers.shape(n_wires=n_wires, n_layers=n_layers)
 
-    rng = np.random.default_rng(seed=42)
-    params = rng.random(shape)
-    params_f32 = np.array(params, dtype=np.float32)
+    rng = pnp.random.default_rng(seed=42)
+    params = rng.random(shape, requires_grad=True)
+    params_f32 = pnp.array(params, dtype=np.float32, requires_grad=True)
 
     dev = qml.device("default.qubit", n_wires)
 
diff --git a/tests/templates/test_state_preparations/test_mottonen_state_prep.py b/tests/templates/test_state_preparations/test_mottonen_state_prep.py
index a2f437213..016753469 100644
--- a/tests/templates/test_state_preparations/test_mottonen_state_prep.py
+++ b/tests/templates/test_state_preparations/test_mottonen_state_prep.py
@@ -17,6 +17,7 @@ Unit tests for the ArbitraryStatePreparation template.
 import pytest
 import numpy as np
 import pennylane as qml
+from pennylane import numpy as pnp
 
 torch = pytest.importorskip("torch", minversion="1.3")
 from pennylane.templates.state_preparations.mottonen import gray_code, _get_alpha_y
@@ -339,7 +340,11 @@ class TestGradient:
     # Make the template fully differentiable and test it.
 
     @pytest.mark.parametrize(
-        "state_vector", [np.array([0.70710678, 0.70710678]), np.array([0.70710678, 0.70710678j])]
+        "state_vector",
+        [
+            pnp.array([0.70710678, 0.70710678], requires_grad=True),
+            pnp.array([0.70710678, 0.70710678j], requires_grad=True),
+        ],
     )
     def test_gradient_evaluated(self, state_vector):
         """Test that the gradient is successfully calculated for a simple example. This test only
diff --git a/tests/templates/test_subroutines/test_kupccgsd.py b/tests/templates/test_subroutines/test_kupccgsd.py
index f3632802b..05db60e50 100644
--- a/tests/templates/test_subroutines/test_kupccgsd.py
+++ b/tests/templates/test_subroutines/test_kupccgsd.py
@@ -522,7 +522,7 @@ class TestInterfaces:
     def test_autograd(self, tol):
         """Test the autograd interface."""
 
-        weights = qml.math.array(np.random.random(size=(1, 6)))
+        weights = qml.numpy.random.random(size=(1, 6), requires_grad=True)
 
         dev = qml.device("default.qubit", wires=4)
 
diff --git a/tests/test_operation.py b/tests/test_operation.py
index 54698d50c..e94e11888 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -70,7 +70,7 @@ class TestOperatorConstruction:
         """Test that an exception is raised if called with wrong number of parameters"""
 
         class DummyOp(qml.operation.Operator):
-            r"""Dummy custom operator"""
+            r"""Dummy custom operator that declares num_params as a instance property"""
             num_wires = 1
             grad_method = "A"
 
@@ -81,6 +81,31 @@ class TestOperatorConstruction:
         with pytest.raises(ValueError, match="wrong number of parameters"):
             DummyOp(0.5, 0.6, wires=0)
 
+        op = DummyOp(0.5, wires=0)
+        assert op.num_params == 1
+
+        class DummyOp2(qml.operation.Operator):
+            r"""Dummy custom operator that declared num_params as a class property"""
+            num_params = 4
+            num_wires = 1
+            grad_method = "A"
+
+        with pytest.raises(ValueError, match="wrong number of parameters"):
+            DummyOp2(0.5, 0.6, wires=0)
+
+        op2 = DummyOp2(0.5, 0.3, 0.1, 0.2, wires=0)
+        assert op2.num_params == 4
+        assert DummyOp2.num_params == 4
+
+        class DummyOp3(qml.operation.Operator):
+            r"""Dummy custom operator that does not declare num_params at all"""
+            num_wires = 1
+            grad_method = "A"
+
+        op3 = DummyOp3(0.5, 0.6, wires=0)
+
+        assert op3.num_params == 2
+
     def test_name_setter(self):
         """Tests that we can set the name of an operator"""
 
diff --git a/tests/test_qnode.py b/tests/test_qnode.py
index aa18728e9..36340e0b6 100644
--- a/tests/test_qnode.py
+++ b/tests/test_qnode.py
@@ -348,7 +348,7 @@ class TestValidation:
             match="SparseHamiltonian observable must be"
             " used with the parameter-shift differentiation method",
         ):
-            qml.grad(circuit)([0.5])
+            qml.grad(circuit, argnum=0)([0.5])
 
     def test_qnode_print(self):
         """Test that printing a QNode object yields the right information."""
@@ -382,7 +382,7 @@ class TestValidation:
         # 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"):
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
             grad = qml.grad(circuit)(0.5)
 
         assert np.allclose(grad, 0)
@@ -711,6 +711,56 @@ class TestIntegration:
 
         assert dev.num_executions == 6
 
+    def test_num_exec_caching_device_swap(self):
+        """Tests that if we swapped the original device (e.g., when
+        diff_method='backprop') then the number of executions recorded is
+        correct."""
+        dev = qml.device("default.qubit", wires=2)
+
+        cache = {}
+
+        @qml.qnode(dev, diff_method="backprop", cache=cache)
+        def circuit():
+            qml.RY(0.345, wires=0)
+            return qml.expval(qml.PauliZ(0))
+
+        for _ in range(15):
+            circuit()
+
+        # Although we've evaluated the QNode more than once, due to caching,
+        # there was one device execution recorded
+        assert dev.num_executions == 1
+        assert cache != {}
+
+    def test_num_exec_caching_device_swap_two_exec(self):
+        """Tests that if we swapped the original device (e.g., when
+        diff_method='backprop') then the number of executions recorded is
+        correct even with multiple QNode evaluations."""
+        dev = qml.device("default.qubit", wires=2)
+
+        cache = {}
+
+        @qml.qnode(dev, diff_method="backprop", cache=cache)
+        def circuit():
+            qml.RY(0.345, wires=0)
+            return qml.expval(qml.PauliZ(0))
+
+        for _ in range(15):
+            circuit()
+
+        @qml.qnode(dev, diff_method="backprop", cache=cache)
+        def circuit():
+            qml.RZ(0.345, wires=0)
+            return qml.expval(qml.PauliZ(0))
+
+        for _ in range(15):
+            circuit()
+
+        # Although we've evaluated the QNode several times, due to caching,
+        # there were two device executions recorded
+        assert dev.num_executions == 2
+        assert cache != {}
+
     @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
@@ -1117,7 +1167,7 @@ class TestTapeExpansion:
         """Test that the device expansion strategy performs the device
         decomposition at construction time, and not at execution time"""
         dev = qml.device("default.qubit", wires=2)
-        x = np.array(0.5)
+        x = pnp.array(0.5, requires_grad=True)
 
         @qnode(dev, diff_method="parameter-shift", expansion_strategy="device")
         def circuit(x):
diff --git a/tests/test_quantum_gradients.py b/tests/test_quantum_gradients.py
index cd98d336b..5fe73aff9 100644
--- a/tests/test_quantum_gradients.py
+++ b/tests/test_quantum_gradients.py
@@ -159,7 +159,7 @@ class TestCVGradient:
         between the finite difference and analytic methods."""
 
         tol = 1e-5
-        par = 0.4
+        par = anp.array(0.4, requires_grad=True)
 
         def circuit(x):
             qml.Displacement(0.5, 0, wires=0)
@@ -218,7 +218,7 @@ class TestCVGradient:
 
     def test_cv_gradients_repeated_gate_parameters(self, gaussian_dev, tol):
         "Tests that repeated use of a free parameter in a multi-parameter gate yield correct gradients."
-        par = [0.2, 0.3]
+        par = anp.array([0.2, 0.3], requires_grad=True)
 
         def qf(x, y):
             qml.Displacement(x, 0, wires=[0])
@@ -261,7 +261,7 @@ class TestCVGradient:
 
     def test_cv_gradient_fanout(self, gaussian_dev, tol):
         "Tests that qnodes can compute the correct gradient when the same parameter is used in multiple gates."
-        par = [0.5, 1.3]
+        par = anp.array([0.5, 1.3], requires_grad=True)
 
         def circuit(x, y):
             qml.Displacement(x, 0, wires=[0])
@@ -280,14 +280,16 @@ class TestCVGradient:
         assert qml.math.allclose(grad_A2, grad_F, atol=tol, rtol=0)
 
     def test_CVOperation_with_heisenberg_and_no_params(self, gaussian_dev, tol):
-        """An integration test for InterferometerUnitary, a gate that supports analytic differentiation
-        if succeeding the gate to be differentiated, but cannot be differentiated
-        itself.
+        """An integration test for InterferometerUnitary, a gate that supports analytic
+        differentiation if succeeding the gate to be differentiated, but cannot be
+        differentiated itself.
 
         This ensures that, assuming the _heisenberg_rep is defined, the quantum
         gradient analytic method can still be used, and returns the correct result.
         """
 
+        x = anp.array(0.5, requires_grad=True)
+
         U = np.array(
             [
                 [0.51310276 + 0.81702166j, 0.13649626 + 0.22487759j],
@@ -301,10 +303,10 @@ class TestCVGradient:
             return qml.expval(qml.X(0))
 
         qnode = qml.QNode(circuit, gaussian_dev)
-        qnode(0.5)
-        grad_F = qml.gradients.finite_diff(qnode)(0.5)
-        grad_A = qml.gradients.param_shift_cv(qnode, dev=gaussian_dev)(0.5)
-        grad_A2 = qml.gradients.param_shift_cv(qnode, dev=gaussian_dev, force_order2=True)(0.5)
+        qnode(x)
+        grad_F = qml.gradients.finite_diff(qnode)(x)
+        grad_A = qml.gradients.param_shift_cv(qnode, dev=gaussian_dev)(x)
+        grad_A2 = qml.gradients.param_shift_cv(qnode, dev=gaussian_dev, force_order2=True)(x)
 
         # the different methods agree
         assert grad_A == pytest.approx(grad_F, abs=tol)
@@ -445,7 +447,7 @@ class TestQubitGradient:
             return qml.expval(qml.PauliZ(0))
 
         qnode = qml.QNode(circuit, qubit_device_2_wires, diff_method="parameter-shift")
-        params = np.array([0.1, -1.6, np.pi / 5])
+        params = anp.array([0.1, -1.6, np.pi / 5], requires_grad=True)
 
         # manual gradients
         grad_fd1 = qml.gradients.finite_diff(qnode, approx_order=1)(*params)
@@ -479,7 +481,7 @@ class TestQubitGradient:
             classifier_circuit, qubit_device_2_wires, diff_method="parameter-shift"
         )
 
-        param = -0.1259
+        param = anp.array(-0.1259, requires_grad=True)
         in_data = qml.numpy.array([-0.1, -0.88, np.exp(0.5)], requires_grad=False)
         out_data = np.array([1.5, np.pi / 3, 0.0])
 
@@ -676,7 +678,7 @@ class TestFourTermParameterShifts:
     def test_controlled_rotation_gradient(self, G, tol):
         """Test gradient of controlled RX gate"""
         dev = qml.device("default.qubit", wires=2)
-        b = 0.123
+        b = anp.array(0.123, requires_grad=True)
 
         @qml.qnode(dev, diff_method="parameter-shift")
         def circuit(b):
diff --git a/tests/test_qubit_device.py b/tests/test_qubit_device.py
index 74e91392f..5ab5ac9f4 100644
--- a/tests/test_qubit_device.py
+++ b/tests/test_qubit_device.py
@@ -960,7 +960,7 @@ class TestShotList:
         assert circuit.device.shots == total_shots
 
         # test gradient works
-        res = qml.jacobian(circuit)(0.5, 0.1)
+        res = qml.jacobian(circuit, argnum=[0, 1])(0.5, 0.1)
 
     shot_data = [
         [[1, 2, 3, 10], [(1, 1), (2, 1), (3, 1), (10, 1)], (4, 2, 2), 16],
@@ -993,7 +993,7 @@ class TestShotList:
         assert circuit.device.shots == total_shots
 
         # test gradient works
-        res = qml.jacobian(circuit)(0.5, 0.1)
+        res = qml.jacobian(circuit, argnum=[0, 1])(0.5, 0.1)
 
     shot_data = [
         [[1, 2, 3, 10], [(1, 1), (2, 1), (3, 1), (10, 1)], [(), (2,), (3,), (10,)], 16],
diff --git a/tests/test_vqe.py b/tests/test_vqe.py
index fde6b08ee..5370eb108 100644
--- a/tests/test_vqe.py
+++ b/tests/test_vqe.py
@@ -526,9 +526,10 @@ class TestVQE:
 
         np.random.seed(1967)
         shape = qml.templates.StronglyEntanglingLayers.shape(n_layers=2, n_wires=4)
-        w = np.random.random(shape)
+        w = pnp.random.random(shape, requires_grad=True)
 
-        dc = qml.grad(cost)(w)
+        with pytest.warns(UserWarning, match="Output seems independent of input"):
+            dc = qml.grad(cost)(w)
         assert np.allclose(dc, 0)
 
     @pytest.mark.slow
diff --git a/tests/transforms/test_batch_params.py b/tests/transforms/test_batch_params.py
index 9f6a86613..0b5d398f1 100644
--- a/tests/transforms/test_batch_params.py
+++ b/tests/transforms/test_batch_params.py
@@ -378,3 +378,18 @@ def test_initial_unbatched_parameter():
 
     with pytest.raises(ValueError, match="Parameter 0.2 does not contain a batch"):
         circuit(x, y)
+
+
+def test_no_batch_param_error():
+    """Test that the right error is thrown when there is nothing to batch"""
+    dev = qml.device("default.qubit", wires=1)
+
+    @qml.batch_params
+    @qml.qnode(dev)
+    def circuit(x):
+        qml.RY(x, wires=0)
+        return qml.expval(qml.PauliZ(0))
+
+    x = [0.2, 0.6, 3]
+    with pytest.raises(ValueError, match="There are no operations to transform"):
+        circuit(x)
diff --git a/tests/transforms/test_batch_transform.py b/tests/transforms/test_batch_transform.py
index a337e8315..586d426d8 100644
--- a/tests/transforms/test_batch_transform.py
+++ b/tests/transforms/test_batch_transform.py
@@ -135,7 +135,7 @@ class TestBatchTransform:
         assert isinstance(res, float)
         assert not np.allclose(res, 0)
 
-        with pytest.warns(UserWarning, match="Output seems independent of input"):
+        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
             qml.grad(circuit)(0.5)
 
     def test_use_qnode_execution_options(self, mocker):
@@ -555,8 +555,10 @@ class TestBatchTransformGradients:
         dev = qml.device("default.qubit", wires=2)
         qnode = qml.QNode(self.circuit, dev, interface="tf", diff_method=diff_method)
 
-        weights = tf.Variable([0.1, 0.2], dtype=tf.float64)
-        x = tf.Variable(0.543, dtype=tf.float64)
+        weights_np = np.array([0.1, 0.2], requires_grad=True)
+        x_np = np.array(0.543, requires_grad=True)
+        weights = tf.Variable(weights_np, dtype=tf.float64)
+        x = tf.Variable(x_np, dtype=tf.float64)
 
         with tf.GradientTape() as tape:
             res = self.my_transform(qnode, weights)(x)
@@ -564,7 +566,8 @@ class TestBatchTransformGradients:
         assert np.allclose(res, self.expval(x, weights))
 
         grad = tape.gradient(res, [x, weights])
-        expected = qml.grad(self.expval)(x.numpy(), weights.numpy())
+        expected = qml.grad(self.expval)(x_np, weights_np)
+        assert len(grad) == len(expected)
         assert all(np.allclose(g, e) for g, e in zip(grad, expected))
 
     def test_differentiable_torch(self, diff_method):
@@ -598,14 +601,17 @@ class TestBatchTransformGradients:
         def cost(x, weights):
             return self.my_transform(qnode, weights, max_diff=1)(x)
 
-        weights = jax.numpy.array([0.1, 0.2])
-        x = jax.numpy.array(0.543)
+        weights_np = np.array([0.1, 0.2], requires_grad=True)
+        x_np = np.array(0.543, requires_grad=True)
+        weights = jax.numpy.array(weights_np)
+        x = jax.numpy.array(x_np)
 
         res = cost(x, weights)
         assert np.allclose(res, self.expval(x, weights))
 
         grad = jax.grad(cost, argnums=[0, 1])(x, weights)
-        expected = qml.grad(self.expval)(np.array(x), np.array(weights))
+        expected = qml.grad(self.expval)(x_np, weights_np)
+        assert len(grad) == len(expected)
         assert all(np.allclose(g, e) for g, e in zip(grad, expected))
 
     def test_batch_transforms_qnode(self, diff_method, mocker):
diff --git a/tests/transforms/test_hamiltonian_expand.py b/tests/transforms/test_hamiltonian_expand.py
index 1585b8f8e..a849b833e 100644
--- a/tests/transforms/test_hamiltonian_expand.py
+++ b/tests/transforms/test_hamiltonian_expand.py
@@ -16,6 +16,7 @@ import pytest
 import numpy as np
 import pennylane as qml
 import pennylane.tape
+from pennylane import numpy as pnp
 
 """Defines the device used for all tests"""
 
@@ -145,17 +146,7 @@ class TestHamiltonianExpval:
             [-0.2, 0.5, 1], [qml.PauliX(1), qml.PauliZ(1) @ qml.PauliY(2), qml.PauliZ(0)]
         )
 
-        var = [
-            np.array(0.1),
-            np.array(0.67),
-            np.array(0.3),
-            np.array(0.4),
-            np.array(-0.5),
-            np.array(0.7),
-            np.array(-0.2),
-            np.array(0.5),
-            np.array(1.0),
-        ]
+        var = pnp.array([0.1, 0.67, 0.3, 0.4, -0.5, 0.7, -0.2, 0.5, 1.0], requires_grad=True)
         output = 0.42294409781940356
         output2 = [
             9.68883500e-02,
@@ -187,7 +178,9 @@ class TestHamiltonianExpval:
             return fn(res)
 
         assert np.isclose(cost(var), output)
+
         grad = qml.grad(cost)(var)
+        assert len(grad) == len(output2)
         for g, o in zip(grad, output2):
             assert np.allclose(g, o, atol=tol)
 
diff --git a/tests/transforms/test_insert_ops.py b/tests/transforms/test_insert_ops.py
index ee18a67cb..d2cd247c5 100644
--- a/tests/transforms/test_insert_ops.py
+++ b/tests/transforms/test_insert_ops.py
@@ -46,6 +46,15 @@ class TestInsert:
         qml.RX(0.6, wires=1)
         qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
 
+    with QuantumTape() as custom_tape:
+        qml.RX(0.9, wires=0)
+        qml.RY(0.4, wires=1)
+        qml.CNOT(wires=[0, 1])
+        qml.RY(0.5, wires=0)
+        qml.PauliZ(wires=1)
+        qml.Identity(wires=2)
+        qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
+
     def test_multiwire_op(self):
         """Tests if a ValueError is raised when multiqubit operations are requested"""
         with pytest.raises(ValueError, match="Only single-qubit operations can be inserted into"):
@@ -140,18 +149,30 @@ class TestInsert:
         assert tape.observables[0].wires.tolist() == [0, 1]
         assert tape.measurements[0].return_type is Expectation
 
-    def test_operation_as_position(self):
+    op_lst = [qml.RX, qml.PauliZ, qml.Identity]
+
+    @pytest.mark.parametrize("op", op_lst)
+    def test_operation_as_position(self, op):
         """Test if expected tape is returned when an operation is passed in position"""
-        tape = insert(qml.PhaseDamping, 0.4, position=qml.RX, before=True)(self.tape)
+        tape = insert(qml.PhaseDamping, 0.4, position=op, before=True)(self.custom_tape)
 
         with QuantumTape() as tape_exp:
-            qml.PhaseDamping(0.4, wires=0)
+            if op == qml.RX:
+                qml.PhaseDamping(0.4, wires=0)
             qml.RX(0.9, wires=0)
+
             qml.RY(0.4, wires=1)
             qml.CNOT(wires=[0, 1])
             qml.RY(0.5, wires=0)
-            qml.PhaseDamping(0.4, wires=1)
-            qml.RX(0.6, wires=1)
+
+            if op == qml.PauliZ:
+                qml.PhaseDamping(0.4, wires=1)
+            qml.PauliZ(wires=1)
+
+            if op == qml.Identity:
+                qml.PhaseDamping(0.4, wires=2)
+            qml.Identity(wires=2)
+
             qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
 
         assert all(o1.name == o2.name for o1, o2 in zip(tape.operations, tape_exp.operations))
diff --git a/tests/transforms/test_metric_tensor.py b/tests/transforms/test_metric_tensor.py
index 3dd50614b..7d6e685c5 100644
--- a/tests/transforms/test_metric_tensor.py
+++ b/tests/transforms/test_metric_tensor.py
@@ -600,6 +600,47 @@ class TestMetricTensor:
         ]
         assert [[type(op) for op in tape.operations] for tape in tapes] == expected_ops
 
+    @pytest.mark.parametrize("interface", ["autograd", "jax", "torch", "tensorflow"])
+    def test_no_trainable_params_qnode(self, interface):
+        """Test that the correct ouput and warning is generated in the absence of any trainable
+        parameters"""
+        if interface != "autograd":
+            pytest.importorskip(interface)
+        dev = qml.device("default.qubit", wires=3)
+
+        @qml.qnode(dev, interface=interface)
+        def circuit(weights):
+            qml.RX(weights[0], wires=0)
+            qml.RY(weights[1], wires=0)
+            return qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
+
+        weights = [0.1, 0.2]
+        with pytest.warns(UserWarning, match="tensor of a QNode with no trainable parameters"):
+            res = qml.metric_tensor(circuit)(weights)
+
+        assert res == ()
+
+    def test_no_trainable_params_tape(self):
+        """Test that the correct ouput and warning is generated in the absence of any trainable
+        parameters"""
+        dev = qml.device("default.qubit", wires=3)
+
+        weights = [0.1, 0.2]
+        with qml.tape.QuantumTape() as tape:
+            qml.RX(weights[0], wires=0)
+            qml.RY(weights[1], wires=0)
+            qml.expval(qml.PauliZ(0) @ qml.PauliZ(1))
+
+        # TODO: remove once #2155 is resolved
+        tape.trainable_params = []
+
+        with pytest.warns(UserWarning, match="tensor of a tape with no trainable parameters"):
+            mt_tapes, post_processing = qml.metric_tensor(tape)
+        res = post_processing(qml.execute(mt_tapes, dev, None))
+
+        assert mt_tapes == []
+        assert res == ()
+
 
 fixed_pars = np.array([-0.2, 0.2, 0.5, 0.3, 0.7], requires_grad=False)
 
diff --git a/tests/transforms/test_qfunc_transform.py b/tests/transforms/test_qfunc_transform.py
index 0f9cc9c28..4879d2fae 100644
--- a/tests/transforms/test_qfunc_transform.py
+++ b/tests/transforms/test_qfunc_transform.py
@@ -369,9 +369,12 @@ class TestQFuncTransformGradients:
         dev = qml.device("default.qubit", wires=2)
         qnode = qml.QNode(self.circuit, dev, interface="tf", diff_method=diff_method)
 
-        a = tf.Variable(0.5, dtype=tf.float64)
-        b = tf.Variable([0.1, 0.2], dtype=tf.float64)
-        x = tf.Variable(0.543, dtype=tf.float64)
+        a_np = np.array(0.5, requires_grad=True)
+        b_np = np.array([0.1, 0.2], requires_grad=True)
+        x_np = np.array(0.543, requires_grad=True)
+        a = tf.Variable(a_np, dtype=tf.float64)
+        b = tf.Variable(b_np, dtype=tf.float64)
+        x = tf.Variable(x_np, dtype=tf.float64)
 
         with tf.GradientTape() as tape:
             res = qnode(x, a, b)
@@ -379,7 +382,7 @@ class TestQFuncTransformGradients:
         assert np.allclose(res, self.expval(x, a, b))
 
         grad = tape.gradient(res, [x, a, b])
-        expected = qml.grad(self.expval)(x.numpy(), a.numpy(), b.numpy())
+        expected = qml.grad(self.expval)(x_np, a_np, b_np)
         assert all(np.allclose(g, e) for g, e in zip(grad, expected))
 
     def test_differentiable_qfunc_torch(self, diff_method):