Repository: PennyLaneAI/pennylane (python)

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.

Pull request description:
improve error when insert tries to transform non-qwc observables
**Context:**
While the insert transform does not modify observables, it still needs to try to diagonalize them in case some rotation gates need to be added to the tape. This change only improves the error message - it might be unclear if you try to decorate an entire function with non-qwc observables why it is failing but some very similar circuits do not fail. This is the nature of our expansion/execution pipeline, so we can at least give users a clearer/more verbose heads-up.

**Description of the Change:**
When `circuit.expand` raises a QuantumFunctionError, it means the circuit has non-qwc observables. The added error message suggests that users apply the transform only to the gates in the circuit.

**Benefits:**
Clearer communication of the drawbacks of the insert transform.

**Possible Drawbacks:**
Sometimes doing what the error suggests (simply wrapping the gates and avoiding the measurements) will not be what they truly want. Example:
```pycon
>>> def circuit():
...     qml.Hadamard(0)
...     return qml.expval(qml.Identity(0)), qml.expval(qml.PauliX(0))
...
>>> with qml.tape.QuantumTape() as tape:
...     _ = circuit()
... 
>>> tape.expand(stop_at=lambda op: not hasattr(qml.templates, op.name) and not isinstance(op, qml.ops.Adjoint))
>>> print(f"operations: {tape.operations}\nmeasurements: {tape.measurements}")
operations: [Hadamard(wires=[0]), RY(-1.5707963267948966, wires=[0])]
measurements: [expval(Identity(wires=[0])), expval(PauliZ(wires=[0]))]
```
If a user was trying to insert noise to all gates in the circuit, but didn't expand the full circuit with measurements, the diagonalizing RY rotation would not get the noise it was supposed to get!

**Related GitHub Issues:**
Fixes #3103

<details><summary>Example experience decorating a circuit with non-qwc observables</summary>

```pycon
>>> import pennylane as qml
>>> dev = qml.device('default.qubit', wires=2, shots=10)
>>> @qml.qnode(dev)
... @qml.transforms.insert(qml.RX, 0.1, position="all")
... def circuit():
...     qml.Hadamard(0)
...     qml.CNOT([0,1])
...     return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))
... 
>>> circuit()
Traceback (most recent call last):
  File "/Users/matthews/src/github.com/PennyLaneAI/pennylane/pennylane/tape/tape.py", line 181, in expand_tape
    rotations, diag_obs = qml.pauli.diagonalize_qwc_pauli_words(
  File "/Users/matthews/src/github.com/PennyLaneAI/pennylane/pennylane/pauli/utils.py", line 1179, in diagonalize_qwc_pauli_words
    raise ValueError("The list of Pauli words are not qubit-wise commuting.")
ValueError: The list of Pauli words are not qubit-wise commuting.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/matthews/src/github.com/PennyLaneAI/pennylane/pennylane/transforms/insert_ops.py", line 214, in insert
    circuit = circuit.expand(
  File "/Users/matthews/src/github.com/PennyLaneAI/pennylane/pennylane/tape/qscript.py", line 1053, in expand
    new_script = qml.tape.tape.expand_tape(
  File "/Users/matthews/src/github.com/PennyLaneAI/pennylane/pennylane/tape/tape.py", line 197, in expand_tape
    raise qml.QuantumFunctionError(
pennylane.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]))]

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/matthews/src/github.com/PennyLaneAI/pennylane/pennylane/qnode.py", line 618, in __call__
    self.construct(args, kwargs)
  File "/Users/matthews/src/github.com/PennyLaneAI/pennylane/pennylane/qnode.py", line 528, in construct
    self._qfunc_output = self.func(*args, **kwargs)
  File "/Users/matthews/src/github.com/PennyLaneAI/pennylane/pennylane/transforms/qfunc_transforms.py", line 199, in internal_wrapper
    tape = tape_transform(tape, *transform_args, **transform_kwargs)
  File "/Users/matthews/src/github.com/PennyLaneAI/pennylane/pennylane/transforms/qfunc_transforms.py", line 167, in __call__
    self.transform_fn(tape, *args, **kwargs)
  File "/Users/matthews/src/github.com/PennyLaneAI/pennylane/pennylane/transforms/insert_ops.py", line 218, in insert
    raise qml.QuantumFunctionError(
pennylane.QuantumFunctionError: The insert transform cannot transform a circuit with non-commuting observables. Consider wrapping the gates in their own function and transforming only that function.
```
</details>

<details><summary>...and a corresponding follow-up action</summary>

```pycon
>>> @qml.transforms.insert(qml.RX, 0.1, position="all")
... def gates():
...     qml.Hadamard(0)
...     qml.CNOT([0,1])
... 
>>> @qml.qnode(dev)
... def circuit():
...     gates()
...     return qml.expval(qml.PauliX(0)), qml.expval(qml.PauliY(0)), qml.expval(qml.PauliZ(0))
... 
>>> print(qml.draw(circuit)())
0: ──H──RX(0.10)─╭●──RX(0.10)─┤  <X>  <Y>  <Z>
1: ──────────────╰X──RX(0.10)─┤
```
</details>

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index d571ddc3f..d72f80ad2 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -101,7 +101,11 @@
   [(#3302)](https://github.com/PennyLaneAI/pennylane/pull/3302)
 
 * A representation has been added to the `Molecule` class.
-  [#3364](https://github.com/PennyLaneAI/pennylane/pull/3364)
+  [(#3364)](https://github.com/PennyLaneAI/pennylane/pull/3364)
+
+* Add detail to the error message when the `insert` transform
+  fails to diagonalize non-qubit-wise-commuting observables.
+  [(#3381)](https://github.com/PennyLaneAI/pennylane/pull/3381)
 
 * Remove private `_wires` setter from the `Controlled.map_wires` method.
   [3405](https://github.com/PennyLaneAI/pennylane/pull/3405)
@@ -182,5 +186,6 @@ Edward Jiang
 Christina Lee
 Albert Mitjans Coma
 Romain Moyard
-David Wierichs
+Matthew Silverman
 Antal Száva
+David Wierichs
diff --git a/pennylane/tape/tape.py b/pennylane/tape/tape.py
index c7407f88f..f09e5935a 100644
--- 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
index c75619c16..979c4256b 100644
--- 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 insert(
     """
     # 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
index 08a8dd931..c61886bb1 100644
--- a/tests/transforms/test_insert_ops.py
+++ b/tests/transforms/test_insert_ops.py
@@ -472,7 +472,7 @@ def test_insert_template():
     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 test_insert_decorator_doesnt_cause_index_error():
         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