Repository: PennyLaneAI/pennylane
Pull request #3390 (2022-11-18 15:44:18)

qml.equal for Hamiltonians and Tensors
**Description of the Change:**
Extend `qml.equal` to also compare `Observables` with one another by implementing the logic already used in `Hamiltonian.compare`

**Possible Drawbacks:**
This does not work if comparing Tensors that are not, mathematically, tensors. Assuming defining dot products using @ is supported, I would expect this to return False. However, it returns True:

```
op1 = qml.PauliX(0) @ qml.PauliY(0)
op2 = qml.PauliY(0) @ qml.PauliX(0)
```

This is an existing issue with `Hamiltonian.compare` that would be extended to `qml.equal`:

```
H = qml.Hamiltonian([1], [op2])
H.compare(op1)
>>> True
```



codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3390?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3390](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3390?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (5dfe9f3) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/cd2a21136a7c853a9e7b6da3c9242af9020041eb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (cd2a211) will **decrease** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master    #3390      +/-   ##
==========================================
- Coverage   99.70%   99.69%   -0.01%     
==========================================
  Files         298      298              
  Lines       26172    26184      +12     
==========================================
+ Hits        26094    26105      +11     
- Misses         78       79       +1     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3390?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/functions/equal.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3390/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9mdW5jdGlvbnMvZXF1YWwucHk=) | `98.41% <100.00%> (-1.59%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index fa316583a..904cddf68 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -133,6 +133,9 @@
   fails to diagonalize non-qubit-wise-commuting observables.
   [(#3381)](https://github.com/PennyLaneAI/pennylane/pull/3381)
 
+* Extended the `qml.equal` function to `Hamiltonian` and `Tensor` objects.
+  [(#3390)](https://github.com/PennyLaneAI/pennylane/pull/3390)
+
 * Remove private `_wires` setter from the `Controlled.map_wires` method.
   [3405](https://github.com/PennyLaneAI/pennylane/pull/3405)
   
@@ -198,6 +201,7 @@
          [-0.37133333, -0.18566667,  0.        ,  0.        ,  0.18566667]])
   ```
 
+
 <h3>Breaking changes</h3>
 
 * The `log_base` attribute has been moved from `MeasurementProcess` to the new `_VnEntropy` and
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
index d75b6bfdc..7a7c15711 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -14,13 +14,14 @@
 """
 This module contains the qml.equal function.
 """
-# pylint: disable=too-many-arguments,too-many-return-statements
+# pylint: disable=too-many-arguments,too-many-return-statements, too-many-branches
 from typing import Union
 
 from functools import singledispatch
 import pennylane as qml
 from pennylane.measurements import MeasurementProcess, ShadowMeasurementProcess
-from pennylane.operation import Operator
+from pennylane.operation import Operator, Observable, Tensor
+from pennylane.ops.qubit.hamiltonian import Hamiltonian
 
 
 def equal(
@@ -33,26 +34,56 @@ def equal(
 ):
     r"""Function for determining operator or measurement equality.
 
+    .. Warning::
+
+        The equal function does **not** check if the matrix representation
+        of a :class:`~.Hermitian` observable is equal to an equivalent
+        observable expressed in terms of Pauli matrices, or as a
+        linear combination of Hermitians.
+        To do so would require the matrix form of Hamiltonians and Tensors
+        be calculated, which may drastically increase runtime.
+
+        The kwargs ``check_interface`` and ``check_trainability`` can only be set when
+        comparing ``Operation`` objects. Comparisons of ``MeasurementProcess`` or ``Observable``
+        objects will use the defualt value of ``True`` for both, regardless of what the user
+        specifies when calling the function.
+
     Args:
         op1 (.Operator or .MeasurementProcess): First object to compare
         op2 (.Operator or .MeasurementProcess): Second object to compare
-        check_interface (bool, optional): Whether to compare interfaces. Default: ``True``
-        check_trainability (bool, optional): Whether to compare trainability status. Default: ``True``
-        rtol (float, optional): Relative tolerance for parameters
-        atol (float, optional): Absolute tolerance for parameters
+        check_interface (bool, optional): Whether to compare interfaces. Default: ``True``. Not used for comparing ``MeasurementProcess``, ``Hamiltonian`` or ``Tensor`` objects.
+        check_trainability (bool, optional): Whether to compare trainability status. Default: ``True``. Not used for comparing ``MeasurementProcess``, ``Hamiltonian`` or ``Tensor`` objects.
+        rtol (float, optional): Relative tolerance for parameters. Not used for comparing ``MeasurementProcess``, ``Hamiltonian`` or ``Tensor`` objects.
+        atol (float, optional): Absolute tolerance for parameters. Not used for comparing ``MeasurementProcess``, ``Hamiltonian`` or ``Tensor`` objects.
 
     Returns:
         bool: ``True`` if the operators or measurement processes are equal, else ``False``
 
     **Example**
 
-    Given two operators or measurement processes, ``qml.equal`` determines their equality:
+    Given two operators or measurement processes, ``qml.equal`` determines their equality.
 
     >>> op1 = qml.RX(np.array(.12), wires=0)
     >>> op2 = qml.RY(np.array(1.23), wires=0)
     >>> qml.equal(op1, op1), qml.equal(op1, op2)
     (True, False)
 
+    >>> T1 = qml.PauliX(0) @ qml.PauliY(1)
+    >>> T2 = qml.PauliY(1) @ qml.PauliX(0)
+    >>> T3 = qml.PauliX(1) @ qml.PauliY(0)
+    >>> qml.equal(T1, T2), qml.equal(T1, T3)
+    (True, False)
+
+    >>> T = qml.PauliX(0) @ qml.PauliY(1)
+    >>> H = qml.Hamiltonian([1], [qml.PauliX(0) @ qml.PauliY(1)])
+    >>> qml.equal(T, H)
+    True
+
+    >>> H1 = qml.Hamiltonian([0.5, 0.5], [qml.PauliZ(0) @ qml.PauliY(1), qml.PauliY(1) @ qml.PauliZ(0) @ qml.Identity("a")])
+    >>> H2 = qml.Hamiltonian([1], [qml.PauliZ(0) @ qml.PauliY(1)])
+    >>> H3 = qml.Hamiltonian([2], [qml.PauliZ(0) @ qml.PauliY(1)])
+    >>> qml.equal(H1, H2), qml.equal(H1, H3)
+    (True, False)
 
     >>> qml.equal(qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(0)) )
     True
@@ -62,10 +93,12 @@ def equal(
     True
 
 
+
     .. details::
         :title: Usage Details
 
-        You can use the optional arguments when comparing Operators to get more specific results.
+        You can use the optional arguments to get more specific results. These arguments are, however, not used
+        for comparing ``MeasurementProcess``, ``Hamiltonian`` or ``Tensor`` objects.
 
         Consider the following comparisons:
 
@@ -86,9 +119,13 @@ def equal(
         True
     """
 
-    if not isinstance(op2, type(op1)):
+    # types don't have to be the same type, they just both have to be Observables
+    if not isinstance(op2, type(op1)) and not isinstance(op1, Observable):
         return False
 
+    if isinstance(op2, (Hamiltonian, Tensor)):
+        return _equal(op2, op1)
+
     return _equal(
         op1,
         op2,
@@ -103,14 +140,16 @@ def equal(
 def _equal(
     op1,
     op2,
-    **kwargs,
+    check_interface=True,
+    check_trainability=True,
+    rtol=1e-5,
+    atol=1e-9,
 ):
-
-    raise NotImplementedError(f"Comparison between {type(op1)} and {type(op2)} not implemented.")
+    raise NotImplementedError(f"Comparison of {type(op1)} and {type(op2)} not implemented")
 
 
 @_equal.register
-def _equal_operator(
+def _equal_operators(
     op1: Operator,
     op2: Operator,
     check_interface=True,
@@ -118,8 +157,12 @@ def _equal_operator(
     rtol=1e-5,
     atol=1e-9,
 ):
+    """Default function to determine whether two Operator objects are equal."""
 
-    """Determine whether two Operator objects are equal"""
+    if not isinstance(
+        op2, type(op1)
+    ):  # clarifies cases involving PauliX/Y/Z (Observable/Operation)
+        return False
 
     if op1.arithmetic_depth != op2.arithmetic_depth:
         return False
@@ -151,9 +194,35 @@ def _equal_operator(
     return getattr(op1, "inverse", False) == getattr(op2, "inverse", False)
 
 
+@_equal.register
+# pylint: disable=unused-argument
+def _equal_tensor(op1: Tensor, op2: Observable, **kwargs):
+    """Determine whether a Tensor object is equal to a Hamiltonian/Tensor"""
+    if not isinstance(op2, Observable):
+        return False
+
+    if isinstance(op2, Hamiltonian):
+        return op2.compare(op1)
+
+    if isinstance(op2, Tensor):
+        return op1._obs_data() == op2._obs_data()  # pylint: disable=protected-access
+
+    raise NotImplementedError(f"Comparison of {type(op1)} and {type(op2)} not implemented")
+
+
+@_equal.register
+# pylint: disable=unused-argument
+def _equal_hamiltonian(op1: Hamiltonian, op2: Observable, **kwargs):
+    """Determine whether a Hamiltonian object is equal to a Hamiltonian/Tensor objects"""
+    if not isinstance(op2, Observable):
+        return False
+    return op1.compare(op2)
+
+
 @_equal.register
 # pylint: disable=unused-argument
 def _equal_measurements(op1: MeasurementProcess, op2: MeasurementProcess, **kwargs):
+
     """Determine whether two MeasurementProcess objects are equal"""
 
     return_types_match = op1.return_type == op2.return_type


Test patch:
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
index c06bad31e..f72e30d4e 100644
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -161,16 +161,139 @@ PARAMETRIZED_MEASUREMENTS_COMBINATIONS = list(
 )
 
 
+equal_hamiltonians = [
+    (
+        qml.Hamiltonian([1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliZ(0)]),
+        qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(0)]),
+        True,
+    ),
+    (
+        qml.Hamiltonian([1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliY(2) @ qml.PauliZ(0)]),
+        qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(0) @ qml.PauliY(2) @ qml.Identity(1)]),
+        True,
+    ),
+    (
+        qml.Hamiltonian(
+            [1, 1, 1], [qml.PauliX(0) @ qml.Identity(1), qml.PauliZ(0), qml.Identity(1)]
+        ),
+        qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(0)]),
+        False,
+    ),
+    (
+        qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]),
+        qml.Hamiltonian([1, 1], [qml.PauliX(1), qml.PauliZ(0)]),
+        False,
+    ),
+    (
+        qml.Hamiltonian([1, 2], [qml.PauliX(0), qml.PauliZ(1)]),
+        qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliZ(1)]),
+        False,
+    ),
+    (
+        qml.Hamiltonian([1, 1], [qml.PauliX("a"), qml.PauliZ("b")]),
+        qml.Hamiltonian([1, 1], [qml.PauliX("a"), qml.PauliZ("b")]),
+        True,
+    ),
+    (
+        qml.Hamiltonian([1, 2], [qml.PauliX("a"), qml.PauliZ("b")]),
+        qml.Hamiltonian([1, 1], [qml.PauliX("b"), qml.PauliZ("a")]),
+        False,
+    ),
+    (qml.Hamiltonian([1], [qml.PauliZ(0) @ qml.PauliX(1)]), qml.PauliZ(0) @ qml.PauliX(1), True),
+    (qml.Hamiltonian([1], [qml.PauliZ(0)]), qml.PauliZ(0), True),
+    (
+        qml.Hamiltonian(
+            [1, 1, 1],
+            [
+                qml.Hermitian(np.array([[1, 0], [0, -1]]), "b") @ qml.Identity(7),
+                qml.PauliZ(3),
+                qml.Identity(1.2),
+            ],
+        ),
+        qml.Hamiltonian(
+            [1, 1, 1],
+            [qml.Hermitian(np.array([[1, 0], [0, -1]]), "b"), qml.PauliZ(3), qml.Identity(1.2)],
+        ),
+        True,
+    ),
+    (
+        qml.Hamiltonian([1, 1], [qml.PauliZ(3) @ qml.Identity(1.2), qml.PauliZ(3)]),
+        qml.Hamiltonian([2], [qml.PauliZ(3)]),
+        True,
+    ),
+]
+
+equal_tensors = [
+    (qml.PauliX(0) @ qml.PauliY(1), qml.PauliY(1) @ qml.PauliX(0), True),
+    (qml.PauliX(0) @ qml.Identity(1) @ qml.PauliZ(2), qml.PauliX(0) @ qml.PauliZ(2), True),
+    (qml.PauliX(0) @ qml.Identity(2) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliZ(2), False),
+    (qml.PauliX(0) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliZ(2), False),
+    (qml.PauliX("a") @ qml.PauliZ("b"), qml.PauliX("a") @ qml.PauliZ("b"), True),
+    (qml.PauliX("a") @ qml.PauliZ("b"), qml.PauliX("c") @ qml.PauliZ("d"), False),
+    (qml.PauliX("a") @ qml.PauliZ("b"), qml.PauliX("b") @ qml.PauliZ("a"), False),
+    (qml.PauliX(1.1) @ qml.PauliZ(1.2), qml.PauliX(1.1) @ qml.PauliZ(1.2), True),
+    (qml.PauliX(1.1) @ qml.PauliZ(1.2), qml.PauliX(1.2) @ qml.PauliZ(0.9), False),
+]
+
+equal_hamiltonians_and_tensors = [
+    (qml.Hamiltonian([1], [qml.PauliX(0) @ qml.PauliY(1)]), qml.PauliY(1) @ qml.PauliX(0), True),
+    (
+        qml.Hamiltonian(
+            [0.5, 0.5],
+            [qml.PauliZ(0) @ qml.PauliY(1), qml.PauliY(1) @ qml.PauliZ(0) @ qml.Identity("a")],
+        ),
+        qml.PauliZ(0) @ qml.PauliY(1),
+        True,
+    ),
+    (qml.Hamiltonian([1], [qml.PauliX(0) @ qml.PauliY(1)]), qml.PauliX(0) @ qml.PauliY(1), True),
+    (qml.Hamiltonian([2], [qml.PauliX(0) @ qml.PauliY(1)]), qml.PauliX(0) @ qml.PauliY(1), False),
+    (qml.Hamiltonian([1], [qml.PauliX(0) @ qml.PauliY(1)]), qml.PauliX(4) @ qml.PauliY(1), False),
+    (
+        qml.Hamiltonian([1], [qml.PauliX("a") @ qml.PauliZ("b")]),
+        qml.PauliX("a") @ qml.PauliZ("b"),
+        True,
+    ),
+    (
+        qml.Hamiltonian([1], [qml.PauliX("a") @ qml.PauliZ("b")]),
+        qml.PauliX("b") @ qml.PauliZ("a"),
+        False,
+    ),
+    (
+        qml.Hamiltonian([1], [qml.PauliX(1.2) @ qml.PauliZ(0.2)]),
+        qml.PauliX(1.2) @ qml.PauliZ(0.2),
+        True,
+    ),
+    (
+        qml.Hamiltonian([1], [qml.PauliX(1.2) @ qml.PauliZ(0.2)]),
+        qml.PauliX(1.3) @ qml.PauliZ(2),
+        False,
+    ),
+]
+
+equal_pauli_operators = [
+    (qml.PauliX(0), qml.PauliX(0), True),
+    (qml.PauliY("a"), qml.PauliY("a"), True),
+    (qml.PauliY(0.3), qml.PauliY(0.3), True),
+    (qml.PauliX(0), qml.PauliX(1), False),
+    (qml.PauliY("a"), qml.PauliY("b"), False),
+    (qml.PauliY(0.3), qml.PauliY(0.7), False),
+    (qml.PauliY(0), qml.PauliX(0), False),
+    (qml.PauliY("a"), qml.PauliX("a"), False),
+    (qml.PauliZ(0.3), qml.PauliY(0.3), False),
+    (qml.PauliZ(0), qml.RX(1.23, 0), False),
+    (qml.Hamiltonian([1], [qml.PauliX("a")]), qml.PauliX("a"), True),
+    (qml.Hamiltonian([1], [qml.PauliX("a")]), qml.PauliX("b"), False),
+    (qml.Hamiltonian([1], [qml.PauliX(1.2)]), qml.PauliX(1.2), True),
+    (qml.Hamiltonian([1], [qml.PauliX(1.2)]), qml.PauliX(1.3), False),
+]
+
+
 class TestEqual:
     @pytest.mark.parametrize("ops", PARAMETRIZED_OPERATIONS_COMBINATIONS)
     def test_equal_simple_diff_op(self, ops):
         """Test different operators return False"""
         assert not qml.equal(ops[0], ops[1], check_trainability=False, check_interface=False)
 
-    def test_equal_unsupported_op_type(self):
-        with pytest.raises(NotImplementedError, match="Comparison between "):
-            qml.equal(3, 4)
-
     @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS)
     def test_equal_simple_same_op(self, op1):
         """Test same operators return True"""
@@ -975,7 +1098,9 @@ class TestEqual:
 
     def test_equal_with_different_arithmetic_depth(self):
         """Test equal method with two operators with different arithmetic depth."""
-        assert not qml.equal(qml.adjoint(qml.PauliX(0)), qml.adjoint(qml.adjoint(qml.PauliX(0))))
+        op1 = qml.RX(0.3, wires=0)
+        op2 = qml.prod(op1, qml.RY(0.25, wires=1))
+        assert not qml.equal(op1, op2)
 
     def test_equal_with_nested_operators_raises_error(self):
         """Test that the equal method with two operators with the same arithmetic depth (>0) raises
@@ -985,7 +1110,7 @@ class TestEqual:
             match="Comparison of operators with an arithmetic"
             + " depth larger than 0 is not yet implemented.",
         ):
-            qml.equal(qml.adjoint(qml.PauliX(0)), qml.adjoint(qml.PauliX(0)))
+            qml.equal(qml.adjoint(qml.RX(1.2, 0)), qml.adjoint(qml.RX(1.2, 0)))
 
     def test_equal_same_inversion(self):
         """Test operations are equal if they are both inverted."""
@@ -1015,3 +1140,59 @@ class TestEqual:
     def test_not_equal_operator_measurement(self, op1, op2):
         """Test operator not equal to measurement"""
         assert not qml.equal(op1, op2)
+
+
+class TestObservablesComparisons:
+    """Tests comparisons between Hamiltonians, Tensors and PauliX/Y/Z operators"""
+
+    @pytest.mark.parametrize(("H1", "H2", "res"), equal_hamiltonians)
+    def test_hamiltonian_equal(self, H1, H2, res):
+        """Tests that equality can be checked between Hamiltonians"""
+        assert qml.equal(H1, H2) == qml.equal(H2, H1)
+        assert qml.equal(H1, H2) == res
+
+    @pytest.mark.parametrize(("T1", "T2", "res"), equal_tensors)
+    def test_tensors_equal(self, T1, T2, res):
+        """Tests that equality can be checked between Tensors"""
+        assert qml.equal(T1, T2) == qml.equal(T2, T1)
+        assert qml.equal(T1, T2) == res
+
+    @pytest.mark.parametrize(("H", "T", "res"), equal_hamiltonians_and_tensors)
+    def test_hamiltonians_and_tensors_equal(self, H, T, res):
+        """Tests that equality can be checked between a Hamiltonian and a Tensor"""
+        assert qml.equal(H, T) == qml.equal(T, H)
+        assert qml.equal(H, T) == res
+
+    @pytest.mark.parametrize(("op1", "op2", "res"), equal_pauli_operators)
+    def test_pauli_operator_equals(self, op1, op2, res):
+        """Tests that equality can be checked between PauliX/Y/Z operators, and between Pauli operators and Hamiltonians"""
+        assert qml.equal(op1, op2) == qml.equal(op2, op1)
+        assert qml.equal(op1, op2) == res
+
+    def test_hamiltonian_and_operation_not_equal(self):
+        """Tests that comparing a Hamiltonian with an Operator that is not an Observable returns False"""
+        op1 = qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliY(0)])
+        op2 = qml.RX(1.2, 0)
+        assert qml.equal(op1, op2) == False
+        assert qml.equal(op2, op1) == False
+
+    def test_tensor_and_operation_not_equal(self):
+        """Tests that comparing a Tensor with an Operator that is not an Observable returns False"""
+        op1 = qml.PauliX(0) @ qml.PauliY(1)
+        op2 = qml.RX(1.2, 0)
+        assert qml.equal(op1, op2) == False
+        assert qml.equal(op2, op1) == False
+
+    def test_tensor_and_unsupported_observable_not_implemented(self):
+        """Tests that trying to compare a Tensor to something other than another Tensor or a Hamiltonian raises a NotImplmenetedError"""
+        op1 = qml.PauliX(0) @ qml.PauliY(1)
+        op2 = qml.Hermitian([[0, 1], [1, 0]], 0)
+
+        with pytest.raises(NotImplementedError, match="Comparison of"):
+            qml.equal(op1, op2)
+
+    def test_unsupported_object_type_not_implemented(self):
+        dev = qml.device("default.qubit", wires=1)
+
+        with pytest.raises(NotImplementedError, match="Comparison of"):
+            qml.equal(dev, dev)