Repository: PennyLaneAI/pennylane
Pull request #5748 (2024-05-28 11:34:27)

Test supported gradient kwargs and raise error for `shots` kwarg
**Context:**
Additional kwargs passed to a QNode at initialization are interpreted as gradient kwargs.
They are checked against the hardcoded collection `qml.gradients.SUPPORTED_GRADIENT_KWARGS` to make sure that this boilerplate interpretation of additional kwargs makes sense.

**Description of the Change:**
This PR adds a test that makes sure that  `qml.gradients.SUPPORTED_GRADIENT_KWARGS`  actually matches all gradient transform kwargs supported by PL.
In addition, this PR introduces an error being raised when the keyword argument `shots` is passed at QNode initialization, because this is not a valid gradient kwarg and even if it were, it would lead to the confusing behaviour of `QNode(..., shots=100)` not executing with `shots=100`.

**Benefits:**
Code quality, better user input validation.

**Possible Drawbacks:**

**Related GitHub Issues:**
#5745 

[sc-64175]


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://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5748?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.67%. Comparing base [(`fbc2a39`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/fbc2a393e90f04e337c3c3609efeebffe4235ff2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`0b544b2`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/0b544b21d7fcd75bc5c2e79ef5e0b29cc12fe3ee?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 1 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5748      +/-   ##
==========================================
- Coverage   99.68%   99.67%   -0.01%     
==========================================
  Files         416      416              
  Lines       39105    38398     -707     
==========================================
- Hits        38981    38273     -708     
- Misses        124      125       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5748?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?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 6d198b3b1..8dd20c57e 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -158,6 +158,9 @@
 
 <h3>Breaking changes 💔</h3>
 
+* Passing `shots` as a keyword argument to a `QNode` initialization now raises an error, instead of ignoring the input.
+  [(#5748)](https://github.com/PennyLaneAI/pennylane/pull/5748)
+
 * A custom decomposition can no longer be provided to `QDrift`. Instead, apply the operations in your custom
   operation directly with `qml.apply`.
   [(#5698)](https://github.com/PennyLaneAI/pennylane/pull/5698)
diff --git a/pennylane/gradients/gradient_transform.py b/pennylane/gradients/gradient_transform.py
index 4b3331bb4..0eb6eab5e 100644
--- a/pennylane/gradients/gradient_transform.py
+++ b/pennylane/gradients/gradient_transform.py
@@ -21,34 +21,31 @@ from functools import partial
 import pennylane as qml
 from pennylane.measurements import MutualInfoMP, ProbabilityMP, StateMP, VarianceMP, VnEntropyMP
 
-SUPPORTED_GRADIENT_KWARGS = [
+SUPPORTED_GRADIENT_KWARGS = {
     "approx_order",
     "argnum",
+    "atol",
     "aux_wire",
     "broadcast",  # [TODO: This is in param_shift. Unify with use_broadcasting in stoch_pulse_grad
     "device_wires",
     "diagonal_shifts",
+    "fallback_fn",
     "f0",
     "force_order2",
     "gradient_recipes",
-    "gradient_kwargs",
     "h",
     "n",
-    "num",
     "num_directions",
     "num_split_times",
     "off_diagonal_shifts",
-    "order",
-    "reduction",
     "sampler",
     "sampler_rng",
     "sampler_seed",
     "shifts",
-    "shots",
     "strategy",
     "use_broadcasting",
     "validate_params",
-]
+}
 
 
 def assert_multimeasure_not_broadcasted(measurements, broadcast):
diff --git a/pennylane/workflow/qnode.py b/pennylane/workflow/qnode.py
index 947a636b0..525f2d662 100644
--- a/pennylane/workflow/qnode.py
+++ b/pennylane/workflow/qnode.py
@@ -487,14 +487,20 @@ class QNode:
         for kwarg in gradient_kwargs:
             if kwarg in ["gradient_fn", "grad_method"]:
                 warnings.warn(
-                    f"It appears you may be trying to set the method of differentiation via the kwarg "
-                    f"{kwarg}. This is not supported in qnode and will default to backpropogation. Use "
-                    f"diff_method instead."
+                    "It appears you may be trying to set the method of differentiation via the "
+                    f"keyword argument {kwarg}. This is not supported in qnode and will default to "
+                    "backpropogation. Use diff_method instead."
+                )
+            elif kwarg == "shots":
+                raise ValueError(
+                    "'shots' is not a valid gradient_kwarg. If your quantum function takes the "
+                    "argument 'shots' or if you want to set the number of shots with which the "
+                    "QNode is executed, pass it to the QNode call, not its definition."
                 )
             elif kwarg not in qml.gradients.SUPPORTED_GRADIENT_KWARGS:
                 warnings.warn(
-                    f"Received gradient_kwarg {kwarg}, which is not included in the list of standard qnode "
-                    f"gradient kwargs."
+                    f"Received gradient_kwarg {kwarg}, which is not included in the list of "
+                    "standard qnode gradient kwargs."
                 )
 
         # input arguments


Test patch:
diff --git a/tests/gradients/core/test_gradient_transform.py b/tests/gradients/core/test_gradient_transform.py
index b46cded82..681920f77 100644
--- a/tests/gradients/core/test_gradient_transform.py
+++ b/tests/gradients/core/test_gradient_transform.py
@@ -12,15 +12,49 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """Tests for the gradients.gradient_transform module."""
+import inspect
+
 import pytest
 
 import pennylane as qml
 from pennylane import numpy as np
 from pennylane.gradients.gradient_transform import (
+    SUPPORTED_GRADIENT_KWARGS,
     _find_gradient_methods,
     _validate_gradient_methods,
     choose_trainable_params,
 )
+from pennylane.transforms.core import TransformDispatcher
+
+
+def test_supported_gradient_kwargs():
+    """Test that all keyword arguments of gradient transforms are
+    registered as supported gradient kwargs, and no others."""
+    # Collect all gradient transforms
+    grad_transforms = []
+    for attr in qml.gradients.__dir__():
+        if attr == "metric_tensor":
+            # Skip metric_tensor because it is not a diff_method
+            continue
+        obj = getattr(qml.gradients, attr)
+        if isinstance(obj, TransformDispatcher):
+            grad_transforms.append(obj)
+
+    # Collect arguments of all gradient transforms
+    grad_kwargs = set()
+    for tr in grad_transforms:
+        grad_kwargs |= set(inspect.signature(tr).parameters)
+
+    # Remove arguments that are not keyword arguments
+    grad_kwargs -= {"tape"}
+    # Remove "dev", because we decided against supporting this kwarg, although
+    # it is an argument to param_shift_cv, to avoid confusion.
+    grad_kwargs -= {"dev"}
+
+    # Check equality of required and supported gradient kwargs
+    assert grad_kwargs == SUPPORTED_GRADIENT_KWARGS
+    # Shots should never be used as a gradient kwarg
+    assert "shots" not in grad_kwargs
 
 
 def test_repr():
diff --git a/tests/test_debugging.py b/tests/test_debugging.py
index 3c259ec11..0434fe22a 100644
--- a/tests/test_debugging.py
+++ b/tests/test_debugging.py
@@ -242,12 +242,12 @@ class TestSnapshot:
 
         assert result == expected
 
-    @pytest.mark.parametrize("shots", [None, 0, 1, 100])
+    @pytest.mark.parametrize("shots", [None, 1, 1000000])
     def test_different_shots(self, shots):
         """Test that snapshots are returned correctly with different QNode shot values."""
-        dev = qml.device("default.qubit", wires=2)
+        dev = qml.device("default.qubit", wires=2, shots=shots)
 
-        @qml.qnode(dev, shots=shots)
+        @qml.qnode(dev)
         def circuit():
             qml.Snapshot()
             qml.Hadamard(wires=0)
@@ -265,7 +265,18 @@ class TestSnapshot:
         }
 
         assert all(k1 == k2 for k1, k2 in zip(result.keys(), expected.keys()))
-        assert all(np.allclose(v1, v2) for v1, v2 in zip(result.values(), expected.values()))
+        for v1, v2 in zip(result.values(), expected.values()):
+            if v1.shape == ():
+                # Expectation value comparison
+                assert v2.shape == ()
+                if shots != 1:
+                    # If many shots or analytic mode, we can compare the result
+                    assert np.allclose(v1, v2, atol=0.006)
+                else:
+                    # If a single shot, it must be in the eigvals of qml.Z
+                    assert v1.item() in {-1.0, 1.0}
+            else:
+                assert np.allclose(v1, v2)
 
     @pytest.mark.parametrize(
         "m,expected_result",
diff --git a/tests/test_qnode.py b/tests/test_qnode.py
index 2682861e6..b8c965b5d 100644
--- a/tests/test_qnode.py
+++ b/tests/test_qnode.py
@@ -1233,6 +1233,22 @@ class TestShots:
         assert len(ansatz1(0.8, shots=0)) == 10
         assert ansatz1.qtape.operations[0].wires.labels == (0,)
 
+    def test_shots_passed_as_unrecognized_kwarg(self):
+        """Test that an error is raised if shots are passed to QNode initialization."""
+        dev = qml.device("default.qubit", wires=[0, 1], shots=10)
+
+        def ansatz0():
+            return qml.expval(qml.X(0))
+
+        with pytest.raises(ValueError, match="'shots' is not a valid gradient_kwarg."):
+            qml.QNode(ansatz0, dev, shots=100)
+
+        with pytest.raises(ValueError, match="'shots' is not a valid gradient_kwarg."):
+
+            @qml.qnode(dev, shots=100)
+            def _():
+                return qml.expval(qml.X(0))
+
     # pylint: disable=unexpected-keyword-arg
     def test_shots_setting_does_not_mutate_device(self):
         """Tests that per-call shots setting does not change the number of shots in the device."""