Repository: PennyLaneAI/pennylane
Pull request #2156 (2022-02-02 18:56:34)

Add warnings for the gradient transforms when there are no trainable parameters
Similar to the previous changes in #2148 and #2139, we'd like to provide a warning or useful error message when a user attempts to compute metric tensor even though there are no trainable parameters, such as in the following example:

```py
dev = qml.device("default.qubit", wires=3)

@qml.qnode(dev)
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]
qml.metric_tensor(circuit)(weights)
```

Most or all gradient transforms actually throw an error at the moment when this is attempted, unless the `hybrid` parameter is set to False:

```
ValueError: need at least one array to stack
```

so this change will also resolve this bug.

With the proposed changes, a warning is raised and a default value returned instead, in both the QNode and tape execution case:

```
UserWarning: Attempted to compute the metric tensor of a QNode with no trainable parameters. If this is unintended, please add trainable parameters via the 'requires_grad' attribute.
  warnings.warn(
()
```

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2156?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> :exclamation: No coverage uploaded for pull request base (`v0.21.0-rc0@0d92f82`). [Click here to learn what that means](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#section-missing-base-commit).
> The diff coverage is `n/a`.

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

```diff
@@              Coverage Diff               @@
##             v0.21.0-rc0    #2156   +/-   ##
==============================================
  Coverage               ?   96.36%           
==============================================
  Files                  ?      230           
  Lines                  ?    17736           
  Branches               ?        0           
==============================================
  Hits                   ?    17092           
  Misses                 ?      644           
  Partials               ?        0           
```



------

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

dime10: One scenario (although not a useful one) where this warning might be superfluous is for empty circuits.


josh146: I guess technically still true though! Or perhaps you could edit the warning:

```
Attempted to compute the metric tensor of a tape with no trainable parameters
or no operations ...
```
dime10: [sc-14244]

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 858c80953..4c34dcf01 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -497,6 +497,7 @@
   [(#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)
 
   ```python
   from pennylane import numpy as np
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/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:


Test patch:
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 509831da8..3867b28dc 100644
--- a/tests/gradients/test_param_shift_hessian.py
+++ b/tests/gradients/test_param_shift_hessian.py
@@ -501,25 +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 = []
 
-        with pytest.warns(UserWarning, match="Attempted to differentiate a function with no"):
-            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/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)