Repository: PennyLaneAI/pennylane
Pull request #1406 (2021-06-14 17:16:41)

Warnings for adjoint diff with finite shots
Fixes Issue #1092 

When a QNode is created with a device with finite shots and adjoint differentiation, the User receives a warning reminding that adjoint differentiation is always exact.

When the adjoint jacobian method is called on a device with finite shots, the User also receives a warning to the same effect.

I only warn instead of interrupting with an error, as finite shots doesn't actually break the code.

josh146: Thanks for tackling this @albi3ro!

> if diff_method="best" and analytic=False, modify the QNode logic to reject adjoint/reversible and instead use parameter-shift. This way gradients will be estimated with a finite number of shots.

I think it’s best to leave this for now. I’ve been thinking about this a lot, and I wonder if the best approach is to instead, for next release, modify the adjoint method to *sample* from the final inner products/expectation values, rather than computing them directly.

This will be a nice, straightforward way of having the adjoint method support finite-shots, while avoiding the need to drop back down to parameter-shift.
albi3ro: @antalszava  Thanks for reminding me about reversible ;) I forgot...

I don't think we should switch the differentiation method to parameter shift behind the scenes, as parameter shift can send off many expensive circuit executions that the user might not be expecting.

I also included the warnings in two places:
* I think we need to warn when the QNode is initialized before anything happens, because that's the best place for users to do something about it
* We also need to warn in the actual `adjoint_jacobian` function, because shots may have changed since the QNode was constructed or users could access the method via a different route.
antalszava: > I don't think we should switch the differentiation method to parameter shift behind the scenes, as parameter shift can send off many expensive circuit executions that the user might not be expecting.

Originally the following was proposed:

> if diff_method="best" and analytic=False, modify the QNode logic to reject adjoint/reversible and instead use parameter-shift. This way gradients will be estimated with a finite number of shots.

That is, the user would specify `diff_method="best"` and `shots=1000` (some finite shots). If the user is aware of the meaning of these options then it would seem sensible to use the parameter shift rule as that's the best diff method for finite shots. There is of course the possibility that these options are specified without knowing the exact details, though not sure if we should tackle it by falling back to exact and adjoint or even a warning. :thinking:


> I think we need to warn when the QNode is initialized before anything happens, because that's the best place for users to do something about it

Would be nice to have a test case for this: the QNode is initialized, but not evaluated and the user sees a warning.
albi3ro: @josh146  I don't it makes sense for adjoint diff or reversible to ever be computed with finite shots.  Finite shots is used to mimic the behaviour of actual quantum devices, but adjoint diff and reversible aren't computations you can reasonably make on actual hardware.

Unless we know the distributions will match that of parameter shift?
albi3ro: Also, neither adjoint nor reversible will ever get selected if `diff_method="best"`.  We should change that, but it should be a different PR.

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 2fdac6772..1ee7f40da 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -577,6 +577,9 @@ random_mat2 = rng.standard_normal(3, requires_grad=False)
 
 <h3>Bug fixes</h3>
 
+* Warns when adjoint or reversible differentiation specified or called on a device with finite shots.
+[(#1406)](https://github.com/PennyLaneAI/pennylane/pull/1406)
+
 * Fixes the differentiability of the operations `IsingXX` and `IsingZZ` for Autograd, Jax and Tensorflow.
 [(#1390)](https://github.com/PennyLaneAI/pennylane/pull/1390)
 
diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py
index b75b83ac0..5300cd088 100644
--- a/pennylane/_qubit_device.py
+++ b/pennylane/_qubit_device.py
@@ -854,6 +854,13 @@ class QubitDevice(Device):
             if not hasattr(m.obs, "base_name"):
                 m.obs.base_name = None  # This is needed for when the observable is a tensor product
 
+        if self.shots is not None:
+            warnings.warn(
+                "Requested adjoint differentiation to be computed with finite shots."
+                " The derivative is always exact when using the adjoint differentiation method.",
+                UserWarning,
+            )
+
         # Initialization of state
         if starting_state is not None:
             ket = self._reshape(starting_state, [2] * self.num_wires)
diff --git a/pennylane/qnode.py b/pennylane/qnode.py
index d0e9a0d7e..3381eaea8 100644
--- a/pennylane/qnode.py
+++ b/pennylane/qnode.py
@@ -263,8 +263,8 @@ class QNode:
         This method attempts to determine support for differentiation
         methods using the following order:
 
-        * ``"backprop"``
         * ``"device"``
+        * ``"backprop"``
         * ``"parameter-shift"``
         * ``"finite-diff"``
 
@@ -386,6 +386,13 @@ class QNode:
                 f"The {device.short_name} device does not support reversible differentiation."
             )
 
+        if device.shots is not None:
+            warnings.warn(
+                "Requested reversible differentiation to be computed with finite shots."
+                " Reversible differentiation always calculated exactly.",
+                UserWarning,
+            )
+
         return ReversibleTape, interface, device, {"method": "analytic"}
 
     @staticmethod
@@ -419,6 +426,13 @@ class QNode:
                 f"The {device.short_name} device does not support adjoint differentiation."
             )
 
+        if device.shots is not None:
+            warnings.warn(
+                "Requested adjoint differentiation to be computed with finite shots."
+                " Adjoint differentiation always calculated exactly.",
+                UserWarning,
+            )
+
         jac_options = {"method": "device", "jacobian_method": "adjoint_jacobian"}
         # reuse the forward pass
         # torch and tensorflow can cache the state
diff --git a/pennylane/tape/reversible.py b/pennylane/tape/reversible.py
index 4396baacc..d181f7525 100644
--- a/pennylane/tape/reversible.py
+++ b/pennylane/tape/reversible.py
@@ -18,6 +18,7 @@ Quantum tape that implements reversible backpropagation.
 import copy
 from functools import reduce
 from string import ascii_letters as ABC
+import warnings
 
 import numpy as np
 
@@ -35,7 +36,7 @@ class ReversibleTape(JacobianTape):
 
     .. note::
 
-        The reversible analytic differentation method has the following restrictions:
+        The reversible analytic differentiation method has the following restrictions:
 
         * As it requires knowledge of the statevector, only statevector simulator devices can be used.
 
@@ -246,6 +247,13 @@ class ReversibleTape(JacobianTape):
 
         # before each Jacobian call, so that the statevector is calculated only once.
         self._final_state = None
+        if device.shots is not None:
+            warnings.warn(
+                "Requested reversible differentiation to be computed with finite shots."
+                " Reversible differentiation always calculated exactly.",
+                UserWarning,
+            )
+
         return super().jacobian(device, params, **options)
 
     def analytic_pd(self, idx, params, **options):


Test patch:
diff --git a/tests/tape/test_qnode.py b/tests/tape/test_qnode.py
index d0d10b7e5..4504236df 100644
--- a/tests/tape/test_qnode.py
+++ b/tests/tape/test_qnode.py
@@ -261,6 +261,60 @@ class TestValidation:
         with pytest.raises(ValueError, match="The default.gaussian device does not"):
             QNode._validate_adjoint_method(dev, "tf")
 
+    def test_validate_adjoint_finite_shots(self):
+        """Test that a UserWarning is raised when device has finite shots"""
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(
+            UserWarning, match="Requested adjoint differentiation to be computed with finite shots."
+        ):
+            QNode._validate_adjoint_method(dev, "autograd")
+
+    def test_adjoint_finite_shots(self):
+        """Tests that UserWarning is raised with the adjoint differentiation method
+        on QNode construction when the device has finite shots
+        """
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(
+            UserWarning, match="Requested adjoint differentiation to be computed with finite shots."
+        ):
+
+            @qml.qnode(dev, diff_method="adjoint")
+            def circ():
+                return qml.expval(qml.PauliZ(0))
+
+    def test_validate_reversible_finite_shots(self):
+        """Test that a UserWarning is raised when validating the reversible differentiation method
+        and using a device that has finite shots
+        """
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(
+            UserWarning,
+            match="Requested reversible differentiation to be computed with finite shots.",
+        ):
+            QNode._validate_reversible_method(dev, "autograd")
+
+    def test_reversible_finite_shots(self):
+        """Tests that UserWarning is raised with the reversible differentiation method
+        on QNode construction when the device has finite shots
+        """
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(
+            UserWarning,
+            match="Requested reversible differentiation to be computed with finite shots.",
+        ):
+
+            @qml.qnode(dev, diff_method="reversible")
+            def circ():
+                return qml.expval(qml.PauliZ(0))
+
     def test_qnode_print(self):
         """Test that printing a QNode object yields the right information."""
         dev = qml.device("default.qubit", wires=1)
diff --git a/tests/tape/test_reversible.py b/tests/tape/test_reversible.py
index a83633a7a..d391a7f68 100644
--- a/tests/tape/test_reversible.py
+++ b/tests/tape/test_reversible.py
@@ -11,7 +11,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-"""Unit tests for the qubit parameter-shift QubitParamShiftTape"""
+"""Unit tests for the ReversibleTape"""
 import pytest
 from pennylane import numpy as np
 
@@ -160,6 +160,21 @@ class TestReversibleTape:
 class TestGradients:
     """Jacobian integration tests for qubit expectations."""
 
+    def test_finite_shots_warning(self):
+        """Test that a warning is raised when calling the jacobian with a device with finite shots"""
+
+        with ReversibleTape() as tape:
+            qml.RX(0.1, wires=0)
+            qml.expval(qml.PauliZ(0))
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(
+            UserWarning,
+            match="Requested reversible differentiation to be computed with finite shots.",
+        ):
+            tape.jacobian(dev)
+
     @pytest.mark.parametrize("theta", np.linspace(-2 * np.pi, 2 * np.pi, 7))
     @pytest.mark.parametrize("G", [qml.RX, qml.RY, qml.RZ])
     def test_pauli_rotation_gradient(self, G, theta, tol):
@@ -369,6 +384,27 @@ class TestGradients:
 class TestQNodeIntegration:
     """Test QNode integration with the reversible method"""
 
+    def test_finite_shots_warning(self):
+        """Test that a warning is raised when calling the jacobian with a device with finite shots"""
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(
+            UserWarning,
+            match="Requested reversible differentiation to be computed with finite shots.",
+        ):
+
+            @qml.qnode(dev, diff_method="reversible")
+            def circ(x):
+                qml.RX(x, wires=0)
+                return qml.expval(qml.PauliZ(0))
+
+        with pytest.warns(
+            UserWarning,
+            match="Requested reversible differentiation to be computed with finite shots.",
+        ):
+            qml.grad(circ)(0.1)
+
     def test_qnode(self, mocker, tol):
         """Test that specifying diff_method allows the reversible
         method to be selected"""
diff --git a/tests/test_qubit_device_adjoint_jacobian.py b/tests/test_qubit_device_adjoint_jacobian.py
index b42109006..910886a4d 100644
--- a/tests/test_qubit_device_adjoint_jacobian.py
+++ b/tests/test_qubit_device_adjoint_jacobian.py
@@ -39,6 +39,19 @@ class TestAdjointJacobian:
         with pytest.raises(qml.QuantumFunctionError, match="Adjoint differentiation method does"):
             dev.adjoint_jacobian(tape)
 
+    def test_finite_shots_warns(self):
+        """Tests warning raised when finite shots specified"""
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with qml.tape.JacobianTape() as tape:
+            qml.expval(qml.PauliZ(0))
+
+        with pytest.warns(
+            UserWarning, match="Requested adjoint differentiation to be computed with finite shots."
+        ):
+            dev.adjoint_jacobian(tape)
+
     def test_unsupported_op(self, dev):
         """Test if a QuantumFunctionError is raised for an unsupported operation, i.e.,
         multi-parameter operations that are not qml.Rot"""
@@ -239,6 +252,25 @@ class TestAdjointJacobianQNode:
     def dev(self):
         return qml.device("default.qubit", wires=2)
 
+    def test_finite_shots_warning(self):
+        """Tests that a warning is raised when computing the adjoint diff on a device with finite shots"""
+
+        dev = qml.device("default.qubit", wires=1, shots=1)
+
+        with pytest.warns(
+            UserWarning, match="Requested adjoint differentiation to be computed with finite shots."
+        ):
+
+            @qml.qnode(dev, diff_method="adjoint")
+            def circ(x):
+                qml.RX(x, wires=0)
+                return qml.expval(qml.PauliZ(0))
+
+        with pytest.warns(
+            UserWarning, match="Requested adjoint differentiation to be computed with finite shots."
+        ):
+            qml.grad(circ)(0.1)
+
     def test_qnode(self, mocker, tol, dev):
         """Test that specifying diff_method allows the adjoint method to be selected"""
         args = np.array([0.54, 0.1, 0.5], requires_grad=True)