Repository: PennyLaneAI/pennylane
Pull request #2299 (2022-03-10 05:32:11)

Minor gradient fixes
Fixes a bug in `qml.gradients.param_shift_hessian` which errors when the diff methods are zero for all parameters. For example in the following scenario:

```py
dev = qml.device('default.qubit', wires=4)
@qml.qnode(dev)
def circuit(params):
    qml.Rot(*params, wires=0)
    return qml.probs([2,3])

params = np.array([0.5, 0.5, 0.5], requires_grad=True)
qml.gradients.param_shift_hessian(circuit)(params)
```
```py
>>> ValueError: need at least one array to stack
```

Both `param_shift` and `param_shift_hessian` assume that the `results` value passed to their post-processing functions (from batched tape execution) has at least one element. Thus, scenarios which do not generate any tapes need to be checked for beforehand:

https://github.com/PennyLaneAI/pennylane/blob/e1b15d27cc5a4a882b552c936580dd903fe3b988/pennylane/gradients/parameter_shift.py#L597-L598

---

In the absence of trainable parameters, some gradient transforms did not produce an empty tuple yet like the rest of our functions. This has been updated.

---

Minor reformatting on the parameter shift hessian transform.

---

Fix missing `requires_grad` attribute on a test, as well as catch expected warning on another test.

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/2299?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.22.0-rc0@628493d`). [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/2299/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/2299?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@              Coverage Diff               @@
##             v0.22.0-rc0    #2299   +/-   ##
==============================================
  Coverage               ?   96.71%           
==============================================
  Files                  ?      242           
  Lines                  ?    19139           
  Branches               ?        0           
==============================================
  Hits                   ?    18510           
  Misses                 ?      629           
  Partials               ?        0           
```



------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2299?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/2299?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [628493d...ed7b8a5](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2299?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/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 0ca72b6cb..2a88890a8 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -411,6 +411,10 @@
   the four-term parameter shift rule. The correct eight-term rule will now be used when
   using the parameter-shift rule.
   [(#2180)](https://github.com/PennyLaneAI/pennylane/pull/2180)
+  
+* Fixes a bug where `qml.gradients.param_shift_hessian` would produce an
+  error whenever all elements of the Hessian are known in advance to be 0.
+  [(#2299)](https://github.com/PennyLaneAI/pennylane/pull/2299)
 
 <h3>Documentation</h3>
 
diff --git a/pennylane/gradients/finite_difference.py b/pennylane/gradients/finite_difference.py
index 0fc974205..79ce33385 100644
--- a/pennylane/gradients/finite_difference.py
+++ b/pennylane/gradients/finite_difference.py
@@ -302,7 +302,7 @@ def finite_diff(
             "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)])
+        return [], lambda _: ()
 
     if validate_params:
         if "grad_method" not in tape._par_info[0]:
diff --git a/pennylane/gradients/param_shift_hessian.py b/pennylane/gradients/param_shift_hessian.py
index 2106aae42..145485f2a 100644
--- a/pennylane/gradients/param_shift_hessian.py
+++ b/pennylane/gradients/param_shift_hessian.py
@@ -170,21 +170,20 @@ def compute_hessian_tapes(tape, diff_methods, f0=None):
             # Compute the elements of the Hessian as the linear combination of
             # results and coefficients, barring optimization cases.
             if j < i:
-                g = hessian[j * h_dim + i]
+                hess = hessian[j * h_dim + i]
             elif s == 0:
-                g = qml.math.zeros(out_dim)
+                hess = qml.math.zeros(out_dim)
             else:
                 res = qml.math.stack(res)
-                hess = qml.math.convert_like(hessian_coeffs[k], res)
-                hess = qml.math.cast(hess, res.dtype)
-                g = qml.math.tensordot(res, hess, [[0], [0]])
+                coeffs = qml.math.cast(qml.math.convert_like(hessian_coeffs[k], res), res.dtype)
+                hess = qml.math.tensordot(res, coeffs, [[0], [0]])
                 if (i, j) in unshifted_coeffs:
-                    g += unshifted_coeffs[(i, j)] * r0
+                    hess = hess + unshifted_coeffs[(i, j)] * r0
 
-            hessian.append(g)
+            hessian.append(hess)
 
         # Reshape the Hessian to have the dimensions of the QNode output on the outside, that is:
-        #         (h_dim, h_dim, out_dim) -> (out_dim, h_dim, h_dim)
+        #     (h_dim*h_dim, out_dim) -> (h_dim, h_dim, out_dim) -> (out_dim, h_dim, h_dim)
         hessian = qml.math.reshape(qml.math.stack(hessian), (h_dim, h_dim) + out_dim)
         reordered_axes = list(range(2, len(out_dim) + 2)) + [0, 1]
         hessian = qml.math.transpose(hessian, axes=reordered_axes)
@@ -344,4 +343,9 @@ def param_shift_hessian(tape, f0=None):
     _gradient_analysis(tape)
     diff_methods = grad_method_validation("analytic", tape)
 
+    if all(g == "0" for g in diff_methods):
+        return [], lambda _: np.zeros(
+            [tape.output_dim, len(tape.trainable_params), len(tape.trainable_params)]
+        )
+
     return compute_hessian_tapes(tape, diff_methods, f0)
diff --git a/pennylane/gradients/parameter_shift.py b/pennylane/gradients/parameter_shift.py
index 4c34bfbbb..71846b438 100644
--- a/pennylane/gradients/parameter_shift.py
+++ b/pennylane/gradients/parameter_shift.py
@@ -611,7 +611,7 @@ def param_shift(
             "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)])
+        return [], lambda _: ()
 
     _gradient_analysis(tape)
     method = "analytic" if fallback_fn is None else "best"
diff --git a/pennylane/gradients/parameter_shift_cv.py b/pennylane/gradients/parameter_shift_cv.py
index 8b48081d1..bee9fb414 100644
--- a/pennylane/gradients/parameter_shift_cv.py
+++ b/pennylane/gradients/parameter_shift_cv.py
@@ -660,7 +660,7 @@ def param_shift_cv(
             "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)])
+        return gradient_tapes, lambda _: ()
 
     def _update(data):
         """Utility function to update the list of gradient tapes,


Test patch:
diff --git a/tests/gradients/test_finite_difference.py b/tests/gradients/test_finite_difference.py
index 05c84f8f7..aedaa5a35 100644
--- a/tests/gradients/test_finite_difference.py
+++ b/tests/gradients/test_finite_difference.py
@@ -219,8 +219,25 @@ class TestFiniteDiff:
         res = post_processing(qml.execute(g_tapes, dev, None))
 
         assert g_tapes == []
-        assert res.size == 0
-        assert np.all(res == np.array([[]]))
+        assert res == ()
+
+    def test_all_zero_diff_methods(self):
+        """Test that the transform works correctly when the diff method for every parameter is
+        identified to be 0, and that no tapes were generated."""
+        dev = qml.device("default.qubit", wires=4)
+
+        @qml.qnode(dev)
+        def circuit(params):
+            qml.Rot(*params, wires=0)
+            return qml.probs([2, 3])
+
+        params = np.array([0.5, 0.5, 0.5], requires_grad=True)
+
+        result = qml.gradients.finite_diff(circuit)(params)
+        assert np.allclose(result, np.zeros((4, 3)), atol=0, rtol=0)
+
+        tapes, _ = qml.gradients.finite_diff(circuit.tape)
+        assert tapes == []
 
     def test_y0(self, mocker):
         """Test that if first order finite differences is used, then
diff --git a/tests/gradients/test_param_shift_hessian.py b/tests/gradients/test_param_shift_hessian.py
index 3867b28dc..6aa488cf7 100644
--- a/tests/gradients/test_param_shift_hessian.py
+++ b/tests/gradients/test_param_shift_hessian.py
@@ -542,6 +542,24 @@ class TestParameterShiftHessian:
         assert h_tapes == []
         assert res == ()
 
+    def test_all_zero_diff_methods(self):
+        """Test that the transform works correctly when the diff method for every parameter is
+        identified to be 0, and that no tapes were generated."""
+        dev = qml.device("default.qubit", wires=4)
+
+        @qml.qnode(dev)
+        def circuit(params):
+            qml.Rot(*params, wires=0)
+            return qml.probs([2, 3])
+
+        params = np.array([0.5, 0.5, 0.5], requires_grad=True)
+
+        result = qml.gradients.param_shift_hessian(circuit)(params)
+        assert np.allclose(result, np.zeros((4, 3, 3)), atol=0, rtol=0)
+
+        tapes, _ = qml.gradients.param_shift_hessian(circuit.tape)
+        assert tapes == []
+
     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 6b70fa171..6b387867e 100644
--- a/tests/gradients/test_parameter_shift.py
+++ b/tests/gradients/test_parameter_shift.py
@@ -236,8 +236,25 @@ class TestParamShift:
         res = post_processing(qml.execute(g_tapes, dev, None))
 
         assert g_tapes == []
-        assert res.size == 0
-        assert np.all(res == np.array([[]]))
+        assert res == ()
+
+    def test_all_zero_diff_methods(self):
+        """Test that the transform works correctly when the diff method for every parameter is
+        identified to be 0, and that no tapes were generated."""
+        dev = qml.device("default.qubit", wires=4)
+
+        @qml.qnode(dev)
+        def circuit(params):
+            qml.Rot(*params, wires=0)
+            return qml.probs([2, 3])
+
+        params = np.array([0.5, 0.5, 0.5], requires_grad=True)
+
+        result = qml.gradients.param_shift(circuit)(params)
+        assert np.allclose(result, np.zeros((4, 3)), atol=0, rtol=0)
+
+        tapes, _ = qml.gradients.param_shift(circuit.tape)
+        assert tapes == []
 
     def test_y0(self):
         """Test that if the gradient recipe has a zero-shift component, then
diff --git a/tests/gradients/test_parameter_shift_cv.py b/tests/gradients/test_parameter_shift_cv.py
index 2f1075d89..bf86136e5 100644
--- a/tests/gradients/test_parameter_shift_cv.py
+++ b/tests/gradients/test_parameter_shift_cv.py
@@ -310,8 +310,25 @@ class TestParameterShiftLogic:
         res = post_processing(qml.execute(g_tapes, dev, None))
 
         assert g_tapes == []
-        assert res.size == 0
-        assert np.all(res == np.array([[]]))
+        assert res == ()
+
+    def test_all_zero_diff_methods(self):
+        """Test that the transform works correctly when the diff method for every parameter is
+        identified to be 0, and that no tapes were generated."""
+        dev = qml.device("default.gaussian", wires=2)
+
+        @qml.qnode(dev)
+        def circuit(params):
+            qml.Rotation(params[0], wires=0)
+            return qml.expval(qml.X(1))
+
+        params = np.array([0.5, 0.5, 0.5], requires_grad=True)
+
+        result = qml.gradients.param_shift_cv(circuit, dev)(params)
+        assert np.allclose(result, np.zeros((2, 3)), atol=0, rtol=0)
+
+        tapes, _ = qml.gradients.param_shift_cv(circuit.tape, dev)
+        assert tapes == []
 
     def test_state_non_differentiable_error(self):
         """Test error raised if attempting to differentiate with
diff --git a/tests/test_quantum_gradients.py b/tests/test_quantum_gradients.py
index 5fe73aff9..6363ac68f 100644
--- a/tests/test_quantum_gradients.py
+++ b/tests/test_quantum_gradients.py
@@ -237,7 +237,7 @@ class TestCVGradient:
 
     def test_cv_gradients_parameters_inside_array(self, gaussian_dev, tol):
         "Tests that free parameters inside an array passed to an Operation yield correct gradients."
-        par = [0.4, 1.3]
+        par = anp.array([0.4, 1.3], requires_grad=True)
 
         def qf(x, y):
             qml.Displacement(0.5, 0, wires=[0])
diff --git a/tests/transforms/test_specs.py b/tests/transforms/test_specs.py
index c169f368c..d4739734b 100644
--- a/tests/transforms/test_specs.py
+++ b/tests/transforms/test_specs.py
@@ -14,6 +14,7 @@
 """Unit tests for the specs transform"""
 import pytest
 from collections import defaultdict
+from contextlib import nullcontext
 
 import pennylane as qml
 from pennylane import numpy as np
@@ -180,8 +181,13 @@ class TestSpecsTransformBetaQNode:
         def circ():
             return qml.expval(qml.PauliZ(0))
 
-        info_func = qml.specs(circ)
-        info = info_func()
+        with (
+            pytest.warns(UserWarning, match="gradient of a tape with no trainable parameters")
+            if diff_method == "parameter-shift"
+            else nullcontext()
+        ):
+            info_func = qml.specs(circ)
+            info = info_func()
         assert len(info) == len_info
 
         assert info["gate_sizes"] == defaultdict(int)
@@ -313,7 +319,8 @@ class TestSpecsTransformBetaQNode:
         def circuit():
             return qml.probs(wires=0)
 
-        info = qml.specs(circuit)()
+        with pytest.warns(UserWarning, match="gradient of a tape with no trainable parameters"):
+            info = qml.specs(circuit)()
         assert info["diff_method"] == "pennylane.gradients.parameter_shift.param_shift"
         assert info["gradient_fn"] == "pennylane.gradients.parameter_shift.param_shift"