Repository: PennyLaneAI/pennylane
Pull request #1591 (2021-08-25 14:56:49)

[Code Together] - Include warning validation on qnode and QNode for unknown keyword arguments (#1496)
Hello ... first time contributor :) I know this isn't ready yet ... just wanted to get feedback on direction.

Were you hoping to take the existing diff_options and create explicit keyword arguments, and use kwargs to handle unknown keyword arguments that would all raise a warning? Sort of what I've added in the code?

Issue #1496 

ianmclean2011: @thisac thanks for the feedback above. I've cleaned up those punctuation issues, and added a test. Is there anything else you'd like me to add/change? 

I'm not sure of the best way to resolve the CodeFactor issue (too many arguments). Also, I assumed that pulling out the previous arguments and setting them as explicit arguments would be covered by additional tests that were already using those (e.g. adjoint_cache).

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 222410636..0c0d59359 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -397,6 +397,8 @@ and requirements-ci.txt (unpinned). This latter would be used by the CI.
   floats, ints, lists and numpy arrays and return numpy output but can not be differentiated.
   [(#1585)](https://github.com/PennyLaneAI/pennylane/pull/1585)
 
+* QNodes now include validation to warn users if a supplied keyword argument is not one of the
+  recognized arguments. [(#1496)](https://github.com/PennyLaneAI/pennylane/pull/1591)
 
 <h3>Breaking changes</h3>
 
@@ -451,8 +453,8 @@ and requirements-ci.txt (unpinned). This latter would be used by the CI.
 This release contains contributions from (in alphabetical order):
 
 Vishnu Ajith, Akash Narayanan B, Thomas Bromley, Olivia Di Matteo, Sahaj Dhamija, Tanya Garg, Josh Izaac,
-Prateek Jain, Ankit Khandelwal, Christina Lee, Johannes Jakob Meyer, Romain Moyard, Esteban Payares, Pratul Saini,
-Maria Schuld, Arshpreet Singh, Ingrid Strandberg, Slimane Thabet, David Wierichs, Vincent Wong.
+Prateek Jain, Ankit Khandelwal, Christina Lee, Ian McLean, Johannes Jakob Meyer, Romain Moyard, Esteban Payares,
+Pratul Saini, Maria Schuld, Arshpreet Singh, Ingrid Strandberg, Slimane Thabet, David Wierichs, Vincent Wong.
 
 # Release 0.17.0 (current release)
 
diff --git a/pennylane/qnode.py b/pennylane/qnode.py
index c93ef9619..492ff4067 100644
--- a/pennylane/qnode.py
+++ b/pennylane/qnode.py
@@ -121,20 +121,28 @@ class QNode:
             supported, and results in a gate decomposition. If any operations in the decomposition
             remain unsupported by the device, another expansion occurs.
 
-    Keyword Args:
-        h=1e-7 (float): step size for the finite difference method
-        order=1 (int): The order of the finite difference method to use. ``1`` corresponds
+        h (float): step size for the finite difference method
+
+        order (int): The order of the finite difference method to use. ``1`` corresponds
             to forward finite differences, ``2`` to centered finite differences.
-        shift=pi/2 (float): the size of the shift for two-term parameter-shift gradient computations
-        adjoint_cache=True (bool): for TensorFlow and PyTorch interfaces and adjoint differentiation,
+
+        shift (float): the size of the shift for two-term parameter-shift gradient computations
+
+        adjoint_cache (bool): For TensorFlow and PyTorch interfaces and adjoint differentiation,
+
             this indicates whether to save the device state after the forward pass.  Doing so saves a
             forward execution. Device state automatically reused with autograd and JAX interfaces.
-        argnum=None (int, list(int), None): Which argument(s) to compute the Jacobian
+
+        argnum (int, list(int), None): Which argument(s) to compute the Jacobian
             with respect to. When there are fewer parameters specified than the
             total number of trainable parameters, the jacobian is being estimated. Note
             that this option is only applicable for the following differentiation methods:
             ``"parameter-shift"``, ``"finite-diff"`` and ``"reversible"``.
 
+        kwargs: used to catch all unrecognized keyword arguments and provide a user warning
+
+            about them
+
     **Example**
 
     >>> def circuit(x):
@@ -154,7 +162,12 @@ class QNode:
         diff_method="best",
         mutable=True,
         max_expansion=10,
-        **diff_options,
+        h=1e-7,
+        order=1,
+        shift=np.pi / 2,
+        adjoint_cache=True,
+        argnum=None,
+        **kwargs,
     ):
 
         if diff_method is None:
@@ -186,6 +199,23 @@ class QNode:
         else:
             self._qfunc_uses_shots_arg = False
 
+        if kwargs:
+            for key in kwargs:
+                warnings.warn(
+                    f"'{key}' is unrecognized, and will not be included in your computation. "
+                    "Please review the QNode class or qnode decorator for the list of available "
+                    "keyword variables.",
+                    UserWarning,
+                )
+
+        diff_options = {
+            "h": h,
+            "order": order,
+            "shift": shift,
+            "adjoint_cache": adjoint_cache,
+            "argnum": argnum,
+        }
+
         self.mutable = mutable
         self.func = func
         self._original_device = device
@@ -204,7 +234,7 @@ class QNode:
             self.diff_method = self._get_best_diff_method(tape_diff_options)
 
         # The arguments to be passed to JacobianTape.jacobian
-        self.diff_options = diff_options or {}
+        self.diff_options = diff_options
         self.diff_options.update(tape_diff_options)
 
         self.dtype = np.float64
@@ -1016,13 +1046,19 @@ class QNode:
     }
 
 
+# pylint:disable=too-many-arguments
 def qnode(
     device,
     interface="autograd",
     diff_method="best",
     mutable=True,
     max_expansion=10,
-    **diff_options,
+    h=1e-7,
+    order=1,
+    shift=np.pi / 2,
+    adjoint_cache=True,
+    argnum=None,
+    **kwargs,
 ):
     """Decorator for creating QNodes.
 
@@ -1105,19 +1141,28 @@ def qnode(
             supported, and results in a gate decomposition. If any operations in the decomposition
             remain unsupported by the device, another expansion occurs.
 
-    Keyword Args:
-        h=1e-7 (float): Step size for the finite difference method.
-        order=1 (int): The order of the finite difference method to use. ``1`` corresponds
+        h (float): step size for the finite difference method
+
+        order (int): The order of the finite difference method to use. ``1`` corresponds
             to forward finite differences, ``2`` to centered finite differences.
-        adjoint_cache=True (bool): for TensorFlow and PyTorch interfaces and adjoint differentiation,
+
+        shift (float): the size of the shift for two-term parameter-shift gradient computations
+
+        adjoint_cache (bool): For TensorFlow and PyTorch interfaces and adjoint differentiation,
+
             this indicates whether to save the device state after the forward pass.  Doing so saves a
             forward execution. Device state automatically reused with autograd and JAX interfaces.
-        argnum=None (int, list(int), None): Which argument(s) to compute the Jacobian
+
+        argnum (int, list(int), None): Which argument(s) to compute the Jacobian
             with respect to. When there are fewer parameters specified than the
             total number of trainable parameters, the jacobian is being estimated. Note
             that this option is only applicable for the following differentiation methods:
             ``"parameter-shift"``, ``"finite-diff"`` and ``"reversible"``.
 
+        kwargs: used to catch all unrecognized keyword arguments and provide a user warning
+
+            about them
+
     **Example**
 
     >>> dev = qml.device("default.qubit", wires=1)
@@ -1137,7 +1182,12 @@ def qnode(
             diff_method=diff_method,
             mutable=mutable,
             max_expansion=max_expansion,
-            **diff_options,
+            h=h,
+            order=order,
+            shift=shift,
+            adjoint_cache=adjoint_cache,
+            argnum=argnum,
+            **kwargs,
         )
         return update_wrapper(qn, func)
 


Test patch:
diff --git a/tests/tape/test_qnode.py b/tests/tape/test_qnode.py
index c7e1fe574..cd3b4b3c4 100644
--- a/tests/tape/test_qnode.py
+++ b/tests/tape/test_qnode.py
@@ -573,6 +573,60 @@ class TestValidation:
         # No differentiation required. No error raised.
         grad()
 
+    def test_unrecognized_keyword_arguments_validation(self):
+        """Tests that a UserWarning is raised when unrecognized keyword arguments are provided."""
+
+        # use two unrecognized methods, to confirm that multiple warnings are raised
+        unrecognized_one = "test_method_one"
+        unrecognized_two = "test_method_two"
+        warning_text = (
+            " is unrecognized, and will not be included in your computation. "
+            "Please review the QNode class or qnode decorator for the list of available "
+            "keyword variables."
+        )
+
+        expected_warnings = {
+            (UserWarning, f"'{unrecognized_one}'{warning_text}"),
+            (UserWarning, f"'{unrecognized_two}'{warning_text}"),
+        }
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(UserWarning) as warning_list:
+
+            QNode(dummyfunc, dev, test_method_one=1, test_method_two=2)
+
+        warnings = {(warning.category, warning.message.args[0]) for warning in warning_list}
+        assert warnings == expected_warnings
+
+    def test_unrecognized_keyword_arguments_validation_decorator(self):
+        """Tests that a UserWarning is raised when unrecognized keyword arguments are provided."""
+
+        # use two unrecognized methods, to confirm that multiple warnings are raised
+        unrecognized_one = "test_method_one"
+        unrecognized_two = "test_method_two"
+        warning_text = (
+            " is unrecognized, and will not be included in your computation. "
+            "Please review the QNode class or qnode decorator for the list of available "
+            "keyword variables."
+        )
+
+        expected_warnings = {
+            (UserWarning, f"'{unrecognized_one}'{warning_text}"),
+            (UserWarning, f"'{unrecognized_two}'{warning_text}"),
+        }
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(UserWarning) as warning_list:
+
+            @qml.qnode(dev, test_method_one=1, test_method_two=2)
+            def circ():
+                return qml.expval(qml.PauliZ(0))
+
+        warnings = {(warning.category, warning.message.args[0]) for warning in warning_list}
+        assert warnings == expected_warnings
+
 
 class TestTapeConstruction:
     """Tests for the tape construction"""