Repository: PennyLaneAI/pennylane (python)

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.

Pull request description:
SProd auto-upgrades tensor and hamiltonian bases
**Context:**
Started in #5031, but missed this upgrade behaviour for `SProd`

**Description of the Change:**
If the base provided to `SProd.__init__` is a Tensor or Hamiltonian, auto-upgrade it to new arithmetic operators using `qml.operation.convert_to_opmath`

**Benefits:**
No confusing errors or unexpected behaviour when working with new operator arithmetic.

**Possible Drawbacks:**
N/A

**Related GitHub Issues:**
Fixes #5060

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 3cec643ed..96ee5ab67 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -35,10 +35,11 @@
 * A new `pennylane.workflow` module is added. This module now contains `qnode.py`, `execution.py`, `set_shots.py`, `jacobian_products.py`, and the submodule `interfaces`.
   [(#5023)](https://github.com/PennyLaneAI/pennylane/pull/5023)
 
-* Composite operations (eg. those made with `qml.prod` and `qml.sum`) convert `Hamiltonian` and
+* Composite operations (eg. those made with `qml.prod` and `qml.sum`) and `SProd` operations convert `Hamiltonian` and
   `Tensor` operands to `Sum` and `Prod` types, respectively. This helps avoid the mixing of
   incompatible operator types.
   [(#5031)](https://github.com/PennyLaneAI/pennylane/pull/5031)
+  [(#5063)](https://github.com/PennyLaneAI/pennylane/pull/5063)
 
 * Raise a more informative error when calling `adjoint_jacobian` with trainable state-prep operations.
   [(#5026)](https://github.com/PennyLaneAI/pennylane/pull/5026)
diff --git a/pennylane/ops/op_math/sprod.py b/pennylane/ops/op_math/sprod.py
index 9c39245c5..6259f487b 100644
--- a/pennylane/ops/op_math/sprod.py
+++ b/pennylane/ops/op_math/sprod.py
@@ -20,7 +20,7 @@ from copy import copy
 
 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 @@ class SProd(ScalarSymbolicOp):
         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
index 01493a7a8..5d009e87d 100644
--- 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
index 8ea66dfc6..aa3ed36d5 100644
--- a/tests/ops/op_math/test_sprod.py
+++ b/tests/ops/op_math/test_sprod.py
@@ -24,7 +24,7 @@ import pennylane as qml
 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 @@ class TestInitialization:
 
         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
index 3aa951186..add86cf8e 100644
--- a/tests/pauli/test_pauli_utils.py
+++ b/tests/pauli/test_pauli_utils.py
@@ -676,13 +676,23 @@ class TestPauliGroup:
 
     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
index ab4e765f4..dc05f5db5 100644
--- a/tests/pulse/test_rydberg.py
+++ b/tests/pulse/test_rydberg.py
@@ -173,7 +173,9 @@ class TestRydbergDrive:
         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 @@ class TestRydbergDrive:
         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
index af2a71845..e20903c95 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -1467,8 +1467,8 @@ class TestTensor:
         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 @@ class TestTensor:
         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)