Repository: PennyLaneAI/pennylane
Pull request #5052 (2024-01-12 15:48:57)

Add native commutators to PauliWord and PauliSentence classes
Adding native commutators for paulis.
Works between `PauliSentence` and any of `Operator, PauliWord, PauliSentence`

Builds on top of https://github.com/PennyLaneAI/pennylane/pull/5051

Todo

- [x] Basic functionality
- [x] Basic functionality tests
- [x] update qml.commutator
- [x] changelog

Qottmann: [sc-53927]
Qottmann: Why native commutator support you ask?

```
n = 10
H1 = PauliWord({0:"X", 1:"Z"})
H2 = PauliWord({0:"X", 1:"Z"})
for i in range(n):
    H1 += 0.5 * PauliWord({i+1:"Y", i+2:"Z"})
    H2 += 0.5 * PauliWord({i+1:"X", i+2:"Y"})
>>> %timeit H1 | H2
253 µs ± 4.32 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
>>> %timeit qml.commutator(H1, H2)
3.12 ms ± 348 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
```
Qottmann: Now fully removed the infix commutator, which removes the capability to have the first input be an Operator (e.g. `PauliX(0) | PauliWord({0:"Y"})`, which required some changes to the tests.
mudit2812: > Why native commutator support you ask?
> 
> ```
> n = 10
> H1 = PauliWord({0:"X", 1:"Z"})
> H2 = PauliWord({0:"X", 1:"Z"})
> for i in range(n):
>     H1 += 0.5 * PauliWord({i+1:"Y", i+2:"Z"})
>     H2 += 0.5 * PauliWord({i+1:"X", i+2:"Y"})
> >>> %timeit H1 | H2
> 253 µs ± 4.32 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
> >>> %timeit qml.commutator(H1, H2)
> 3.12 ms ± 348 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
> ```
> 
> 🚀

Considering the speedup, `qml.comm` should probably be updated to use the native commutators if `Pauli=True`.
Qottmann: > Considering the speedup, qml.comm should probably be updated to use the native commutators if Pauli=True.

Is there something missing in https://github.com/PennyLaneAI/pennylane/pull/5052/commits/ba5166fd74d3387f5fba4bdbaced7c6776ef68cd ?
mudit2812: > > Considering the speedup, qml.comm should probably be updated to use the native commutators if Pauli=True.
> 
> Is there something missing in [ba5166f](https://github.com/PennyLaneAI/pennylane/commit/ba5166fd74d3387f5fba4bdbaced7c6776ef68cd) ?

Missed that change. Sorry about that!
Qottmann: Realized there was a legacy clause in `PauliSentence.commutator` returning `copy(self)` when `len(other)=0`, which doesnt make sense as the empty PauliSentence is the 0 element.

Removed that bit, updated a test checking that the logic is sound now, and updated the docs.
Qottmann: Ready for review now after some CI trouble.

This PR adds a commutator method native to PauliWord/Sentence instances that is substantially faster.
It also updates`qml.commutator` to use the native method `pauli1.commutator(pauli2)`.

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 0da8efd7f..16ea70792 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -41,6 +41,7 @@
 * New `qml.commutator` function that allows to compute commutators between
   `qml.operation.Operator`, `qml.pauli.PauliWord` and `qml.pauli.PauliSentence` instances.
   [(#5051)](https://github.com/PennyLaneAI/pennylane/pull/5051)
+  [(#5052)](https://github.com/PennyLaneAI/pennylane/pull/5052)
 
   Basic usage with PennyLane operators.
 
@@ -69,6 +70,16 @@
   + 2j * X(0) @ Z(1)
   ```
 
+  We can also compute commutators with Pauli operators natively with the `PauliSentence.commutator` method.
+
+  ```pycon
+  >>> op1 = PauliWord({0:"X", 1:"X"})
+  >>> op2 = PauliWord({0:"Y"}) + PauliWord({1:"Y"})
+  >>> op1.commutator(op2)
+  2j * Z(0) @ X(1)
+  + 2j * X(0) @ Z(1)
+  ```
+
 
 <h4>Parity Mapping</h4>
 
@@ -89,6 +100,7 @@
    ((0.25+0j)*(PauliX(wires=[0]))) + (0.25j*(PauliY(wires=[0]) @ PauliZ(wires=[1])))
    ```
 
+
 <h3>Improvements 🛠</h3>
 
 * The `qml.qsvt` function uses `qml.GlobalPhase` instead of `qml.exp` to define global phase.
diff --git a/pennylane/ops/functions/commutator.py b/pennylane/ops/functions/commutator.py
index 27c534006..b3e634c70 100644
--- a/pennylane/ops/functions/commutator.py
+++ b/pennylane/ops/functions/commutator.py
@@ -65,6 +65,15 @@ def commutator(op1, op2, pauli=False):
     >>> qml.commutator(op1, op2, pauli=True)
     (2j*(PauliX(wires=[1]) @ PauliZ(wires=[0]))) + (2j*(PauliZ(wires=[1]) @ PauliX(wires=[0])))
 
+
+    It is also worth highlighting that computing commutators with Paulis is typically faster.
+    So whenever all input operators have a pauli representation, we recommend using `pauli=True`.
+    The result can then still be transformed into a PennyLane operator.
+
+    >>> res = qml.commutator(qml.PauliX(0), qml.PauliY(0), pauli=True)
+    >>> res.operation()
+
+
     .. details::
         :title: Usage Details
 
@@ -97,7 +106,7 @@ def commutator(op1, op2, pauli=False):
             op1 = qml.pauli.pauli_sentence(op1)
         if not isinstance(op2, PauliSentence):
             op2 = qml.pauli.pauli_sentence(op2)
-        return op1 @ op2 - op2 @ op1
+        return op1.commutator(op2)
 
     with qml.QueuingManager.stop_recording():
         if isinstance(op1, (PauliWord, PauliSentence)):
diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py
index 470a9a44a..7947454c0 100644
--- a/pennylane/pauli/pauli_arithmetic.py
+++ b/pennylane/pauli/pauli_arithmetic.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """The Pauli arithmetic abstract reduced representation classes"""
+# pylint:disable=protected-access
 import warnings
 from copy import copy
 from functools import reduce, lru_cache
@@ -58,6 +59,13 @@ mat_map = {
     Z: matZ,
 }
 
+anticom_map = {
+    I: {I: 0, X: 0, Y: 0, Z: 0},
+    X: {I: 0, X: 0, Y: 1, Z: 1},
+    Y: {I: 0, X: 1, Y: 0, Z: 1},
+    Z: {I: 0, X: 1, Y: 1, Z: 0},
+}
+
 
 @lru_cache
 def _cached_sparse_data(op):
@@ -312,6 +320,71 @@ class PauliWord(dict):
             f"PauliWord can only be divided by numerical data. Attempting to divide by {other} of type {type(other)}"
         )
 
+    def commutes_with(self, other):
+        """Fast check if two PauliWords commute with each other"""
+        wires = set(self) & set(other)
+        if not wires:
+            return True
+        anticom_count = sum(anticom_map[self[wire]][other[wire]] for wire in wires)
+        return (anticom_count % 2) == 0
+
+    def _commutator(self, other):
+        """comm between two PauliWords, returns tuple (new_word, coeff) for faster arithmetic"""
+        # This may be helpful to developers that need a more lightweight comm between pauli words
+        # without creating PauliSentence classes
+
+        if self.commutes_with(other):
+            return PauliWord({}), 0.0
+        new_word, coeff = self._matmul(other)
+        return new_word, 2 * coeff
+
+    def commutator(self, other):
+        """
+        Compute commutator between a ``PauliWord`` :math:`P` and other operator :math:`O`
+
+        .. math:: [P, O] = P O - O P
+
+        When the other operator is a :class:`~PauliWord` or :class:`~PauliSentence`,
+        this method is faster than computing ``P @ O - O @ P``. It is what is being used
+        in :func:`~commutator` when setting ``pauli=True``.
+
+        Args:
+            other (Union[Operator, PauliWord, PauliSentence]): Second operator
+
+        Returns:
+            ~PauliSentence: The commutator result in form of a :class:`~PauliSentence` instances.
+
+        **Examples**
+
+        You can compute commutators between :class:`~PauliWord` instances.
+
+        >>> pw = PauliWord({0:"X"})
+        >>> pw.commutator(PauliWord({0:"Y"}))
+        2j * Z(0)
+
+        You can also compute the commutator with other operator types if they have a Pauli representation.
+
+        >>> pw.commutator(qml.PauliY(0))
+        2j * Z(0)
+        """
+        if isinstance(other, PauliWord):
+            new_word, coeff = self._commutator(other)
+            if coeff == 0:
+                return PauliSentence({})
+            return PauliSentence({new_word: coeff})
+
+        if isinstance(other, qml.operation.Operator):
+            op_self = PauliSentence({self: 1.0})
+            return op_self.commutator(other)
+
+        if isinstance(other, PauliSentence):
+            # for infix method, this would be handled by __ror__
+            return -1.0 * other.commutator(self)
+
+        raise NotImplementedError(
+            f"Cannot compute natively a commutator between PauliWord and {other} of type {type(other)}"
+        )
+
     def __str__(self):
         """String representation of a PauliWord."""
         if len(self) == 0:
@@ -482,6 +555,24 @@ class PauliSentence(dict):
     + 1 * I
     + 1.0 * Z(3)
 
+    Note that while the empty :class:`~PauliWord` ``PauliWord({})`` respresents the identity, the empty ``PauliSentence`` represents 0
+
+    >>> PauliSentence({})
+    0 * I
+
+    We can compute commutators using the `PauliSentence.commutator()` method
+
+    >>> op1 = PauliWord({0:"X", 1:"X"})
+    >>> op2 = PauliWord({0:"Y"}) + PauliWord({1:"Y"})
+    >>> op1.commutator(op2)
+    2j * Z(0) @ X(1)
+    + 2j * X(0) @ Z(1)
+
+    Or, alternatively, use :func:`~commutator`.
+    >>> qml.commutator(op1, op2, pauli=True)
+
+    Note that we need to specify ``pauli=True`` as :func:`~.commutator` returns PennyLane operators by default.
+
     """
 
     # this allows scalar multiplication from left with numpy arrays np.array(0.5) * ps1
@@ -631,6 +722,63 @@ class PauliSentence(dict):
             f"PauliSentence can only be divided by numerical data. Attempting to divide by {other} of type {type(other)}"
         )
 
+    def commutator(self, other):
+        """
+        Compute commutator between a ``PauliSentence`` :math:`P` and other operator :math:`O`
+
+        .. math:: [P, O] = P O - O P
+
+        When the other operator is a :class:`~PauliWord` or :class:`~PauliSentence`,
+        this method is faster than computing ``P @ O - O @ P``. It is what is being used
+        in :func:`~commutator` when setting ``pauli=True``.
+
+        Args:
+            other (Union[Operator, PauliWord, PauliSentence]): Second operator
+
+        Returns:
+            ~PauliSentence: The commutator result in form of a :class:`~PauliSentence` instances.
+
+        **Examples**
+
+        You can compute commutators between :class:`~PauliSentence` instances.
+
+        >>> pw1 = PauliWord({0:"X"})
+        >>> pw2 = PauliWord({1:"X"})
+        >>> ps1 = PauliSentence({pw1: 1., pw2: 2.})
+        >>> ps2 = PauliSentence({pw1: 0.5j, pw2: 1j})
+        >>> ps1.commutator(ps2)
+        0 * I
+
+        You can also compute the commutator with other operator types if they have a Pauli representation.
+
+        >>> ps1.commutator(qml.PauliY(0))
+        2j * Z(0)"""
+        final_ps = PauliSentence()
+
+        if isinstance(other, PauliWord):
+            for pw1 in self:
+                comm_pw, coeff = pw1._commutator(other)
+                if len(comm_pw) != 0:
+                    final_ps[comm_pw] += coeff * self[pw1]
+
+            return final_ps
+
+        if not isinstance(other, PauliSentence):
+            if other.pauli_rep is None:
+                raise NotImplementedError(
+                    f"Cannot compute a native commutator of a Pauli word or sentence with the operator {other} of type {type(other)}."
+                    f"You can try to use qml.commutator(op1, op2, pauli=False) instead."
+                )
+            other = qml.pauli.pauli_sentence(other)
+
+        for pw1 in self:
+            for pw2 in other:
+                comm_pw, coeff = pw1._commutator(pw2)
+                if len(comm_pw) != 0:
+                    final_ps[comm_pw] += coeff * self[pw1] * other[pw2]
+
+        return final_ps
+
     def __str__(self):
         """String representation of the PauliSentence."""
         if len(self) == 0:
@@ -767,7 +915,6 @@ class PauliSentence(dict):
             mv += vector[:, entries] * data.reshape(1, -1)
         return mv.reshape(vector.shape)
 
-    # pylint: disable=protected-access
     def _get_same_structure_csr(self, pauli_words, wire_order):
         """Returns the CSR indices and data for Pauli words with the same sparse structure."""
         indices = pauli_words[0]._get_csr_indices(wire_order)


Test patch:
diff --git a/tests/ops/functions/test_commutator.py b/tests/ops/functions/test_commutator.py
index bff5e3af0..38686bb77 100644
--- a/tests/ops/functions/test_commutator.py
+++ b/tests/ops/functions/test_commutator.py
@@ -74,7 +74,7 @@ class TestLegacySupport:
 
 
 def test_alias():
-    """Test that the alias comm() works as expected"""
+    """Test that the alias qml.comm() works as expected"""
     res1 = qml.comm(X0, Y0)
     res1_true = qml.commutator(X0, Y0)
     assert res1 == res1_true
@@ -131,9 +131,9 @@ class TestcommPauli:
 
     data_pauli_relations = (
         # word and word
-        (X0, X0, PauliSentence({pw_id: 0})),
-        (Y0, Y0, PauliSentence({pw_id: 0})),
-        (Z0, Z0, PauliSentence({pw_id: 0})),
+        (X0, X0, PauliSentence({})),
+        (Y0, Y0, PauliSentence({})),
+        (Z0, Z0, PauliSentence({})),
         (X0, Y0, PauliSentence({Z0: 2j})),
         (Y0, Z0, PauliSentence({X0: 2j})),
         (Z0, X0, PauliSentence({Y0: 2j})),
@@ -151,23 +151,25 @@ class TestcommPauli:
         assert res == true_res
         assert isinstance(res, PauliSentence)
 
-    data_more_comm_relations = (
+    data_more_comm_relations_commutes = (
         (
             PauliWord({0: "X", 1: "X"}),
             PauliWord({0: "Y", 1: "Y"}),
-            PauliSentence({PauliWord({0: "Z", 1: "Z"}): 0.0}),
+            PauliSentence({}),
         ),
         (
             PauliWord({0: "X", 1: "X"}),
             PauliWord({"a": "X", "b": "Y"}),
-            PauliSentence({PauliWord({0: "X", 1: "X", "a": "X", "b": "Y"}): 0.0}),
+            PauliSentence({}),
         ),
     )
 
     @pytest.mark.parametrize("transform_type1", [_id, _pauli_to_op, _pw_to_ps])
     @pytest.mark.parametrize("transform_type2", [_id, _pauli_to_op, _pw_to_ps])
-    @pytest.mark.parametrize("op1, op2, true_res", data_more_comm_relations)
-    def test_comm_relations_pauli_words(self, op1, op2, true_res, transform_type1, transform_type2):
+    @pytest.mark.parametrize("op1, op2, true_res", data_more_comm_relations_commutes)
+    def test_comm_relations_pauli_words_that_commute(
+        self, op1, op2, true_res, transform_type1, transform_type2
+    ):
         """Test more comm relations between Paulis"""
         res = qml.commutator(transform_type1(op1), transform_type2(op2), pauli=True)
         assert res == true_res
@@ -222,3 +224,13 @@ class TestcommPauliFalseSimplify:
         assert res == true_res
         assert isinstance(res, Operator)
         assert isinstance(res, SProd)
+
+    def test_consistency_with_native_pauli_comms(self):
+        """Test consistent behavior between native comms in PauliWord and PauliSentence and qml.commutor"""
+        op1 = qml.PauliX(0) @ qml.PauliX(1)
+        op2 = qml.PauliY(0) + qml.PauliY(1)
+        res1 = qml.commutator(op1, op2, pauli=True)
+        res2 = PauliWord({0: "X", 1: "X"}).commutator(PauliWord({0: "Y"}) + PauliWord({1: "Y"}))
+        assert isinstance(res1, PauliSentence)
+        assert isinstance(res2, PauliSentence)
+        assert res1 == res2
diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py
index 7aa688afb..c7ed7269e 100644
--- a/tests/pauli/test_pauli_arithmetic.py
+++ b/tests/pauli/test_pauli_arithmetic.py
@@ -12,7 +12,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """Unit Tests for the PauliWord and PauliSentence classes"""
-# pylint: disable=too-many-public-methods, protected-access
+# pylint: disable=too-many-public-methods,too-many-arguments,protected-access
+
 import pickle
 from copy import copy, deepcopy
 import pytest
@@ -54,6 +55,26 @@ ps5 = PauliSentence({})
 
 sentences = [ps1, ps2, ps3, ps4, ps5, ps1_hamiltonian, ps2_hamiltonian]
 
+X0 = PauliWord({0: "X"})
+Y0 = PauliWord({0: "Y"})
+Z0 = PauliWord({0: "Z"})
+
+
+def _pauli_to_op(p):
+    """convert PauliWord or PauliSentence to Operator"""
+    return p.operation()
+
+
+def _pw_to_ps(p):
+    """convert PauliWord to PauliSentence"""
+    return PauliSentence({p: 1.0})
+
+
+def _id(p):
+    """Leave operator as is"""
+    # this is used for parametrization of tests
+    return p
+
 
 class TestDeprecations:
     def test_deprecation_warning_PauliWord(
@@ -1109,6 +1130,197 @@ class TestPauliSentence:
         )
 
 
+class TestPaulicomms:
+    """Test 'native' commutator in PauliWord and PauliSentence"""
+
+    def test_pauli_word_comm_raises_NotImplementedError(self):
+        """Test that a NotImplementedError is raised when a PauliWord.commutator() for type that is not PauliWord, PauliSentence or Operator"""
+        op1 = PauliWord({0: "X"})
+        matrix = np.eye(2)
+        with pytest.raises(NotImplementedError, match="Cannot compute natively a commutator"):
+            op1.commutator(matrix)
+
+    def test_pauli_raises_NotImplementedError_without_pauli_rep_ps(self):
+        """Test that a NotImplementedError is raised in PauliSentence when ``other`` is an operator without a pauli_rep"""
+        with pytest.raises(
+            NotImplementedError, match="Cannot compute a native commutator of a Pauli word"
+        ):
+            _ = ps1.commutator(qml.Hadamard(0))
+
+    def test_pauli_raises_NotImplementedError_without_pauli_rep_pw(self):
+        """Test that a NotImplementedError is raised in PauliWord when ``other`` is an operator without a pauli_rep"""
+        with pytest.raises(
+            NotImplementedError, match="Cannot compute a native commutator of a Pauli word"
+        ):
+            _ = pw1.commutator(qml.Hadamard(0))
+
+    def test_commutators_with_zeros_ps(self):
+        """Test that commutators between PauliSentences where one represents the 0 word is treated correctly"""
+        op_zero = PauliSentence({})
+        op = PauliSentence({PauliWord({0: "X"}): 1.0})
+
+        assert op_zero.commutator(op) == op_zero
+        assert op.commutator(op_zero) == op_zero
+
+    data_pauli_relations_commutes = [
+        # word and word
+        (X0, X0, PauliSentence({})),
+        (Y0, Y0, PauliSentence({})),
+        (Z0, Z0, PauliSentence({})),
+        (PauliWord({"a": X}), PauliWord({"a": X}), PauliSentence({})),
+        (PauliWord({"a": X}), PauliWord({"b": Y}), PauliSentence({})),
+    ]
+
+    @pytest.mark.parametrize("op1, op2, true_res", data_pauli_relations_commutes)
+    def test_zero_return_pauli_word(self, op1, op2, true_res):
+        """Test the return when both operators are zero"""
+        res1 = op1.commutator(op2)
+        res1m = op2.commutator(op1)
+        assert res1 == true_res
+        assert res1m == true_res
+
+    @pytest.mark.parametrize("convert1", [_id, _pw_to_ps])
+    @pytest.mark.parametrize("op1, op2, true_res", data_pauli_relations_commutes)
+    def test_zero_return_pauli_word_different_types(self, op1, op2, true_res, convert1):
+        """Test the return when both operators are zero and potentially of different type"""
+        op1 = convert1(op1)
+        res2 = op1.commutator(op2)
+        res2m = op2.commutator(op1)
+        assert res2 == true_res
+        assert res2m == true_res
+
+    @pytest.mark.parametrize("convert1", [_id, _pauli_to_op, _pw_to_ps])
+    @pytest.mark.parametrize("op1, op2, true_res", data_pauli_relations_commutes)
+    def test_zero_return_pauli_word_different_types_with_operator(
+        self, op1, op2, true_res, convert1
+    ):
+        """Test the return when both operators are zero and potentially of different type"""
+        op1 = convert1(op1)
+        res = op2.commutator(op1)
+        assert res == true_res
+
+    data_pauli_relations = [
+        # word and word
+        (X0, X0, PauliSentence({})),
+        (Y0, Y0, PauliSentence({})),
+        (Z0, Z0, PauliSentence({})),
+        (X0, Y0, PauliSentence({Z0: 2j})),
+        (Y0, Z0, PauliSentence({X0: 2j})),
+        (Z0, X0, PauliSentence({Y0: 2j})),
+        (Y0, X0, PauliSentence({Z0: -2j})),
+        (Z0, Y0, PauliSentence({X0: -2j})),
+        (X0, Z0, PauliSentence({Y0: -2j})),
+    ]
+
+    @pytest.mark.parametrize("op1, op2, true_res", data_pauli_relations)
+    def test_pauli_word_commutator(self, op1, op2, true_res):
+        """Test native comm in PauliWord class"""
+        res1 = op1.commutator(op2)
+        res1m = op2.commutator(op1)
+        assert res1 == true_res
+        assert res1m == -1 * true_res
+
+    data_pauli_relations_private_func = (
+        # test when using the private _comm function that returns a tuple instead of a PauliSentence
+        (X0, X0, pw_id, 0),
+        (Y0, Y0, pw_id, 0),
+        (Z0, Z0, pw_id, 0),
+        (X0, Y0, Z0, 2j),
+        (Y0, Z0, X0, 2j),
+        (Z0, X0, Y0, 2j),
+        (Y0, X0, Z0, -2j),
+        (Z0, Y0, X0, -2j),
+        (X0, Z0, Y0, -2j),
+    )
+
+    @pytest.mark.parametrize("op1, op2, true_word, true_coeff", data_pauli_relations_private_func)
+    def test_pauli_word_private_commutator(self, op1, op2, true_word, true_coeff):
+        """Test native _comm in PauliWord class that returns tuples"""
+        res1 = op1._commutator(op2)
+        res1m = op2._commutator(op1)
+        assert res1[0] == true_word
+        assert res1[1] == true_coeff
+        assert res1m[0] == true_word
+        assert res1m[1] == -1 * true_coeff
+
+    data_pauli_relations_different_types = [
+        (X0, Y0, PauliSentence({Z0: 2j})),
+        (Y0, Z0, PauliSentence({X0: 2j})),
+        (Z0, X0, PauliSentence({Y0: 2j})),
+        (Y0, X0, PauliSentence({Z0: -2j})),
+        (Z0, Y0, PauliSentence({X0: -2j})),
+        (X0, Z0, PauliSentence({Y0: -2j})),
+    ]
+
+    @pytest.mark.parametrize("convert1", [_id, _pw_to_ps])
+    @pytest.mark.parametrize("convert2", [_id, _pw_to_ps])
+    @pytest.mark.parametrize("op1, op2, true_res", data_pauli_relations_different_types)
+    def test_pauli_word_comm_different_types(self, op1, op2, true_res, convert1, convert2):
+        """Test native comm in between a PauliSentence and either of PauliWord, PauliSentence, Operator"""
+        op1 = convert1(op1)
+        op2 = convert2(op2)
+        res2 = op1.commutator(op2)
+        res2m = op2.commutator(op1)
+        assert res2 == true_res
+        assert res2m == -1 * true_res
+        assert all(isinstance(res, PauliSentence) for res in [res2, res2m])
+
+    @pytest.mark.parametrize("convert1", [_id, _pw_to_ps])
+    @pytest.mark.parametrize("convert2", [_pauli_to_op])
+    @pytest.mark.parametrize("op1, op2, true_res", data_pauli_relations_different_types)
+    def test_pauli_word_comm_different_types_with_ops(self, op1, op2, true_res, convert1, convert2):
+        """Test native comm in between a PauliWord, PauliSentence and Operator"""
+        op1 = convert1(op1)
+        op2 = convert2(op2)
+        res2 = op1.commutator(op2)
+        assert res2 == true_res
+        assert isinstance(res2, PauliSentence)
+
+    data_pauli_relations_commutes = [
+        (X0, X0, PauliSentence({})),
+        (Y0, Y0, PauliSentence({})),
+        (Z0, Z0, PauliSentence({})),
+    ]
+
+    @pytest.mark.parametrize("convert1", [_id, _pw_to_ps])
+    @pytest.mark.parametrize("op1, op2, true_res", data_pauli_relations_different_types)
+    def test_pauli_word_comm_commutes(self, op1, op2, true_res, convert1):
+        """Test native comm in between a PauliSentence and either of PauliWord, PauliSentence, Operator for the case when the operators commute"""
+        op1 = convert1(op1)
+        op2 = qml.pauli.pauli_sentence(op2)
+        res2 = op1.commutator(op2)
+        res2m = op2.commutator(op1)
+        assert res2 == true_res
+        assert res2m == -1 * true_res
+        assert all(isinstance(res, PauliSentence) for res in [res2, res2m])
+
+    @pytest.mark.parametrize("convert1", [_id, _pw_to_ps])
+    @pytest.mark.parametrize("op1, op2, true_res", data_pauli_relations_different_types)
+    def test_pauli_commutator_with_operator(self, op1, op2, true_res, convert1):
+        """Test native comm in between a PauliSentence and either of PauliWord, PauliSentence, Operator for the case when the operators commute"""
+        op1 = convert1(op1)
+        op2 = _pauli_to_op(op2)
+        res2 = op1.commutator(op2)
+        assert res2 == true_res
+        assert isinstance(res2, PauliSentence)
+
+    data_consistency_paulis = [
+        PauliWord({0: "X", 1: "X"}),
+        PauliWord({0: "Y"}) + PauliWord({1: "Y"}),
+        1.5 * PauliWord({0: "Y"}) + 0.5 * PauliWord({1: "Y"}),
+    ]
+
+    @pytest.mark.parametrize("op1", data_consistency_paulis)
+    @pytest.mark.parametrize("op2", data_consistency_paulis)
+    def test_consistency_pw_ps(self, op1, op2):
+        """Test consistent behavior when inputting PauliWord and PauliSentence"""
+        op1 = PauliWord({0: "X", 1: "X"})
+        op2 = PauliWord({0: "Y"}) + PauliWord({1: "Y"})
+        res1 = op1.commutator(op2)
+        res2 = qml.commutator(op1, op2, pauli=True)
+        assert res1 == res2
+
+
 class TestPauliArithmeticIntegration:
     def test_pauli_arithmetic_integration(self):
         """Test creating operators from PauliWord, PauliSentence and scalars"""