Repository: PennyLaneAI/pennylane

Issue:
[Bug] `PauliSentence.to_mat(wire_order)` cannot handle Identities with wires
```python
PauliSentence({PauliWord({}): 1.}).to_mat([0, 1])
```

```pycon
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[4], line 1
----> 1 PauliSentence({PauliWord({}): 1.}).to_mat([0, 1])

File ~/Xanadu/pennylane/pennylane/pauli/pauli_arithmetic.py:876, in PauliSentence.to_mat(self, wire_order, format, buffer_size)
    864 mats_and_wires_gen = (
    865     (
    866         coeff * pw.to_mat(wire_order=_pw_wires(pw.wires), format=format),
   (...)
    869     for pw, coeff in self.items()
    870 )
    872 reduced_mat, result_wire_order = math.reduce_matrices(
    873     mats_and_wires_gen=mats_and_wires_gen, reduce_func=math.add
    874 )
--> 876 return math.expand_matrix(reduced_mat, result_wire_order, wire_order=wire_order)

File ~/Xanadu/pennylane/pennylane/math/matrix_manipulation.py:135, in expand_matrix(mat, wires, wire_order, sparse_format)
    131 # get a subset of `wire_order` values that contain all wire labels inside `wires` argument
    132 # e.g. wire_order = [0, 1, 2, 3, 4]; wires = [3, 0, 2]
    133 # --> subset_wire_order = [0, 1, 2, 3]; expanded_wires = [3, 0, 2, 1]
    134 wire_indices = [wire_order.index(wire) for wire in wires]
--> 135 subset_wire_order = wire_order[min(wire_indices) : max(wire_indices) + 1]
    136 wire_difference = list(set(subset_wire_order) - set(wires))
    137 expanded_wires = wires + wire_difference

ValueError: min() arg is an empty sequence
```


Discussion:
Hello @Qottmann,

I'm interested in contributing to this issue. I've recently started working with PennyLane and am eager to help out. Could I work on this one?

Thank you!
@Tarun-Kumar07 cool that you are interested in contributing :) I am not sure how beginner friendly this issue is, but we can try with no guarantee for success :)

`PauliWord` is a special class handling pauli operators X, Y, Z on different wires. It is a wrapper for a dictionary where the keys are the wires and the values are strings representing the X, Y, and Z operators. For example `PauliWord({0:"X", 1:"Y"})` represents the operator $\sigma^x_0 \otimes \sigma^y_1$. You can also explicitly point out identities like `PauliWord({0:"X", 1:"I", 2:"Y"})` for $\sigma^x_0 \otimes I_1 \otimes \sigma^y_2$, but the identities are actually unnecessary, and because of that, stripped from the `PauliWord` instance upon initilization.

Now, an empty `PauliWord({})` just represents the identity (mathematically speaking, its the multiplicative Identity.
You can get its matrix for different dimensions by passing the `wire_order` argument:
```
>>> PauliWord({}).to_mat([0])
array([[1., 0.],
       [0., 1.]])
>>> PauliWord({}).to_mat([0,1])
array([[1., 0., 0., 0.],
       [0., 1., 0., 0.],
       [0., 0., 1., 0.],
       [0., 0., 0., 1.]])
```

A `PauliSentence` represents a linear combination of `PauliWord` operators. It is also a wrapped dicitonary, with keys being `PauliWord` instances and the values being the real or complex coefficients.
An empty `PauliSentence({})` just represents 0 (mathematically, the additive identity).
A pauli sentence with a PauliWord, is just the Identity with a real (or complex) coefficient.
However, the `to_mat()` method is currently broken. What I would expect is the following (which defines the goal of this issue).
```
>>> PauliSentence({PauliWord({}): 2.}).to_mat([0])
array([[2., 0.],
       [0., 2.]])
>>> PauliSentence({PauliWord({}): 3.5}).to_mat([0, 1])
array([[3.5, 0., 0., 0.],
       [0., 3.5, 0., 0.],
       [0., 0., 3.5, 0.],
       [0., 0., 0., 3.5]])
```
Hello @Qottmann,

Thank you for explaining the issue. I'm interested in tackling this bug to expand my understanding of PennyLane. It's a great opportunity for me to learn and contribute. Could I take on this task?

Thank you!
In my mind, a fix would involve updating this inner function:

https://github.com/PennyLaneAI/pennylane/blob/58ad1dfdc661fb58034225743503167e3006d86f/pennylane/pauli/pauli_arithmetic.py#L829

When the `PauliWord` itself doesn't have wires, it falls back to pulling a wire from the `PauliSentence` object instead.  But in this case, the `PauliSentence` doesn't have any wires either.

I could try pulling a random wire from the `wire_order` instead of from the `PauliSentence` instance and see if that fixes the issue and still passes tests.
Hey @Qottmann, would you mind assigning this bug to me, please?
@albi3ro, the fix worked well. I just need a bit of time to understand the code better. 
I'll aim to submit a PR by tomorrow. 
Thank you for your assistance!

Resolving patch:
diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py
--- a/pennylane/pauli/pauli_arithmetic.py
+++ b/pennylane/pauli/pauli_arithmetic.py
@@ -832,7 +832,16 @@ def _pw_wires(w: Iterable) -> Wires:
             the PauliWord is empty ({}), choose any arbitrary wire from the
             PauliSentence it is composed in.
             """
-            return w or Wires(self.wires[0]) if self.wires else self.wires
+            if w:
+                # PauliWord is not empty, so we can use its wires
+                return Wires(w)
+
+            if wire_order:
+                # PauliWord is empty, treat it as Identity operator on any wire
+                # Pick any arbitrary wire from wire_order
+                return Wires(wire_order[0])
+
+            return wire_order
 
         if len(self) == 0:
             n = len(wire_order) if wire_order is not None else 0


Test patch:
diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py
--- a/tests/pauli/test_pauli_arithmetic.py
+++ b/tests/pauli/test_pauli_arithmetic.py
@@ -23,7 +23,6 @@
 from pennylane import numpy as np
 from pennylane.pauli.pauli_arithmetic import PauliWord, PauliSentence, I, X, Y, Z
 
-
 matI = np.eye(2)
 matX = np.array([[0, 1], [1, 0]])
 matY = np.array([[0, -1j], [1j, 0]])
@@ -877,6 +876,11 @@ def test_to_mat_empty(self, ps, true_res):
         assert sparse.issparse(res_sparse)
         assert qml.math.allclose(res_sparse.todense(), true_res)
 
+    def test_empty_pauli_to_mat_with_wire_order(self):
+        """Test the to_mat method with an empty PauliSentence and PauliWord and an external wire order."""
+        actual = PauliSentence({PauliWord({}): 1.5}).to_mat([0, 1])
+        assert np.allclose(actual, 1.5 * np.eye(4))
+
     ps_wire_order = ((ps1, []), (ps1, [0, 1, 2, "a", "b"]), (ps3, [0, 1, "c"]))
 
     @pytest.mark.parametrize("ps, wire_order", ps_wire_order)