Repository: PennyLaneAI/pennylane
Pull request #1588 (2021-08-24 15:30:48)

Error when creating qnode with backprop and finite-shots
Fixes Issue #1481 

Right now, you can create a qnode that uses finite shots and backpropagation.  This is fine on qnode execution on the forward pass, but will always fail and raise some sort of unhelpful error message of the backward pass/ gradient computation.

For example:

```python
dev = qml.device('default.qubit.autograd', shots=10, wires=1)

@qml.qnode(dev, interface='autograd', diff_method='backprop')
def circ(x):
    qml.RX(x, wires=0)
    return qml.expval(qml.PauliZ(0))

## works fine
circ(0.1)

# incomprehensible error
qml.grad(circ)(0.1)
```

For context, that error is:
```
ValueError: setting an array element with a sequence.
```

With this change, during QNode creation, a `UserWarning` is raise.  I decided to use a warning instead of a Error, as users may still want to calculate finite shot forward passes on an interface device.  This way, they at least have an explanation for the strange errors they will end up getting.

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [.github/CHANGELOG.md](/PennyLaneAI/pennylane/blob/master/.github/CHANGELOG.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.
antalszava: [ch8003]
antalszava: Hi @albi3ro what's the status on this?

> I decided to use a warning instead of a Error, as users may still want to calculate finite shot forward passes on an interface device.

Could they simply just evaluate the QNode without trying to differentiate it? If so, maybe we could refer to that in the error message.
albi3ro: @rmoyard  what do we need to add?
rmoyard: @albi3ro I think one one the test using jax was deleted. `default_qubit_jax.py` line 269 more precisely we should add something covering this line.

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 0c0d59359..b08b371fe 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -402,6 +402,10 @@ and requirements-ci.txt (unpinned). This latter would be used by the CI.
 
 <h3>Breaking changes</h3>
 
+* An error is raised during QNode creation when a user requests backpropagation on
+  a device with finite-shots.
+  [(#1588)](https://github.com/PennyLaneAI/pennylane/pull/1588)
+
 * The class `qml.Interferometer` is deprecated and will be renamed `qml.InterferometerUnitary`
   after one release cycle.
   [(#1546)](https://github.com/PennyLaneAI/pennylane/pull/1546)
diff --git a/pennylane/qnode.py b/pennylane/qnode.py
index 492ff4067..9726d48de 100644
--- a/pennylane/qnode.py
+++ b/pennylane/qnode.py
@@ -372,6 +372,12 @@ class QNode:
             qml.QuantumFunctionError: if the device does not support backpropagation, or the
             interface provided is not compatible with the device
         """
+        if device.shots is not None:
+            raise qml.QuantumFunctionError(
+                "Devices with finite shots are incompatible with backpropogation. "
+                "Please set shots=None or chose a different diff_method."
+            )
+
         # determine if the device supports backpropagation
         backprop_interface = device.capabilities().get("passthru_interface", None)
 


Test patch:
diff --git a/tests/devices/test_default_qubit_jax.py b/tests/devices/test_default_qubit_jax.py
index ecb4cf6d2..5593618cf 100644
--- a/tests/devices/test_default_qubit_jax.py
+++ b/tests/devices/test_default_qubit_jax.py
@@ -159,7 +159,7 @@ class TestQNodeIntegration:
         def circuit(key):
             dev = qml.device("default.qubit.jax", wires=1, shots=1000, prng_key=key)
 
-            @qml.qnode(dev, interface="jax", diff_method="backprop")
+            @qml.qnode(dev, interface="jax", diff_method=None)
             def inner_circuit():
                 qml.Hadamard(0)
                 return qml.sample(qml.PauliZ(wires=0))
@@ -176,7 +176,7 @@ class TestQNodeIntegration:
         """Test that op-by-op sampling works as a new user would expect"""
         dev = qml.device("default.qubit.jax", wires=1, shots=1000)
 
-        @qml.qnode(dev, interface="jax", diff_method="backprop")
+        @qml.qnode(dev, interface="jax", diff_method=None)
         def circuit():
             qml.Hadamard(0)
             return qml.sample(qml.PauliZ(wires=0))
@@ -191,7 +191,7 @@ class TestQNodeIntegration:
         """
         dev = qml.device("default.qubit.jax", wires=1, shots=None)
 
-        @qml.qnode(dev, interface="jax", diff_method="backprop")
+        @qml.qnode(dev, interface="jax", diff_method=None)
         def circuit():
             return qml.sample(qml.PauliZ(wires=0))
 
@@ -206,7 +206,7 @@ class TestQNodeIntegration:
         """Test for gates that weren't covered by other tests."""
         dev = qml.device("default.qubit.jax", wires=2, shots=1000)
 
-        @qml.qnode(dev, interface="jax", diff_method="backprop")
+        @qml.qnode(dev, interface="jax", diff_method=None)
         def circuit():
             qml.CRZ(0.0, wires=[0, 1])
             qml.CRX(0.0, wires=[0, 1])
@@ -222,7 +222,7 @@ class TestQNodeIntegration:
         """Test that diagonal gates can be used."""
         dev = qml.device("default.qubit.jax", wires=1, shots=1000)
 
-        @qml.qnode(dev, interface="jax", diff_method="backprop")
+        @qml.qnode(dev, interface="jax", diff_method=None)
         def circuit():
             qml.DiagonalQubitUnitary(np.array([1.0, 1.0]), wires=0)
             return qml.sample(qml.PauliZ(wires=0))
diff --git a/tests/devices/test_default_qubit_tf.py b/tests/devices/test_default_qubit_tf.py
index 6ca5d1d53..6e549ce83 100644
--- a/tests/devices/test_default_qubit_tf.py
+++ b/tests/devices/test_default_qubit_tf.py
@@ -1312,7 +1312,7 @@ class TestSamples:
         shots = 100
         dev = qml.device("default.qubit.tf", wires=2, shots=shots)
 
-        @qml.qnode(dev, diff_method="backprop", interface="tf")
+        @qml.qnode(dev, diff_method="best", interface="tf")
         def circuit(a):
             qml.RX(a, wires=0)
             return qml.sample(qml.PauliZ(0))
@@ -1324,28 +1324,11 @@ class TestSamples:
         assert res.shape == (shots,)
         assert set(res.numpy()) == {-1, 1}
 
-    def test_sample_observables_non_differentiable(self):
-        """Test that sampled observables cannot be differentiated."""
-        shots = 100
-        dev = qml.device("default.qubit.tf", wires=2, shots=shots)
-
-        @qml.qnode(dev, diff_method="backprop", interface="tf")
-        def circuit(a):
-            qml.RX(a, wires=0)
-            return qml.sample(qml.PauliZ(0))
-
-        a = tf.Variable(0.54)
-
-        with tf.GradientTape() as tape:
-            res = circuit(a)
-
-        assert tape.gradient(res, a) is None
-
     def test_estimating_marginal_probability(self, tol):
         """Test that the probability of a subset of wires is accurately estimated."""
         dev = qml.device("default.qubit.tf", wires=2, shots=1000)
 
-        @qml.qnode(dev, diff_method="backprop", interface="tf")
+        @qml.qnode(dev, diff_method=None, interface="tf")
         def circuit():
             qml.PauliX(0)
             return qml.probs(wires=[0])
@@ -1361,7 +1344,7 @@ class TestSamples:
         """Test that the probability of a subset of wires is accurately estimated."""
         dev = qml.device("default.qubit.tf", wires=2, shots=1000)
 
-        @qml.qnode(dev, diff_method="backprop", interface="tf")
+        @qml.qnode(dev, diff_method=None, interface="tf")
         def circuit():
             qml.PauliX(0)
             qml.PauliX(1)
@@ -1379,7 +1362,7 @@ class TestSamples:
         of shots produces a numeric tensor"""
         dev = qml.device("default.qubit.tf", wires=3, shots=1000)
 
-        @qml.qnode(dev, diff_method="backprop", interface="tf")
+        @qml.qnode(dev, diff_method=None, interface="tf")
         def circuit(a, b):
             qml.RX(a, wires=[0])
             qml.RX(b, wires=[1])
@@ -1397,28 +1380,6 @@ class TestSamples:
         # expected = [tf.cos(a), tf.cos(a) * tf.cos(b)]
         # assert np.allclose(res, expected, atol=tol, rtol=0)
 
-    def test_estimating_expectation_values_not_differentiable(self, tol):
-        """Test that finite shots results in non-differentiable QNodes"""
-
-        dev = qml.device("default.qubit.tf", wires=3, shots=1000)
-
-        @qml.qnode(dev, diff_method="backprop", interface="tf")
-        def circuit(a, b):
-            qml.RX(a, wires=[0])
-            qml.RX(b, wires=[1])
-            qml.CNOT(wires=[0, 1])
-            return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
-
-        a = tf.Variable(0.543)
-        b = tf.Variable(0.43)
-
-        with tf.GradientTape() as tape:
-            res = circuit(a, b)
-
-        assert isinstance(res, tf.Tensor)
-        grad = tape.gradient(res, [a, b])
-        assert grad == [None, None]
-
 
 class TestHighLevelIntegration:
     """Tests for integration with higher level components of PennyLane."""
diff --git a/tests/devices/test_default_qubit_torch.py b/tests/devices/test_default_qubit_torch.py
index 851f20251..66485376e 100644
--- a/tests/devices/test_default_qubit_torch.py
+++ b/tests/devices/test_default_qubit_torch.py
@@ -1435,7 +1435,7 @@ class TestSamples:
         shots = 100
         dev = qml.device("default.qubit.torch", wires=2, shots=shots)
 
-        @qml.qnode(dev, diff_method="backprop", interface="torch")
+        @qml.qnode(dev, diff_method=None, interface="torch")
         def circuit(a):
             qml.RX(a, wires=0)
             return qml.sample(qml.PauliZ(0))
@@ -1451,7 +1451,7 @@ class TestSamples:
         """Test that the probability of a subset of wires is accurately estimated."""
         dev = qml.device("default.qubit.torch", wires=2, shots=1000)
 
-        @qml.qnode(dev, diff_method="backprop", interface="torch")
+        @qml.qnode(dev, diff_method=None, interface="torch")
         def circuit():
             qml.PauliX(0)
             return qml.probs(wires=[0])
@@ -1467,7 +1467,7 @@ class TestSamples:
         """Test that the probability of a subset of wires is accurately estimated."""
         dev = qml.device("default.qubit.torch", wires=2, shots=1000)
 
-        @qml.qnode(dev, diff_method="backprop", interface="torch")
+        @qml.qnode(dev, diff_method=None, interface="torch")
         def circuit():
             qml.PauliX(0)
             qml.PauliX(1)
@@ -1485,7 +1485,7 @@ class TestSamples:
         of shots produces a numeric tensor"""
         dev = qml.device("default.qubit.torch", wires=3, shots=1000)
 
-        @qml.qnode(dev, diff_method="backprop", interface="torch")
+        @qml.qnode(dev, diff_method=None, interface="torch")
         def circuit(a, b):
             qml.RX(a, wires=[0])
             qml.RX(b, wires=[1])
@@ -1503,26 +1503,6 @@ class TestSamples:
         # expected = [torch.cos(a), torch.cos(a) * torch.cos(b)]
         # assert np.allclose(res, expected, atol=tol, rtol=0)
 
-    def test_estimating_expectation_values_not_differentiable(self, tol):
-        """Test that finite shots results in non-differentiable QNodes"""
-
-        dev = qml.device("default.qubit.torch", wires=3, shots=1000)
-
-        @qml.qnode(dev, diff_method="backprop", interface="torch")
-        def circuit(a, b):
-            qml.RX(a, wires=[0])
-            qml.RX(b, wires=[1])
-            qml.CNOT(wires=[0, 1])
-            return qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliZ(1))
-
-        a = torch.tensor(0.543)
-        b = torch.tensor(0.43)
-
-        res = circuit(a, b)
-
-        with pytest.raises(RuntimeError):
-            res.backward()
-
 
 class TestHighLevelIntegration:
     """Tests for integration with higher level components of PennyLane."""
diff --git a/tests/tape/test_qnode.py b/tests/tape/test_qnode.py
index cd3b4b3c4..6d3ed79fa 100644
--- a/tests/tape/test_qnode.py
+++ b/tests/tape/test_qnode.py
@@ -70,6 +70,15 @@ class TestValidation:
         assert interface == "interface"
         assert device is dev
 
+    @pytest.mark.parametrize("interface", ("autograd", "torch", "tensorflow", "jax"))
+    def test_validate_backprop_method_finite_shots(self, interface):
+        """Tests that an error is raised for backpropagation with finite shots."""
+
+        dev = qml.device("default.qubit", wires=1, shots=3)
+
+        with pytest.raises(qml.QuantumFunctionError, match="Devices with finite shots"):
+            QNode._validate_backprop_method(dev, interface)
+
     def test_validate_backprop_method_invalid_device(self):
         """Test that the method for validating the backprop diff method
         tape raises an exception if the device does not support backprop."""