Repository: PennyLaneAI/pennylane

Issue:
[Bug] qml.equal return False unexpectedly
Encountered this problem in https://github.com/PennyLaneAI/pennylane/pull/5051 for creating a `qml.commutator` function.

Even though the result from computing the commutator seems the same, `qml.equal` returns `False` when checking with the true result. Really not clear what the problem is here 🤔 

```
H1 = qml.prod(qml.PauliX(0), qml.PauliX(1))
H2 = qml.s_prod(1., qml.sum(qml.PauliY(0), qml.PauliY(1)))
true_res = qml.sum(qml.s_prod(2j, qml.PauliZ(0) @ qml.PauliX(1)), qml.s_prod(2j, qml.PauliX(0) @ qml.PauliZ(1)))
res = qml.prod(H1, H2) - qml.prod(H2, H1)
res = res.simplify()
res = res.simplify()

>>> true_res, res
((2j*(PauliZ(wires=[0]) @ PauliX(wires=[1]))) + (2j*(PauliX(wires=[0]) @ PauliZ(wires=[1]))),
 (2j*(PauliZ(wires=[0]) @ PauliX(wires=[1]))) + (2j*(PauliX(wires=[0]) @ PauliZ(wires=[1]))))
>>> qml.equal(true_res, res)
False
>>> type(true_res), type(res)
(pennylane.ops.op_math.sum.Sum, pennylane.ops.op_math.sum.Sum)
```

The same happens when using legacy classes (Tensor, Hamiltonian), the operators are still identical though the ordering of some of the operators changes:

```
H1 = qml.Hamiltonian([1.], [qml.PauliX(0) @ qml.PauliX(1)])
H2 = qml.Hamiltonian([1.], [qml.PauliY(0) + qml.PauliY(1)])
true_res = qml.sum(qml.s_prod(2j, qml.PauliZ(0) @ qml.PauliX(1)), qml.s_prod(2j, qml.PauliX(0) @ qml.PauliZ(1)))
res = qml.prod(H1, H2) - qml.prod(H2, H1)
res = res.simplify()
res = res.simplify()

>>> true_res, res # order of operators changes but that should _not_ be an issue for qml.equal
((2j*(PauliZ(wires=[0]) @ PauliX(wires=[1]))) + (2j*(PauliX(wires=[0]) @ PauliZ(wires=[1]))),
 (2j*(PauliX(wires=[1]) @ PauliZ(wires=[0]))) + (2j*(PauliZ(wires=[1]) @ PauliX(wires=[0]))))
>>> qml.equal(true_res, res)
False
>>> type(true_res), type(res)
(pennylane.ops.op_math.sum.Sum, pennylane.ops.op_math.sum.Sum)
```

I tried on the branch of this [bug fix](https://github.com/PennyLaneAI/pennylane/pull/5037), without success. I think this is a separate issue.


Discussion:

Resolving patch:
diff --git a/pennylane/ops/op_math/sprod.py b/pennylane/ops/op_math/sprod.py
--- a/pennylane/ops/op_math/sprod.py
+++ b/pennylane/ops/op_math/sprod.py
@@ -20,7 +20,7 @@
 
 import pennylane as qml
 import pennylane.math as qnp
-from pennylane.operation import Operator
+from pennylane.operation import Operator, convert_to_opmath
 from pennylane.ops.op_math.pow import Pow
 from pennylane.ops.op_math.sum import Sum
 from pennylane.queuing import QueuingManager
@@ -137,6 +137,7 @@ def _unflatten(cls, data, _):
         return cls(data[0], data[1])
 
     def __init__(self, scalar: Union[int, float, complex], base: Operator, id=None):
+        base = convert_to_opmath(base)
         super().__init__(base=base, scalar=scalar, id=id)
 
         if (base_pauli_rep := getattr(self.base, "_pauli_rep", None)) and (self.batch_size is None):
diff --git a/pennylane/pauli/utils.py b/pennylane/pauli/utils.py
--- a/pennylane/pauli/utils.py
+++ b/pennylane/pauli/utils.py
@@ -464,10 +464,10 @@ def pauli_word_to_string(pauli_word, wire_map=None):
     if isinstance(pauli_word, Hamiltonian):
         # hamiltonian contains only one term
         pauli_word = pauli_word.ops[0]
-    elif isinstance(pauli_word, Prod):
-        pauli_word = Tensor(*pauli_word.operands)
     elif isinstance(pauli_word, SProd):
         pauli_word = pauli_word.base
+    if isinstance(pauli_word, Prod):
+        pauli_word = Tensor(*pauli_word.operands)
 
     character_map = {"Identity": "I", "PauliX": "X", "PauliY": "Y", "PauliZ": "Z"}
 


Test patch:
diff --git a/tests/ops/op_math/test_sprod.py b/tests/ops/op_math/test_sprod.py
--- a/tests/ops/op_math/test_sprod.py
+++ b/tests/ops/op_math/test_sprod.py
@@ -24,7 +24,7 @@
 import pennylane.numpy as qnp
 from pennylane import math
 from pennylane.operation import AnyWires, DecompositionUndefinedError, MatrixUndefinedError
-from pennylane.ops.op_math.sprod import SProd, s_prod
+from pennylane.ops.op_math import SProd, s_prod, Prod, Sum
 from pennylane.wires import Wires
 
 
@@ -181,6 +181,16 @@ def test_diagonalizing_gates(self):
 
         assert np.allclose(diagonalizing_gates, true_diagonalizing_gates)
 
+    def test_base_gets_cast_to_new_type(self):
+        """Test that Tensor and Hamiltonian instances get cast to new types."""
+        base_H = qml.Hamiltonian([1.1, 2.2], [qml.PauliZ(0), qml.PauliZ(1)])
+        op_H = qml.s_prod(2j, base_H)
+        assert isinstance(op_H.base, Sum)
+
+        base_T = qml.PauliZ(0) @ qml.PauliZ(1)
+        op_T = qml.s_prod(2j, base_T)
+        assert isinstance(op_T.base, Prod)
+
 
 class TestMscMethods:
     """Test miscellaneous methods of the SProd class."""
diff --git a/tests/pauli/test_pauli_utils.py b/tests/pauli/test_pauli_utils.py
--- a/tests/pauli/test_pauli_utils.py
+++ b/tests/pauli/test_pauli_utils.py
@@ -676,13 +676,23 @@ def test_pauli_mult_with_phase_using_prod(self, pauli_word_1, pauli_word_2, expe
 
     def test_deprecated_pauli_mult(self):
         """Test that pauli_mult is deprecated."""
-        with pytest.warns(qml.PennyLaneDeprecationWarning, match=""):
+        with pytest.warns(qml.PennyLaneDeprecationWarning, match="`pauli_mult` is deprecated"):
             pauli_mult(PauliX(0), PauliY(1))
 
-    def test_deprecated_pauli_mult_with_phase(self):
+    @pytest.mark.parametrize(
+        "pauli_word_1,pauli_word_2,expected_phase",
+        [
+            (PauliZ(0), PauliY(0), -1j),
+            (PauliZ("a") @ PauliY("b"), PauliX("a") @ PauliZ("b"), -1),
+        ],
+    )
+    def test_deprecated_pauli_mult_with_phase(self, pauli_word_1, pauli_word_2, expected_phase):
         """Test that pauli_mult_with_phase is deprecated."""
-        with pytest.warns(qml.PennyLaneDeprecationWarning, match=""):
-            pauli_mult_with_phase(PauliX(0), PauliY(1))
+        with pytest.warns(
+            qml.PennyLaneDeprecationWarning, match="`pauli_mult_with_phase` is deprecated"
+        ):
+            _, obtained_phase = pauli_mult_with_phase(pauli_word_1, pauli_word_2)
+        assert obtained_phase == expected_phase
 
 
 class TestPartitionPauliGroup:
diff --git a/tests/pulse/test_rydberg.py b/tests/pulse/test_rydberg.py
--- a/tests/pulse/test_rydberg.py
+++ b/tests/pulse/test_rydberg.py
@@ -173,7 +173,9 @@ def fb(p, t):
         assert Hd.pulses == H1.pulses + H2.pulses
 
         # Hamiltonian is as expected
-        assert qml.equal(Hd([0.5, -0.5], t=5), H_expected([0.5, -0.5], t=5))
+        actual = Hd([0.5, -0.5], t=5).simplify()
+        expected = H_expected([0.5, -0.5], t=5).simplify()
+        assert qml.equal(actual, expected)
 
     def test_no_amplitude(self):
         """Test that when amplitude is not specified, the drive term is correctly defined."""
@@ -193,7 +195,9 @@ def f(p, t):
         coeffs_expected = [f]
         H_expected = HardwareHamiltonian(coeffs_expected, ops_expected)
 
-        assert qml.equal(Hd([0.1], 10), H_expected([0.1], 10))
+        actual = Hd([0.1], 10).simplify()
+        expected = H_expected([0.1], 10).simplify()
+        assert qml.equal(actual, expected)
         assert isinstance(Hd, HardwareHamiltonian)
         assert Hd.wires == Wires([0, 3])
         assert Hd.settings is None
diff --git a/tests/test_operation.py b/tests/test_operation.py
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -1467,8 +1467,8 @@ def test_params(self):
         t = Tensor(X, Y)
         assert t.data == (p,)
 
-    def test_data_setter(self):
-        """Test the data setter"""
+    def test_data_setter_list(self):
+        """Test the data setter with a list"""
         p = np.eye(4)
         X = qml.PauliX(0)
         Y = qml.Hermitian(p, wires=[1, 2])
@@ -1478,6 +1478,17 @@ def test_data_setter(self):
         t.data = [(), (new_data,)]
         assert qml.math.allequal(t.data, (new_data,))
 
+    def test_data_setter_tuple(self):
+        """Test the data setter with a tuple"""
+        p = np.eye(4)
+        X = qml.PauliX(0)
+        Y = qml.Hermitian(p, wires=[1, 2])
+        t = Tensor(X, Y)
+        assert t.data == (p,)
+        new_data = np.eye(4) * 6
+        t.data = (new_data,)
+        assert qml.math.allequal(t.data, (new_data,))
+
     def test_num_params(self):
         """Test that the correct number of parameters is returned"""
         p = np.eye(4)