Repository: PennyLaneAI/pennylane

Issue:
[BUG] Inline transforms vs. Decorator transforms
### Expected behavior

This  script:

```
def circuit(circuit_param):
    qml.RY(circuit_param, wires=0)
    qml.Hadamard(wires=0)
    qml.T(wires=0)

def noise(noise_param, wires):
    qml.CRX(noise_param, wires=[0,1])
    qml.CNOT(wires=[1,0])
    
dev = qml.device("default.mixed", wires=2)

noise_param = 0.3
    
@qml.qnode(dev)
@qml.transforms.insert(noise, noise_param, position="all")
def noisy_circuit(circuit_param):
        
    circuit(circuit_param)
    
    return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))

noisy_circuit(0.4)
```
And this one
```
def circuit(circuit_param):
    qml.RY(circuit_param, wires=0)
    qml.Hadamard(wires=0)
    qml.T(wires=0)

def noise(noise_param, wires):
    qml.CRX(noise_param, wires=[0,1])
    qml.CNOT(wires=[1,0])
    
dev = qml.device("default.mixed", wires=2)

noise_param = 0.3
    
@qml.qnode(dev)
def noisy_circuit(circuit_param):
        
    qml.transforms.insert(noise, noise_param, position="all")(circuit)(circuit_param)
    
    return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))

noisy_circuit(0.4)
```
should both return `array([0.64497588, 0.61505021, 0.40503904])`

### Actual behavior

The second script works, but the first one returns errors.

### Additional information

I know this is an issue with how tapes are handled in either case. Please reconsider how this interacts with users.

### Source code

```shell
def circuit(circuit_param):
    qml.RY(circuit_param, wires=0)
    qml.Hadamard(wires=0)
    qml.T(wires=0)

def noise(noise_param, wires):
    qml.CRX(noise_param, wires=[0,1])
    qml.CNOT(wires=[1,0])
    
dev = qml.device("default.mixed", wires=2)

noise_param = 0.3
    
@qml.qnode(dev)
@qml.transforms.insert(noise, noise_param, position="all")
def noisy_circuit(circuit_param):
        
    circuit(circuit_param)
    
    return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))

noisy_circuit(0.4)
```


### Tracebacks

```shell
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~\anaconda3\lib\site-packages\pennylane\tape\tape.py in expand_tape(tape, depth, stop_at, expand_measurements)
    156             try:
--> 157                 rotations, diag_obs = qml.grouping.diagonalize_qwc_pauli_words(
    158                     tape._obs_sharing_wires

~\anaconda3\lib\site-packages\pennylane\grouping\transformations.py in diagonalize_qwc_pauli_words(qwc_grouping)
    139     if not are_pauli_words_qwc(qwc_grouping):
--> 140         raise ValueError("The list of Pauli words are not qubit-wise commuting.")
    141 

ValueError: The list of Pauli words are not qubit-wise commuting.

The above exception was the direct cause of the following exception:

QuantumFunctionError                      Traceback (most recent call last)
~\anaconda3\lib\site-packages\pennylane\transforms\qfunc_transforms.py in __call__(self, tape, *args, **kwargs)
    169         with tape_class() as new_tape:
--> 170             self.transform_fn(tape, *args, **kwargs)
    171 

~\anaconda3\lib\site-packages\pennylane\transforms\insert_ops.py in insert(circuit, op, op_args, position, before)
    212     # TODO: change this to be cleaner and more robust
--> 213     circuit = circuit.expand(
    214         stop_at=lambda op: not hasattr(qml.templates, op.name) and not isinstance(op, Adjoint)

~\anaconda3\lib\site-packages\pennylane\tape\tape.py in expand(self, depth, stop_at, expand_measurements)
    686         """
--> 687         new_tape = expand_tape(
    688             self, depth=depth, stop_at=stop_at, expand_measurements=expand_measurements

~\anaconda3\lib\site-packages\pennylane\tape\tape.py in expand_tape(tape, depth, stop_at, expand_measurements)
    172 
--> 173                 raise qml.QuantumFunctionError(
    174                     "Only observables that are qubit-wise commuting "

QuantumFunctionError: Only observables that are qubit-wise commuting Pauli words can be returned on the same wire, some of the following measurements do not commute:
[expval(PauliX(wires=[0])), expval(PauliY(wires=[0])), expval(PauliZ(wires=[0]))]

During handling of the above exception, another exception occurred:

IndexError                                Traceback (most recent call last)
~\anaconda3\lib\site-packages\pennylane\qnode.py in construct(self, args, kwargs)
    525         with self.tape:
--> 526             self._qfunc_output = self.func(*args, **kwargs)
    527         self._tape._qfunc_output = self._qfunc_output

~\anaconda3\lib\site-packages\pennylane\transforms\qfunc_transforms.py in internal_wrapper(*args, **kwargs)
    201         tape = make_tape(fn)(*args, **kwargs)
--> 202         tape = tape_transform(tape, *transform_args, **transform_kwargs)
    203 

~\anaconda3\lib\site-packages\pennylane\transforms\qfunc_transforms.py in __call__(self, tape, *args, **kwargs)
    169         with tape_class() as new_tape:
--> 170             self.transform_fn(tape, *args, **kwargs)
    171 

~\anaconda3\lib\site-packages\pennylane\tape\tape.py in __exit__(self, exception_type, exception_value, traceback)
    389         try:
--> 390             super().__exit__(exception_type, exception_value, traceback)
    391             # After other optimizations in #2963, #2986 and follow-up work, we should check whether

~\anaconda3\lib\site-packages\pennylane\queuing.py in __exit__(self, exception_type, exception_value, traceback)
    127         """Remove this instance from the global list of active contexts."""
--> 128         QueuingContext._active_contexts.pop()
    129 

IndexError: pop from an empty deque

During handling of the above exception, another exception occurred:

IndexError                                Traceback (most recent call last)
<ipython-input-33-37d3113aba14> in <module>
     20     return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))
     21 
---> 22 noisy_circuit(0.4)

~\anaconda3\lib\site-packages\pennylane\qnode.py in __call__(self, *args, **kwargs)
    609 
    610         # construct the tape
--> 611         self.construct(args, kwargs)
    612 
    613         cache = self.execute_kwargs.get("cache", False)

~\anaconda3\lib\site-packages\pennylane\qnode.py in construct(self, args, kwargs)
    524 
    525         with self.tape:
--> 526             self._qfunc_output = self.func(*args, **kwargs)
    527         self._tape._qfunc_output = self._qfunc_output
    528 

~\anaconda3\lib\site-packages\pennylane\tape\tape.py in __exit__(self, exception_type, exception_value, traceback)
    388     def __exit__(self, exception_type, exception_value, traceback):
    389         try:
--> 390             super().__exit__(exception_type, exception_value, traceback)
    391             # After other optimizations in #2963, #2986 and follow-up work, we should check whether
    392             # calling `_process_queue` only if there is no `exception_type` saves time. This would

~\anaconda3\lib\site-packages\pennylane\queuing.py in __exit__(self, exception_type, exception_value, traceback)
    126     def __exit__(self, exception_type, exception_value, traceback):
    127         """Remove this instance from the global list of active contexts."""
--> 128         QueuingContext._active_contexts.pop()
    129 
    130     @abc.abstractmethod

IndexError: pop from an empty deque
```


### System information

```shell
Name: PennyLane
Version: 0.26.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: c:\users\alvaro\anaconda3\lib\site-packages
Requires: numpy, appdirs, cachetools, retworkx, toml, autograd, semantic-version, pennylane-lightning, autoray, scipy, networkx
Required-by: PennyLane-SF, pennylane-qulacs, PennyLane-Qchem, PennyLane-Lightning

Platform info:           Windows-10-10.0.19041-SP0
Python version:          3.8.8
Numpy version:           1.22.1
Scipy version:           1.6.2
Installed devices:
- default.gaussian (PennyLane-0.26.0)
- default.mixed (PennyLane-0.26.0)
- default.qubit (PennyLane-0.26.0)
- default.qubit.autograd (PennyLane-0.26.0)
- default.qubit.jax (PennyLane-0.26.0)
- default.qubit.tf (PennyLane-0.26.0)
- default.qubit.torch (PennyLane-0.26.0)
- default.qutrit (PennyLane-0.26.0)
- strawberryfields.fock (PennyLane-SF-0.19.0)
- strawberryfields.gaussian (PennyLane-SF-0.19.0)
- strawberryfields.gbs (PennyLane-SF-0.19.0)
- strawberryfields.remote (PennyLane-SF-0.19.0)
- strawberryfields.tf (PennyLane-SF-0.19.0)
- qulacs.simulator (pennylane-qulacs-0.16.0)
- lightning.qubit (PennyLane-Lightning-0.26.0)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.


Discussion:
Hi @alvaro-at-xanadu, thanks for reporting this! :tada: It's on our radar, we had a discussion about it and we'll be rolling a fix out. :+1: 

Resolving patch:
diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py
--- a/pennylane/tape/tape.py
+++ b/pennylane/tape/tape.py
@@ -94,6 +94,9 @@ def expand_tape(qscript, depth=1, stop_at=None, expand_measurements=False):
     Returns:
         QuantumScript: The expanded version of ``qscript``.
 
+    Raises:
+        QuantumFunctionError: if some observables in the qscript are not qubit-wise commuting
+
     **Example**
 
     Consider the following nested tape:
diff --git a/pennylane/transforms/insert_ops.py b/pennylane/transforms/insert_ops.py
--- a/pennylane/transforms/insert_ops.py
+++ b/pennylane/transforms/insert_ops.py
@@ -90,6 +90,7 @@ def insert(
         device which will transform circuits before execution
 
     Raises:
+        QuantumFunctionError: if some observables in the qscript are not qubit-wise commuting
         ValueError: if a single operation acting on multiple wires is passed to ``op``
         ValueError: if the requested ``position`` argument is not ``'start'``, ``'end'`` or
             ``'all'`` OR PennyLane Operation
@@ -210,9 +211,15 @@ def f(w, x, y, z):
     """
     # decompose templates and their adjoints (which fixes a bug in the tutorial_error_mitigation demo)
     # TODO: change this to be cleaner and more robust
-    circuit = circuit.expand(
-        stop_at=lambda op: not hasattr(qml.templates, op.name) and not isinstance(op, Adjoint)
-    )
+    try:
+        circuit = circuit.expand(
+            stop_at=lambda op: not hasattr(qml.templates, op.name) and not isinstance(op, Adjoint)
+        )
+    except qml.QuantumFunctionError as e:
+        raise qml.QuantumFunctionError(
+            "The insert transform cannot transform a circuit measuring non-commuting observables. "
+            "Consider wrapping the gates in their own function and transforming only that function."
+        ) from e
 
     if not isinstance(op, FunctionType) and op.num_wires != 1:
         raise ValueError("Only single-qubit operations can be inserted into the circuit")


Test patch:
diff --git a/tests/transforms/test_insert_ops.py b/tests/transforms/test_insert_ops.py
--- a/tests/transforms/test_insert_ops.py
+++ b/tests/transforms/test_insert_ops.py
@@ -472,7 +472,7 @@ def f2(w1, w2):
     assert np.allclose(f1(w1, w2), f2(w1, w2))
 
 
-def test_insert_decorator_doesnt_cause_index_error():
+def test_insert_decorator_causes_custom_insert_error_non_qwc_obs():
     """Test that the insert transform catches and reports errors from the enclosed function."""
 
     def noise(noise_param, wires):
@@ -490,7 +490,8 @@ def noisy_circuit(circuit_param):
         return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))
 
     # This tape's expansion fails, but shouldn't cause a downstream IndexError. See issue #3103
-    with pytest.raises(Exception, match="Only observables that are qubit-wise commuting") as e:
+    with pytest.raises(
+        qml.QuantumFunctionError,
+        match="The insert transform cannot transform a circuit measuring non-commuting observables",
+    ):
         noisy_circuit(0.4)
-    assert e.type != IndexError
-    assert e.type == qml.QuantumFunctionError