Repository: PennyLaneAI/pennylane

Issue:
Extend `qml.equal` to `MeasurementProcess` objects
PennyLane now has a `qml.equal` function that is useful for comparing the equality of `Operator`'s.

  We could extend this function to `MeasurementProcess` objects as well.

```pycon
>>> qml.equal(qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(0)) )
True
>>> qml.equal(qml.probs(wires=(0,1)), qml.probs(wires=(1,2)) )
False
```

If both inputs were `MeasurementProcess` objects, we would then need to:
1) compare `return_type`
2) Check the existence of `m.obs`, compare if exists for both
3) Check wires


Discussion:
Hi, I am interested in taking this issue. I have good software experience but I haven't done much Open Source contributions.

Is there an expected date when I should have the PR ready by?

Edit October 9th: I am currently working on a local branch and will push it within the next couple of days.
@kmyali Are you still working on this?
> @kmyali Are you still working on this?

Yes, I am. I got distracted for a bit, but I should be opening my PR within 24 hours. 

Resolving patch:
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -15,44 +15,53 @@
 This module contains the qml.equal function.
 """
 # pylint: disable=too-many-arguments,too-many-return-statements
+from typing import Union
 import pennylane as qml
+from pennylane.measurements import MeasurementProcess, ShadowMeasurementProcess
 from pennylane.operation import Operator
 
 
 def equal(
-    op1: Operator,
-    op2: Operator,
+    op1: Union[Operator, MeasurementProcess, ShadowMeasurementProcess],
+    op2: Union[Operator, MeasurementProcess, ShadowMeasurementProcess],
     check_interface=True,
     check_trainability=True,
     rtol=1e-5,
     atol=1e-9,
 ):
-    r"""Function for determining operator equality.
+    r"""Function for determining operator or measurement equality.
 
     Args:
-        op1 (.Operator): First operator to compare
-        op2 (.Operator): Second operator to compare
+        op1 (.Operator, .MeasurementProcess, or .ShadowMeasurementProcess): First object to compare
+        op2 (.Operator, .MeasurementProcess, or .ShadowMeasurementProcess): 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
 
     Returns:
-        bool: ``True`` if the operators are equal, else ``False``
+        bool: ``True`` if the operators or measurement processes are equal, else ``False``
 
     **Example**
 
-    Given two operators, ``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
 
+    >>> qml.equal(qml.expval(qml.PauliX(0)), qml.expval(qml.PauliX(0)) )
+    >>> True
+    >>> qml.equal(qml.probs(wires=(0,1)), qml.probs(wires=(1,2)) )
+    >>> False
+    >>> qml.equal(qml.classical_shadow(wires=[0,1]), qml.classical_shadow(wires=[0,1]) )
+    >>> True
+
     .. details::
         :title: Usage Details
 
-        You can use the optional arguments to get more specific results.
+        You can use the optional arguments when comparing Operators to get more specific results.
 
         Consider the following comparisons:
 
@@ -72,8 +81,24 @@ def equal(
         >>> qml.equal(op3, op4, check_trainability=False)
         True
     """
-    if op1.__class__ is not op2.__class__ or op1.arithmetic_depth != op2.arithmetic_depth:
+
+    if op1.__class__ is not op2.__class__:
         return False
+
+    if op1.__class__ is MeasurementProcess:
+        return equal_measurements(op1, op2)
+
+    if op1.__class__ is ShadowMeasurementProcess:
+        return equal_shadow_measurements(op1, op2)
+
+    return equal_operator(op1, op2, check_interface, check_trainability, rtol, atol)
+
+
+def equal_operator(op1, op2, check_interface, check_trainability, rtol, atol):
+    """Determine whether two Operator objects are equal"""
+    if op1.arithmetic_depth != op2.arithmetic_depth:
+        return False
+
     if op1.arithmetic_depth > 0:
         raise NotImplementedError(
             "Comparison of operators with an arithmetic depth larger than 0 is not yet implemented."
@@ -99,3 +124,34 @@ def equal(
                 return False
 
     return getattr(op1, "inverse", False) == getattr(op2, "inverse", False)
+
+
+def equal_measurements(op1, op2):
+    """Determine whether two MeasurementProcess objects are equal"""
+    return_types_match = op1.return_type == op2.return_type
+    if op1.obs is not None and op2.obs is not None:
+        observables_match = equal(op1.obs, op2.obs)
+    # check obs equality when either one is None (False) or both are None (True)
+    else:
+        observables_match = op1.obs == op2.obs
+    wires_match = op1.wires == op2.wires
+    eigvals_match = qml.math.allequal(op1.eigvals(), op2.eigvals())
+    log_base_match = op1.log_base == op2.log_base
+
+    return (
+        return_types_match
+        and observables_match
+        and wires_match
+        and eigvals_match
+        and log_base_match
+    )
+
+
+def equal_shadow_measurements(op1, op2):
+    """Determine whether two ShadowMeasurementProcess objects are equal"""
+    return_types_match = op1.return_type == op2.return_type
+    wires_match = op1.wires == op2.wires
+    H_match = op1.H == op2.H
+    k_match = op1.k == op2.k
+
+    return return_types_match and wires_match and H_match and k_match


Test patch:
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -22,6 +22,7 @@
 
 import pennylane as qml
 from pennylane import numpy as npp
+from pennylane.measurements import MeasurementProcess, ObservableReturnTypes
 
 PARAMETRIZED_OPERATIONS_1P_1W = [
     qml.RX,
@@ -107,6 +108,59 @@
 )
 
 
+PARAMETRIZED_MEASUREMENTS = [
+    qml.sample(qml.PauliY(0)),
+    qml.sample(wires=0),
+    qml.sample(),
+    qml.counts(qml.PauliZ(0)),
+    qml.counts(wires=[0, 1]),
+    qml.counts(wires=[1, 0]),
+    qml.counts(),
+    qml.density_matrix(wires=0),
+    qml.density_matrix(wires=1),
+    qml.var(qml.PauliY(1)),
+    qml.var(qml.PauliY(0)),
+    qml.expval(qml.PauliX(0)),
+    qml.expval(qml.PauliX(1)),
+    qml.probs(wires=1),
+    qml.probs(wires=0),
+    qml.probs(qml.PauliZ(0)),
+    qml.probs(qml.PauliZ(1)),
+    qml.state(),
+    qml.vn_entropy(wires=0),
+    qml.vn_entropy(wires=0, log_base=np.e),
+    qml.mutual_info(wires0=[0], wires1=[1]),
+    qml.mutual_info(wires0=[1], wires1=[0]),
+    qml.mutual_info(wires0=[1], wires1=[0], log_base=2),
+    qml.classical_shadow(wires=[0, 1]),
+    qml.classical_shadow(wires=[1, 0]),
+    qml.shadow_expval(
+        H=qml.Hamiltonian(
+            [1.0, 1.0], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliX(1)]
+        ),
+        k=2,
+    ),
+    qml.shadow_expval(
+        H=qml.Hamiltonian(
+            [1.0, 1.0], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliX(0) @ qml.PauliX(1)]
+        )
+    ),
+    qml.shadow_expval(
+        H=qml.Hamiltonian(
+            [1.0, 1.0], [qml.PauliX(0) @ qml.PauliX(1), qml.PauliZ(0) @ qml.PauliZ(1)]
+        )
+    ),
+    MeasurementProcess(ObservableReturnTypes.Expectation, eigvals=[1, -1]),
+    MeasurementProcess(ObservableReturnTypes.Expectation, eigvals=[1, 2]),
+]
+PARAMETRIZED_MEASUREMENTS_COMBINATIONS = list(
+    itertools.combinations(
+        PARAMETRIZED_MEASUREMENTS,
+        2,
+    )
+)
+
+
 class TestEqual:
     @pytest.mark.parametrize("ops", PARAMETRIZED_OPERATIONS_COMBINATIONS)
     def test_equal_simple_diff_op(self, ops):
@@ -940,3 +994,20 @@ def test_not_equal_different_inversion(self):
         op1 = qml.PauliX(0)
         op2 = qml.PauliX(0).inv()
         assert not qml.equal(op1, op2)
+
+    # Measurements test cases
+    @pytest.mark.parametrize("ops", PARAMETRIZED_MEASUREMENTS_COMBINATIONS)
+    def test_not_equal_diff_measurement(self, ops):
+        """Test different measurements return False"""
+        assert not qml.equal(ops[0], ops[1])
+
+    @pytest.mark.parametrize("op1", PARAMETRIZED_MEASUREMENTS)
+    def test_equal_same_measurement(self, op1):
+        """Test same measurements return True"""
+        assert qml.equal(op1, op1)
+
+    @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS)
+    @pytest.mark.parametrize("op2", PARAMETRIZED_MEASUREMENTS)
+    def test_not_equal_operator_measurement(self, op1, op2):
+        """Test operator not equal to measurement"""
+        assert not qml.equal(op1, op2)