Repository: PennyLaneAI/pennylane

Issue:
[BUG] `qml.equal` does not work with `qml.GlobalPhase`
### Expected behavior

```python
qml.equal(qml.PauliX(0)@qml.GlobalPhase(np.pi), qml.PauliX(0)@qml.GlobalPhase(np.pi))
```

This should be true

### Actual behavior

```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
[<ipython-input-16-74827f3d67e3>](https://hmoo5kikp7-496ff2e9c6d22116-0-colab.googleusercontent.com/outputframe.html?vrz=colab_20240408-060136_RC00_622797729#) in <cell line: 1>()
----> 1 qml.equal(qml.PauliX(0)@qml.GlobalPhase(np.pi), qml.PauliX(0)@qml.GlobalPhase(np.pi))

4 frames
[/usr/local/lib/python3.10/dist-packages/pennylane/ops/op_math/prod.py](https://hmoo5kikp7-496ff2e9c6d22116-0-colab.googleusercontent.com/outputframe.html?vrz=colab_20240408-060136_RC00_622797729#) in _swappable_ops(op1, op2, wire_map)
    545     wires2 = set(wires2)
    546     # compare strings of wire labels so that we can compare arbitrary wire labels like 0 and "a"
--> 547     return False if wires1 & wires2 else str(wires1.pop()) > str(wires2.pop())
    548 
    549 

KeyError: 'pop from an empty set'
```

### Additional information

if I specify the wires in GlobalPhase, it works

### Source code

_No response_

### Tracebacks

_No response_

### System information

```shell
Name: PennyLane
Version: 0.36.0.dev0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /usr/local/lib/python3.10/dist-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane_Lightning

Platform info:           Linux-6.1.58+-x86_64-with-glibc2.35
Python version:          3.10.12
Numpy version:           1.25.2
Scipy version:           1.11.4
Installed devices:
- lightning.qubit (PennyLane_Lightning-0.35.1)
- default.clifford (PennyLane-0.36.0.dev0)
- default.gaussian (PennyLane-0.36.0.dev0)
- default.mixed (PennyLane-0.36.0.dev0)
- default.qubit (PennyLane-0.36.0.dev0)
- default.qubit.autograd (PennyLane-0.36.0.dev0)
- default.qubit.jax (PennyLane-0.36.0.dev0)
- default.qubit.legacy (PennyLane-0.36.0.dev0)
- default.qubit.tf (PennyLane-0.36.0.dev0)
- default.qubit.torch (PennyLane-0.36.0.dev0)
- default.qutrit (PennyLane-0.36.0.dev0)
- null.qubit (PennyLane-0.36.0.dev0)
```


### Existing GitHub issues

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


Discussion:

Resolving patch:
diff --git a/pennylane/ops/op_math/prod.py b/pennylane/ops/op_math/prod.py
--- a/pennylane/ops/op_math/prod.py
+++ b/pennylane/ops/op_math/prod.py
@@ -536,6 +536,11 @@ def _swappable_ops(op1, op2, wire_map: dict = None) -> bool:
     Returns:
         bool: True if operators should be swapped, False otherwise.
     """
+    # one is broadcasted onto all wires.
+    if not op1.wires:
+        return True
+    if not op2.wires:
+        return False
     wires1 = op1.wires
     wires2 = op2.wires
     if wire_map is not None:
diff --git a/pennylane/ops/op_math/sum.py b/pennylane/ops/op_math/sum.py
--- a/pennylane/ops/op_math/sum.py
+++ b/pennylane/ops/op_math/sum.py
@@ -574,7 +574,10 @@ def _sort_key(op: Operator) -> tuple:
             wires = op.wires
             if wire_map is not None:
                 wires = wires.map(wire_map)
-            return sorted(list(map(str, wires)))[0], len(wires), str(op)
+            if not op.wires:
+                return ("", 0, str(op))
+            sorted_wires = sorted(list(map(str, wires)))[0]
+            return sorted_wires, len(wires), str(op)
 
         return sorted(op_list, key=_sort_key)
 


Test patch:
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -1744,6 +1744,14 @@ def test_prod_of_prods(self):
         op2 = qml.prod(*[0.5 * X(i) for i in range(5)])
         assert qml.equal(op1, op2)
 
+    def test_prod_global_phase(self):
+        """Test that a prod with a global phase can be used with qml.equal."""
+
+        p1 = qml.GlobalPhase(np.pi) @ qml.X(0)
+        p2 = qml.X(0) @ qml.GlobalPhase(np.pi)
+
+        assert qml.equal(p1, p2)
+
 
 @pytest.mark.usefixtures("use_new_opmath")
 class TestSumComparisons:
@@ -1856,6 +1864,12 @@ def test_sum_of_sums(self):
         op2 = qml.sum(*[0.5 * X(i) for i in range(10)])
         assert qml.equal(op1, op2)
 
+    def test_sum_global_phase(self):
+        """Test that a sum containing a no-wires op can still be compared."""
+        op1 = qml.sum(qml.X(0), qml.GlobalPhase(np.pi))
+        op2 = qml.sum(qml.GlobalPhase(np.pi), qml.X(0))
+        assert qml.equal(op1, op2)
+
 
 def f1(p, t):
     return np.polyval(p, t)
diff --git a/tests/ops/op_math/test_prod.py b/tests/ops/op_math/test_prod.py
--- a/tests/ops/op_math/test_prod.py
+++ b/tests/ops/op_math/test_prod.py
@@ -1659,6 +1659,15 @@ def test_sorting_operators_with_wire_map(self):
             assert op1.wires == op2.wires
             assert op1.data == op2.data
 
+    def test_sorting_operators_with_no_wires(self):
+        """Test that sorting can occur when an operator acts on no wires."""
+
+        op_list = (qml.GlobalPhase(0.5), qml.X(0), qml.I(), qml.CNOT((1, 2)), qml.I())
+
+        sorted_list = qml.ops.Prod._sort(op_list)
+        expected = [qml.X(0), qml.CNOT((1, 2)), qml.I(), qml.I(), qml.GlobalPhase(0.5)]
+        assert sorted_list == expected
+
 
 swappable_ops = [
     (qml.PauliX(1), qml.PauliY(0)),
diff --git a/tests/ops/op_math/test_sum.py b/tests/ops/op_math/test_sum.py
--- a/tests/ops/op_math/test_sum.py
+++ b/tests/ops/op_math/test_sum.py
@@ -1145,6 +1145,16 @@ def test_sort_wires_alphabetically(self):
         for op1, op2 in zip(final_list, sorted_list):
             assert qml.equal(op1, op2)
 
+    def test_sorting_operators_with_no_wires(self):
+        """Test that sorting can occur when an operator acts on no wires."""
+
+        op_list = [qml.GlobalPhase(0.5), qml.X(0), qml.Y(1), qml.I(), qml.CNOT((1, 2)), qml.I()]
+
+        sorted_list = Sum._sort(op_list)  # pylint: disable=protected-access
+
+        expected = [qml.GlobalPhase(0.5), qml.I(), qml.I(), qml.X(0), qml.Y(1), qml.CNOT((1, 2))]
+        assert sorted_list == expected
+
 
 class TestWrapperFunc:
     """Test wrapper function."""