Repository: PennyLaneAI/pennylane
Pull request #5051 (2024-01-12 14:21:25)

Add qml.commutator function
A function to compute commutators between operators, PauliWord and PauliSentence instances.
Work in progress, happy to hear early feedback!

- [x] Basic functionality for paulis
- [x] Basic functionality for operators
- [x] Basic functionality between paulis and operators
- [x] General Tests
- [x] Documentation
- [x] changelog

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5051?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`c01cb4b`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/c01cb4b96f758a9f1a0c4e422115829b80e113cb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.67% compared to head [(`5c65800`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5051?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.66%.
> Report is 2 commits behind head on master.


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5051      +/-   ##
==========================================
- Coverage   99.67%   99.66%   -0.01%     
==========================================
  Files         394      395       +1     
  Lines       35670    35423     -247     
==========================================
- Hits        35554    35306     -248     
- Misses        116      117       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5051?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

Qottmann: [sc-53926]
Qottmann: Changed to a briefer version `qml.comm` after feedback from nathan. Any strong opposition to that?

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 9b64d507e..d37156fde 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -39,6 +39,37 @@
   is now strictly faster for jax.
   [(#4963)](https://github.com/PennyLaneAI/pennylane/pull/4963)
 
+* 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)
+
+  Basic usage with PennyLane operators.
+
+  ```pycon
+  >>> qml.commutator(qml.PauliX(0), qml.PauliY(0))
+  2j*(PauliZ(wires=[0]))
+  ```
+
+  We can return a `PauliSentence` instance by setting `pauli=True`.
+
+  ```pycon
+  >>> op1 = qml.PauliX(0) @ qml.PauliX(1)
+  >>> op2 = qml.PauliY(0) + qml.PauliY(1)
+  >>> qml.commutator(op1, op2, pauli=True)
+  2j * X(1) @ Z(0)
+  + 2j * Z(1) @ X(0)
+  ```
+
+  We can also input `PauliWord` and `PauliSentence` instances.
+
+  ```pycon
+  >>> op1 = PauliWord({0:"X", 1:"X"})
+  >>> op2 = PauliWord({0:"Y"}) + PauliWord({1:"Y"})
+  >>> qml.commutator(op1, op2, pauli=True)
+  2j * Z(0) @ X(1)
+  + 2j * X(0) @ Z(1)
+  ```
+
 <h3>Improvements 🛠</h3>
 
 * `device_vjp` can now be used with normal Tensorflow. Support has not yet been added
diff --git a/pennylane/__init__.py b/pennylane/__init__.py
index 0cee2e584..a000a50e4 100644
--- a/pennylane/__init__.py
+++ b/pennylane/__init__.py
@@ -112,6 +112,8 @@ from pennylane.ops.functions import (
     matrix,
     simplify,
     iterative_qpe,
+    commutator,
+    comm,
 )
 from pennylane.optimize import *
 from pennylane.vqe import ExpvalCost
diff --git a/pennylane/ops/functions/__init__.py b/pennylane/ops/functions/__init__.py
index e879b1c36..36315d11d 100644
--- a/pennylane/ops/functions/__init__.py
+++ b/pennylane/ops/functions/__init__.py
@@ -33,9 +33,12 @@ This module contains functions that act on operators and tapes.
     ~matrix
     ~simplify
     ~iterative_qpe
+    ~commutator
+    ~comm
 
 """
 from .bind_new_parameters import bind_new_parameters
+from .commutator import commutator, comm
 from .dot import dot
 from .eigvals import eigvals
 from .equal import equal
diff --git a/pennylane/ops/functions/commutator.py b/pennylane/ops/functions/commutator.py
new file mode 100644
index 000000000..27c534006
--- /dev/null
+++ b/pennylane/ops/functions/commutator.py
@@ -0,0 +1,113 @@
+# Copyright 2024 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+This file contains the implementation of the commutator function in PennyLane
+"""
+import pennylane as qml
+from pennylane.pauli import PauliWord, PauliSentence
+
+
+def commutator(op1, op2, pauli=False):
+    r"""Compute commutator between two operators in PennyLane
+
+    .. math:: [O_1, O_2] = O_1 O_2 - O_2 O_1
+
+    Args:
+        op1 (Union[Operator, PauliWord, PauliSentence]): First operator
+        op2 (Union[Operator, PauliWord, PauliSentence]): Second operator
+        pauli (bool): When ``True``, all results are passed as a ``PauliSentence`` instance. Else, results are always returned as ``Operator`` instances.
+
+    Returns:
+        ~Operator or ~PauliSentence: The commutator
+
+    **Examples**
+
+    You can compute commutators between operators in PennyLane.
+
+    >>> qml.commutator(qml.PauliX(0), qml.PauliY(0))
+    2j*(PauliZ(wires=[0]))
+
+    >>> op1 = qml.PauliX(0) @ qml.PauliX(1)
+    >>> op2 = qml.PauliY(0) @ qml.PauliY(1)
+    >>> qml.commutator(op1, op2)
+    0*(Identity(wires=[0, 1]))
+
+    We can return a :class:`~PauliSentence` instance by setting ``pauli=True``.
+
+    >>> op1 = qml.PauliX(0) @ qml.PauliX(1)
+    >>> op2 = qml.PauliY(0) + qml.PauliY(1)
+    >>> qml.commutator(op1, op2, pauli=True)
+    2j * X(1) @ Z(0)
+    + 2j * Z(1) @ X(0)
+
+    We can also input :class:`~PauliWord` and :class:`~PauliSentence` instances.
+
+    >>> op1 = PauliWord({0:"X", 1:"X"})
+    >>> op2 = PauliWord({0:"Y"}) + PauliWord({1:"Y"})
+    >>> qml.commutator(op1, op2, pauli=True)
+    2j * Z(0) @ X(1)
+    + 2j * X(0) @ Z(1)
+
+    Note that when ``pauli=False``, even if Pauli operators are used
+    as inputs, ``qml.commutator`` returns Operators.
+
+    >>> qml.commutator(op1, op2, pauli=True)
+    (2j*(PauliX(wires=[1]) @ PauliZ(wires=[0]))) + (2j*(PauliZ(wires=[1]) @ PauliX(wires=[0])))
+
+    .. details::
+        :title: Usage Details
+
+        The input and result of ``qml.commutator`` is not recorded in a tape context (and inside a :class:`~QNode`).
+
+        .. code-block:: python3
+
+            with qml.tape.QuantumTape() as tape:
+                a = qml.PauliX(0)      # gets recorded
+                b = PauliWord({0:"Y"}) # does not get recorded
+                comm = qml.commutator(a, b) # does not get recorded
+
+        In this example, we obtain ``tape.operations = [qml.PauliX(0)]``. When desired, we can still record the result of
+        the commutator by using :func:`~apply`, i.e. ``qml.apply(comm)`` inside the recording context.
+
+        A peculiarity worth repeating is how in a recording context every created operator is recorded.
+
+        .. code-block:: python3
+
+            with qml.tape.QuantumTape() as tape:
+                comm = qml.commutator(qml.PauliX(0), qml.PauliY(0))
+
+        In this example, both :class:`~PauliX` and :class:`PauliY` get recorded because they were created inside the
+        recording context. To avoid this, create the input to ``qml.commutator`` outside the recording context / qnode
+        or insert an extra ``stop_recording()`` context (see :class:`~QueuingManager`).
+
+    """
+    if pauli:
+        if not isinstance(op1, PauliSentence):
+            op1 = qml.pauli.pauli_sentence(op1)
+        if not isinstance(op2, PauliSentence):
+            op2 = qml.pauli.pauli_sentence(op2)
+        return op1 @ op2 - op2 @ op1
+
+    with qml.QueuingManager.stop_recording():
+        if isinstance(op1, (PauliWord, PauliSentence)):
+            op1 = op1.operation()
+        if isinstance(op2, (PauliWord, PauliSentence)):
+            op2 = op2.operation()
+
+        res = qml.sum(qml.prod(op1, op2), qml.s_prod(-1.0, qml.prod(op2, op1)))
+        res = res.simplify()
+    return res
+
+
+comm = commutator  # Slightly shorter alias
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
index 3fe4ceced..efa99f304 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -269,7 +269,6 @@ def _equal_operators(
 # pylint: disable=unused-argument, protected-access
 def _equal_prod_and_sum(op1: CompositeOp, op2: CompositeOp, **kwargs):
     """Determine whether two Prod or Sum objects are equal"""
-
     if len(op1.operands) != len(op2.operands):
         return False
 
diff --git a/pennylane/pauli/conversion.py b/pennylane/pauli/conversion.py
index f9c2210f6..395a0dbd6 100644
--- a/pennylane/pauli/conversion.py
+++ b/pennylane/pauli/conversion.py
@@ -340,6 +340,13 @@ def pauli_sentence(op):
     Returns:
         .PauliSentence: the PauliSentence representation of an arithmetic operator or Hamiltonian
     """
+
+    if isinstance(op, PauliWord):
+        return PauliSentence({op: 1.0})
+
+    if isinstance(op, PauliSentence):
+        return op
+
     if (ps := op.pauli_rep) is not None:
         return ps
 


Test patch:
diff --git a/tests/ops/functions/test_commutator.py b/tests/ops/functions/test_commutator.py
new file mode 100644
index 000000000..c0ff2a64f
--- /dev/null
+++ b/tests/ops/functions/test_commutator.py
@@ -0,0 +1,225 @@
+# Copyright 2024 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Unit tests for the comm function
+"""
+# pylint: disable=too-many-arguments, unused-variable
+import pytest
+
+import pennylane as qml
+from pennylane.ops import SProd, Sum
+from pennylane.operation import Operator
+from pennylane.pauli import PauliWord, PauliSentence
+
+X, Y, Z, Id = qml.PauliX, qml.PauliY, qml.PauliZ, qml.Identity
+
+X0 = PauliWord({0: "X"})
+Y0 = PauliWord({0: "Y"})
+Z0 = PauliWord({0: "Z"})
+pw_id = PauliWord({})
+
+
+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 TestLegacySupport:
+    """Test support for legacy operator classes like Tensor and Hamiltonian"""
+
+    def test_Hamiltonian_single(self):
+        """Test that Hamiltonians get transformed to new operator classes and return the correct result"""
+        H1 = qml.Hamiltonian([1.0], [qml.PauliX(0)])
+        H2 = qml.Hamiltonian([1.0], [qml.PauliY(0)])
+        res = qml.commutator(H1, H2)
+        true_res = qml.s_prod(2j, qml.PauliZ(0))
+        assert isinstance(res, SProd)
+        assert true_res == res
+
+    @pytest.mark.xfail
+    def test_Hamiltonian_sum(self):
+        """Test that Hamiltonians with Tensors and sums get transformed to new operator classes and return the correct result"""
+        H1 = qml.Hamiltonian([1.0], [qml.PauliX(0) @ qml.PauliX(1)])
+        H2 = qml.Hamiltonian([1.0], [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.commutator(H1, H2).simplify()
+        assert isinstance(res, Sum)
+        assert qml.equal(
+            true_res, res
+        )  # issue https://github.com/PennyLaneAI/pennylane/issues/5060 as well as potential fix https://github.com/PennyLaneAI/pennylane/pull/5037
+
+
+def test_alias():
+    """Test that the alias comm() works as expected"""
+    res1 = qml.comm(X0, Y0)
+    res1_true = qml.commutator(X0, Y0)
+    assert res1 == res1_true
+    res2 = qml.comm(X0, Y0, pauli=True)
+    res2_true = qml.commutator(X0, Y0, pauli=True)
+    assert res2 == res2_true
+
+
+def test_no_recording_in_context():
+    """Test that commutator is not recorded"""
+    with qml.tape.QuantumTape() as tape:
+        a = qml.PauliX(0)  # gets recorded
+        b = qml.PauliY(0)  # gets recorded
+        comm = qml.commutator(a, b)
+
+    with qml.tape.QuantumTape() as tape2:
+        qml.PauliX(0)
+        qml.PauliY(0)
+
+    assert qml.equal(tape, tape2)
+
+
+def test_no_recording_in_context_with_pauli():
+    """Test that commutator is not recorded while one of the ops is a Pauli"""
+    with qml.tape.QuantumTape() as tape:
+        a = qml.PauliX(0)  # gets recorded
+        b = PauliWord({0: "Y"})  # does not get recorded
+        comm = qml.commutator(a, b)
+
+    with qml.tape.QuantumTape() as tape2:
+        qml.PauliX(0)
+
+    assert qml.equal(tape, tape2)
+
+
+def test_recording_wanted():
+    """Test that commutator can be correctly recorded with qml.apply still"""
+    with qml.tape.QuantumTape() as tape:
+        a = qml.PauliX(0)
+        b = qml.PauliY(0)
+        comm = qml.commutator(a, b)
+        qml.apply(comm)
+
+    with qml.tape.QuantumTape() as tape2:
+        qml.PauliX(0)
+        qml.PauliY(0)
+        qml.s_prod(2j, qml.PauliZ(0))
+
+    assert qml.equal(tape, tape2)
+
+
+class TestcommPauli:
+    """Test qml.comm for pauli=True"""
+
+    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, 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("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_pauli_relations)
+    def test_basic_comm_relations(self, op1, op2, true_res, transform_type1, transform_type2):
+        """Test basic comm relations between Paulis for PauliWord, PauliSentence and Operator instances"""
+        res = qml.commutator(transform_type1(op1), transform_type2(op2), pauli=True)
+        assert res == true_res
+        assert isinstance(res, PauliSentence)
+
+    data_more_comm_relations = (
+        (
+            PauliWord({0: "X", 1: "X"}),
+            PauliWord({0: "Y", 1: "Y"}),
+            PauliSentence({PauliWord({0: "Z", 1: "Z"}): 0.0}),
+        ),
+        (
+            PauliWord({0: "X", 1: "X"}),
+            PauliWord({"a": "X", "b": "Y"}),
+            PauliSentence({PauliWord({0: "X", 1: "X", "a": "X", "b": "Y"}): 0.0}),
+        ),
+    )
+
+    @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):
+        """Test more comm relations between Paulis"""
+        res = qml.commutator(transform_type1(op1), transform_type2(op2), pauli=True)
+        assert res == true_res
+        assert isinstance(res, PauliSentence)
+
+
+class TestcommPauliFalseSimplify:
+    """Test qml.comm for pauli=False (default behavior)"""
+
+    data_pauli_relations_ops = (
+        # word and word
+        (X0, X0, qml.s_prod(0.0, Id(0))),
+        (Y0, Y0, qml.s_prod(0.0, Id(0))),
+        (Z0, Z0, qml.s_prod(0.0, Id(0))),
+        (X0, Y0, qml.s_prod(2j, Z(0))),
+        (Y0, Z0, qml.s_prod(2j, X(0))),
+        (Z0, X0, qml.s_prod(2j, Y(0))),
+        (Y0, X0, qml.s_prod(-2j, Z(0))),
+        (Z0, Y0, qml.s_prod(-2j, X(0))),
+        (X0, Z0, qml.s_prod(-2j, Y(0))),
+    )
+
+    @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_pauli_relations_ops)
+    def test_basic_comm_relations(self, op1, op2, true_res, transform_type1, transform_type2):
+        """Test basic comm relations between Paulis for PauliWord, PauliSentence and Operator instances"""
+        res = qml.commutator(transform_type1(op1), transform_type2(op2), pauli=False)
+        assert res == true_res
+        assert isinstance(res, Operator)
+        assert isinstance(res, SProd)
+
+    data_more_comm_relations_op = (
+        (
+            PauliWord({0: "X", 1: "X"}),
+            PauliWord({0: "Y", 1: "Y"}),
+            qml.s_prod(0.0, Id([0, 1])),
+        ),
+        (
+            PauliWord({0: "X", 1: "X"}),
+            PauliWord({"a": "X", "b": "Y"}),
+            qml.s_prod(0.0, Id([0, 1, "a", "b"])),
+        ),
+    )
+
+    @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_op)
+    def test_comm_relations_pauli_words(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=False)
+        assert res == true_res
+        assert isinstance(res, Operator)
+        assert isinstance(res, SProd)
diff --git a/tests/pauli/test_conversion.py b/tests/pauli/test_conversion.py
index 435ff74b7..9c37d3160 100644
--- a/tests/pauli/test_conversion.py
+++ b/tests/pauli/test_conversion.py
@@ -527,3 +527,25 @@ class TestPauliSentence:
             ValueError, match="Op must be a linear combination of Pauli operators only, got:"
         ):
             pauli_sentence(op)
+
+    words = (
+        PauliWord({0: "X"}),
+        PauliWord({0: "X", 1: "Y"}),
+        PauliWord({"a": "X", 0: "Y"}),
+    )
+
+    @pytest.mark.parametrize("pw", words)
+    def test_trivial_pauli_word(self, pw):
+        """Test that trivially pauli_sentence(pw) returns a PauliSentence with the pw and coeff 1"""
+        res = pauli_sentence(pw)
+        assert res == PauliSentence({pw: 1.0})
+
+    @pytest.mark.parametrize("pw1", words)
+    @pytest.mark.parametrize("pw2", words)
+    def test_trivial_pauli_sentence(self, pw1, pw2):
+        """Test that trivially pauli_sentence(ps) returns the pauli sentence"""
+        ps1 = PauliSentence({pw1: 1.0})
+        ps2 = PauliSentence({pw2: 1.0})
+        ps = 0.5 * ps1 + (-0.5) * ps2
+        res = pauli_sentence(ps)
+        assert res == ps