Repository: PennyLaneAI/pennylane (python)

Issue:
GradientDescentOptimizer fails when `inv()` is used in circuit
Description of the issue

An error is thrown when trying to perform gradient descent on a simple circuit containing an inverse gate

* *Expected behavior:* circuits with inverse gates can be optimized without error

* *Actual behavior:* GradientDescentOptimizer fails when `inv()` is used in circuit

* *Reproduces how often:* every time

* *System information:* 

```
Name: PennyLane
Version: 0.14.0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/XanaduAI/pennylane
Author: None
Author-email: None
License: Apache License 2.0
Location: /home/nathan/anaconda3/lib/python3.7/site-packages
Requires: autograd, numpy, scipy, toml, networkx, semantic-version, appdirs
Required-by: PennyLane-SF, PennyLane-Cirq, PennyLane-Orquestra
Platform info:           Linux-5.4.0-65-generic-x86_64-with-debian-bullseye-sid
Python version:          3.7.6
Numpy version:           1.18.1
Scipy version:           1.4.1
Installed devices:
- default.gaussian (PennyLane-0.14.0)
- default.mixed (PennyLane-0.14.0)
- default.qubit (PennyLane-0.14.0)
- default.qubit.autograd (PennyLane-0.14.0)
- default.qubit.jax (PennyLane-0.14.0)
- default.qubit.tf (PennyLane-0.14.0)
- default.tensor (PennyLane-0.14.0)
- default.tensor.tf (PennyLane-0.14.0)
- strawberryfields.fock (PennyLane-SF-0.12.0)
- strawberryfields.gaussian (PennyLane-SF-0.12.0)
- strawberryfields.gbs (PennyLane-SF-0.12.0)
- strawberryfields.remote (PennyLane-SF-0.12.0)
- strawberryfields.tf (PennyLane-SF-0.12.0)
- cirq.mixedsimulator (PennyLane-Cirq-0.10.0.dev0)
- cirq.pasqal (PennyLane-Cirq-0.10.0.dev0)
- cirq.simulator (PennyLane-Cirq-0.10.0.dev0)
- orquestra.forest (PennyLane-Orquestra-0.13.0.dev0)
- orquestra.ibmq (PennyLane-Orquestra-0.13.0.dev0)
- orquestra.qiskit (PennyLane-Orquestra-0.13.0.dev0)
- orquestra.qulacs (PennyLane-Orquestra-0.13.0.dev0)

```

#### Source code and tracebacks
```
>>> import pennylane as qml
>>> dev = qml.device('default.qubit', wires=2)
>>> @qml.qnode(dev)
... def circuit(x):
...     qml.RZ(x, wires=1).inv()
...     return qml.expval(qml.PauliZ(0))
... 
>>> circuit(0)
tensor(1., requires_grad=True)
>>> opt = qml.GradientDescentOptimizer(0.1)
>>> x0 = 0.5
>>> opt.step(circuit, x0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/optimize/gradient_descent.py", line 98, in step
    g, _ = self.compute_grad(objective_fn, args, kwargs, grad_fn=grad_fn)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/optimize/gradient_descent.py", line 127, in compute_grad
    grad = g(*args, **kwargs)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/_grad.py", line 96, in __call__
    grad_value, ans = self._get_grad_fn(args)(*args, **kwargs)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/autograd/wrap_util.py", line 20, in nary_f
    return unary_operator(unary_f, x, *nary_op_args, **nary_op_kwargs)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/_grad.py", line 113, in _grad_with_forward
    vjp, ans = _make_vjp(fun, x)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/autograd/core.py", line 10, in make_vjp
    end_value, end_node =  trace(start_node, fun, x)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/autograd/tracer.py", line 10, in trace
    end_box = fun(start_box)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/autograd/wrap_util.py", line 15, in unary_f
    return fun(*subargs, **kwargs)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/tape/qnode.py", line 521, in __call__
    res = self.qtape.execute(device=self.device)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/tape/tapes/tape.py", line 1070, in execute
    return self._execute(params, device=device)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/tape/tapes/tape.py", line 1101, in execute_device
    res = device.execute(self)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/_qubit_device.py", line 204, in execute
    self.apply(circuit.operations, rotations=circuit.diagonalizing_gates, **kwargs)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/devices/default_qubit.py", line 188, in apply
    self._state = self._apply_operation(self._state, operation)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/devices/default_qubit.py", line 213, in _apply_operation
    matrix = self._get_unitary_matrix(operation)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/devices/default_qubit_autograd.py", line 167, in _get_unitary_matrix
    return unitary.eigvals
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/operation.py", line 878, in eigvals
    return super().eigvals
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/operation.py", line 753, in eigvals
    op_eigvals = self._eigvals(*self.parameters)
  File "/home/nathan/anaconda3/lib/python3.7/site-packages/pennylane/ops/qubit.py", line 678, in _eigvals
    p = cmath.exp(-0.5j * theta)
TypeError: must be real number, not ArrayBox

```


Pull request description:
 Fixes a bug where inverse operations could not be differentiated using backprop
**Context:**

* A hangover from the old device API, where operations would be serialized to strings before sending them to the device, is that  inverting a gate mutates the operation name; the operation name becomes `OriginalName.inv`.

* `default.qubit.autograd`, `default.qubit.tf`, and `default.qubit.jax`, when using backpropagation as the differentiation method, need to ensure that all mathematical operations use autograd/TF respectively.

These two devices were not taking into account operation names that might have been appended with `'.inv'`, and thus inverted parametric operations could not be differentiated in backprop mode.

This was missed in previous releases for a couple of reasons:

* The shared integration tests were testing *evaluation* of inverse operations, but not gradients.
* The device unit tests were testing various circuits under backpropgation, but none included inverse gates.
* Finally, since the default diff method was parameter-shift, end-users were unlikely to encounter this bug.

**Description of the Change:**

* Ensures that inverse operations are taken into account directly in `default.qubit.autograd`, `default.qubit.tf`, and `default.qubit.jax`.
* Adds tests for backpropagation with inverse gates.

**Benefits:**

* Circuits with inverse gates can be differentiated in backprop mode.

**Possible Drawbacks:**

* Now that operations are no longer serialized before being passed to devices, we should refactor how inverse operations are handled, and make sure they do not mutate the operation.

**Related GitHub Issues:** Fixes #1071


Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index e95daf3f2..087f4a117 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -64,10 +64,14 @@
 
 <h3>Bug fixes</h3>
 
+* Fixes a bug where inverse operations could not be differentiated
+  using backpropagation on `default.qubit`.
+  [(#1072)](https://github.com/PennyLaneAI/pennylane/pull/1072)
+
 * If only one argument to the function `qml.grad` has the `requires_grad` attribute
   set to True, then the returned gradient will be a NumPy array, rather than a
   tuple of length 1.
-  [(#)](https://github.com/PennyLaneAI/pennylane/pull/)
+  [(#1067)](https://github.com/PennyLaneAI/pennylane/pull/1067)
 
 <h3>Documentation</h3>
 
@@ -75,7 +79,7 @@
 
 This release contains contributions from (in alphabetical order):
 
-Thomas Bromley, Josh Izaac, Daniel Polatajko, Chase Roberts
+Thomas Bromley, Josh Izaac, Daniel Polatajko, Chase Roberts.
 
 # Release 0.14.0 (current release)
 
diff --git a/pennylane/devices/default_qubit_autograd.py b/pennylane/devices/default_qubit_autograd.py
index 6ddf70344..fc2cf5531 100644
--- a/pennylane/devices/default_qubit_autograd.py
+++ b/pennylane/devices/default_qubit_autograd.py
@@ -158,11 +158,18 @@ class DefaultQubitAutograd(DefaultQubit):
             the unitary in the computational basis, or, in the case of a diagonal unitary,
             a 1D array representing the matrix diagonal.
         """
-        op_name = unitary.name
+        op_name = unitary.name.split(".inv")[0]
+
         if op_name in self.parametric_ops:
             if op_name == "MultiRZ":
-                return self.parametric_ops[unitary.name](*unitary.parameters, len(unitary.wires))
-            return self.parametric_ops[unitary.name](*unitary.parameters)
+                mat = self.parametric_ops[op_name](*unitary.parameters, len(unitary.wires))
+            else:
+                mat = self.parametric_ops[op_name](*unitary.parameters)
+
+            if unitary.inverse:
+                mat = self._transpose(self._conj(mat))
+
+            return mat
 
         if isinstance(unitary, DiagonalOperation):
             return unitary.eigvals
diff --git a/pennylane/devices/default_qubit_jax.py b/pennylane/devices/default_qubit_jax.py
index 6759c45a5..cd51195aa 100644
--- a/pennylane/devices/default_qubit_jax.py
+++ b/pennylane/devices/default_qubit_jax.py
@@ -205,11 +205,18 @@ class DefaultQubitJax(DefaultQubit):
             the unitary in the computational basis, or, in the case of a diagonal unitary,
             a 1D array representing the matrix diagonal.
         """
-        op_name = unitary.name
+        op_name = unitary.name.split(".inv")[0]
+
         if op_name in self.parametric_ops:
             if op_name == "MultiRZ":
-                return self.parametric_ops[unitary.name](*unitary.parameters, len(unitary.wires))
-            return self.parametric_ops[unitary.name](*unitary.parameters)
+                mat = self.parametric_ops[op_name](*unitary.parameters, len(unitary.wires))
+            else:
+                mat = self.parametric_ops[op_name](*unitary.parameters)
+
+            if unitary.inverse:
+                mat = self._transpose(self._conj(mat))
+
+            return mat
 
         if isinstance(unitary, DiagonalOperation):
             return unitary.eigvals
diff --git a/pennylane/devices/default_qubit_tf.py b/pennylane/devices/default_qubit_tf.py
index 7a3828fb5..c80f20e0e 100644
--- a/pennylane/devices/default_qubit_tf.py
+++ b/pennylane/devices/default_qubit_tf.py
@@ -215,10 +215,18 @@ class DefaultQubitTF(DefaultQubit):
             the return type will be a ``np.ndarray``. For parametric unitaries, a ``tf.Tensor``
             object will be returned.
         """
-        if unitary.name in self.parametric_ops:
-            if unitary.name == "MultiRZ":
-                return self.parametric_ops[unitary.name](unitary.parameters, len(unitary.wires))
-            return self.parametric_ops[unitary.name](*unitary.parameters)
+        op_name = unitary.name.split(".inv")[0]
+
+        if op_name in self.parametric_ops:
+            if op_name == "MultiRZ":
+                mat = self.parametric_ops[op_name](*unitary.parameters, len(unitary.wires))
+            else:
+                mat = self.parametric_ops[op_name](*unitary.parameters)
+
+            if unitary.inverse:
+                mat = self._transpose(self._conj(mat))
+
+            return mat
 
         if isinstance(unitary, DiagonalOperation):
             return unitary.eigvals


Test patch:
diff --git a/tests/devices/test_default_qubit_autograd.py b/tests/devices/test_default_qubit_autograd.py
index ecd44edfd..c9b063d0b 100644
--- a/tests/devices/test_default_qubit_autograd.py
+++ b/tests/devices/test_default_qubit_autograd.py
@@ -395,6 +395,23 @@ class TestOps:
         res = qml.jacobian(circuit)(param)
         assert np.allclose(res, np.zeros(wires **2))
 
+    def test_inverse_operation_jacobian_backprop(self, tol):
+        """Test that inverse operations work in backprop
+        mode"""
+        dev = qml.device('default.qubit.autograd', wires=1)
+
+        @qml.qnode(dev, diff_method="backprop")
+        def circuit(param):
+            qml.RY(param, wires=0).inv()
+            return qml.expval(qml.PauliX(0))
+
+        x = 0.3
+        res = circuit(x)
+        assert np.allclose(res, -np.sin(x), atol=tol, rtol=0)
+
+        grad = qml.grad(circuit)(x)
+        assert np.allclose(grad, -np.cos(x), atol=tol, rtol=0)
+
     def test_full_subsystem(self, mocker):
         """Test applying a state vector to the full subsystem"""
         dev = DefaultQubitAutograd(wires=['a', 'b', 'c'])
diff --git a/tests/devices/test_default_qubit_jax.py b/tests/devices/test_default_qubit_jax.py
index 04b0ffe67..6a85aa6bc 100644
--- a/tests/devices/test_default_qubit_jax.py
+++ b/tests/devices/test_default_qubit_jax.py
@@ -455,6 +455,23 @@ class TestOps:
         res = jacobian_transform(circuit)(param)
         assert jnp.allclose(res, jnp.zeros(wires ** 2))
 
+    def test_inverse_operation_jacobian_backprop(self, tol):
+        """Test that inverse operations work in backprop
+        mode"""
+        dev = qml.device('default.qubit.jax', wires=1)
+
+        @qml.qnode(dev, diff_method="backprop", interface="jax")
+        def circuit(param):
+            qml.RY(param, wires=0).inv()
+            return qml.expval(qml.PauliX(0))
+
+        x = 0.3
+        res = circuit(x)
+        assert np.allclose(res, -np.sin(x), atol=tol, rtol=0)
+
+        grad = jax.grad(lambda a: circuit(a).reshape(()))(x)
+        assert np.allclose(grad, -np.cos(x), atol=tol, rtol=0)
+
     def test_full_subsystem(self, mocker):
         """Test applying a state vector to the full subsystem"""
         dev = DefaultQubitJax(wires=["a", "b", "c"])
diff --git a/tests/devices/test_default_qubit_tf.py b/tests/devices/test_default_qubit_tf.py
index bc6847175..c1dd80234 100644
--- a/tests/devices/test_default_qubit_tf.py
+++ b/tests/devices/test_default_qubit_tf.py
@@ -1251,6 +1251,26 @@ class TestPassthruIntegration:
         )
         assert np.allclose(res.numpy(), expected_grad, atol=tol, rtol=0)
 
+    def test_inverse_operation_jacobian_backprop(self, tol):
+        """Test that inverse operations work in backprop
+        mode"""
+        dev = qml.device('default.qubit.tf', wires=1)
+
+        @qml.qnode(dev, diff_method="backprop", interface="tf")
+        def circuit(param):
+            qml.RY(param, wires=0).inv()
+            return qml.expval(qml.PauliX(0))
+
+        x = tf.Variable(0.3)
+
+        with tf.GradientTape() as tape:
+            res = circuit(x)
+
+        assert np.allclose(res, -tf.sin(x), atol=tol, rtol=0)
+
+        grad = tape.gradient(res, x)
+        assert np.allclose(grad, -tf.cos(x), atol=tol, rtol=0)
+
     @pytest.mark.parametrize("interface", ["autograd", "torch"])
     def test_error_backprop_wrong_interface(self, interface, tol):
         """Tests that an error is raised if diff_method='backprop' but not using