Repository: PennyLaneAI/pennylane

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

```



Discussion:
Thanks for catching this Nathan!

I've dug into it, and the issue seems to be that `default.qubit.autograd` and `default.qubit.tf` do not go through the 'autograd'/'tf' pipeline for inverse operations; hence why circuit evaluation works, but gradients do not.

At the crux of the issue is the fact that inverting a gate mutates an operation; the operation name becomes `OriginalName.inv`. This was a workaround years ago when we had to support a device API that passed operations as strings, that we knew was substandard at the time. We should definitely remove this behaviour now that circuits objects are passed fully to the device.

Resolving patch:
diff --git a/pennylane/devices/default_qubit_autograd.py b/pennylane/devices/default_qubit_autograd.py
--- a/pennylane/devices/default_qubit_autograd.py
+++ b/pennylane/devices/default_qubit_autograd.py
@@ -158,11 +158,18 @@ def _get_unitary_matrix(self, unitary):
             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
--- a/pennylane/devices/default_qubit_jax.py
+++ b/pennylane/devices/default_qubit_jax.py
@@ -205,11 +205,18 @@ def _get_unitary_matrix(self, unitary):
             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
--- a/pennylane/devices/default_qubit_tf.py
+++ b/pennylane/devices/default_qubit_tf.py
@@ -215,10 +215,18 @@ def _get_unitary_matrix(self, unitary):
             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
--- a/tests/devices/test_default_qubit_autograd.py
+++ b/tests/devices/test_default_qubit_autograd.py
@@ -395,6 +395,23 @@ def circuit(param):
         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
--- a/tests/devices/test_default_qubit_jax.py
+++ b/tests/devices/test_default_qubit_jax.py
@@ -455,6 +455,23 @@ def circuit(param):
         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
--- a/tests/devices/test_default_qubit_tf.py
+++ b/tests/devices/test_default_qubit_tf.py
@@ -1251,6 +1251,26 @@ def cost(params):
         )
         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