Repository: PennyLaneAI/pennylane

Issue:
Repeated applications of the torch interface result in an error when using templates
Consider the following code:

```python
import torch
import pennylane as qml

n_qubits = 2
dev = qml.device("default.qubit", wires=n_qubits)

def circuit(weights):
    qml.templates.BasicEntanglerLayers(weights, wires=range(n_qubits))
    return qml.expval(qml.PauliZ(wires=0))

qnode = qml.QNode(circuit, dev, interface="torch")

weights = torch.ones((3, 2))
qnode(weights)

qnode.to_torch()
```
Here, we are creating a QNode that contains `BasicEntanglerLayers` - an operation that was previously a tape (changed in https://github.com/PennyLaneAI/pennylane/pull/1138/). We then evaluate the QNode using `qnode(weights)`, which will cause the contained quantum tape to be converted to the Torch interface. Finally, we convert again to the torch interface in the last line. This results in the error:
```
TypeError: Cannot create a consistent method resolution
order (MRO) for bases TorchInterface, TorchQuantumTape
```

Instead, if we were to use a different circuit without templates, the error does not appear:
```python
def circuit(weights):
    for i in range(3):
        for j in range(2):
            qml.RY(weights[i, j], wires=j)
    return qml.expval(qml.PauliZ(wires=0))
```
Also, the circuit with `BasicEntanglerLayers` does not cause an error before #1138 was merged in. Hence, the migration from templates to operations has introduced this issue.

Could this be to do with the use of `expand()` by the templates? The `expand()` method returns a quantum tape, do we have to be careful about the interface?

#### Additional information

This issue was identified due to [failing tests](https://github.com/PennyLaneAI/qml/runs/2314856986?check_suite_focus=true#step:8:1464) for the [TorchLayer](https://pennylane.ai/qml/demos/tutorial_qnn_module_torch.html) tutorial. This tutorial creates multiple `TorchLayer`s from the same QNode, resulting in `to_torch()` being called multiple times. We don't typically expect `to_torch()` to be called explicitly by users.



Discussion:
@mariaschuld, adding you since this is related to the template-to-ops refactor. Although, I wonder if this is a problem more generally for operations that are expanded because of not being supported on the device :thinking:
Once this is fixed, it would be good to revert the changes of https://github.com/PennyLaneAI/qml/pull/247.
Interesting...

I checked the following three differences between templates (which are now operations) and "original" operations, but none seems to make a difference:

* In templates we directly define the `expand()` function, instead of defining `decomposition` which is called in the parent classes `expand()` function. But I changed the `BasicEntanglingLayers` to define a decomposition instead and the same error occurs. 
* Templates sit in another folder, but that does not seem to be the problem either - I moved `QFT` to the template folder, changed the `decompose` to an `expand` function and the code example runs fine... But for another template like `StronglyEntanglingLayers` the error persists. 
* Templates overwrite the `__init__` method of the `Operation` class. But adding this method to `QFT` does not cause an error, so it can't be the culprit either.

It must be a very strange edge case. 
Removing the QNode, to make the non-working example even more minimal:

```python
import torch
import pennylane as qml
from pennylane.interfaces.torch import TorchInterface

n_qubits = 2
dev = qml.device("default.qubit", wires=n_qubits)

weights = torch.ones((3,))

with TorchInterface.apply(qml.tape.QuantumTape()) as tape:
    qml.U3(*weights, wires=0)
    qml.expval(qml.PauliZ(wires=0))

tape = tape.expand()

res = tape.execute(dev)
print(res)

TorchInterface.apply(tape)  # this line errors with the same error message as above
res = tape.execute(dev)
print(res)
```

Resolving patch:
diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py
--- a/pennylane/tape/tape.py
+++ b/pennylane/tape/tape.py
@@ -153,6 +153,7 @@ def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False):
         stop_at = lambda obj: False
 
     new_tape = tape.__class__()
+    new_tape.__bare__ = getattr(tape, "__bare__", tape.__class__)
 
     # Check for observables acting on the same wire. If present, observables must be
     # qubit-wise commuting Pauli words. In this case, the tape is expanded with joint


Test patch:
diff --git a/tests/interfaces/test_tape_torch.py b/tests/interfaces/test_tape_torch.py
--- a/tests/interfaces/test_tape_torch.py
+++ b/tests/interfaces/test_tape_torch.py
@@ -453,3 +453,24 @@ def test_complex_min_version(self, monkeypatch):
             m.setattr(qml.interfaces.torch, "COMPLEX_SUPPORT", False)
             with pytest.raises(qml.QuantumFunctionError, match=r"Version 1\.6\.0 or above of PyTorch"):
                 TorchInterface.apply(JacobianTape(), dtype=torch.complex128)
+
+    def test_repeated_application_after_expand(self, tol):
+        """Test that the Torch interface continues to work after
+        tape expansions, and repeated torch application"""
+        n_qubits = 2
+        dev = qml.device("default.qubit", wires=n_qubits)
+
+        weights = torch.ones((3,))
+
+        with TorchInterface.apply(qml.tape.QuantumTape()) as tape:
+            qml.U3(*weights, wires=0)
+            qml.expval(qml.PauliZ(wires=0))
+
+        tape = tape.expand()
+
+        res1 = tape.execute(dev)
+
+        TorchInterface.apply(tape)
+        res2 = tape.execute(dev)
+
+        assert np.allclose(res1, res2, atol=tol, rtol=0)