Repository: PennyLaneAI/pennylane
Pull request #3463 (2022-12-02 19:24:10)

qml.equal for Controlled operators
**Context:**
qml.equal doesn't can't compare two `Controlled` objects. Currently a `NotImplementedError` is raised.

**Description of the Change:**
Add a function to compare two `Controlled` or `ControlledOp` objects.


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

```diff
@@           Coverage Diff           @@
##           master    #3463   +/-   ##
=======================================
  Coverage   99.68%   99.68%           
=======================================
  Files         307      307           
  Lines       26979    26987    +8     
=======================================
+ Hits        26894    26902    +8     
  Misses         85       85           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3463?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/3463/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9mdW5jdGlvbnMvZXF1YWwucHk=) | `98.88% <100.00%> (+0.10%)` | :arrow_up: |

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 fa637f2b7..4f99ff8da 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -261,9 +261,13 @@
   Replaces `qml.transforms.make_tape` with `make_qscript`.
   [(#3429)](https://github.com/PennyLaneAI/pennylane/pull/3429)
 
+* Extended the `qml.equal` function to `Controlled` and `ControlledOp` objects.
+  [(#3463)](https://github.com/PennyLaneAI/pennylane/pull/3463)
+
 * Replace (almost) all instances of `with QuantumTape()` with `QuantumScript` construction.
   [(#3454)](https://github.com/PennyLaneAI/pennylane/pull/3454)
 
+
 <h4>Return types project</h4>
 
 * The autograd interface for the new return types now supports devices with shot vectors.
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
index 1dc8a4694..5936d78ad 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -24,7 +24,7 @@ from pennylane.measurements.classical_shadow import _ShadowExpval
 from pennylane.measurements.mutual_info import _MutualInfo
 from pennylane.measurements.vn_entropy import _VnEntropy
 from pennylane.operation import Observable, Operator, Tensor
-from pennylane.ops import Hamiltonian
+from pennylane.ops import Hamiltonian, Controlled
 
 
 def equal(
@@ -101,7 +101,8 @@ def equal(
         :title: Usage Details
 
         You can use the optional arguments to get more specific results. These arguments are, however, not used
-        for comparing ``MeasurementProcess``, ``Hamiltonian`` or ``Tensor`` objects.
+        for comparing ``MeasurementProcess``, ``Hamiltonian`` or ``Tensor`` objects. Additionally, they are
+        applied only to the base operator of `Controlled` operators.
 
         Consider the following comparisons:
 
@@ -120,6 +121,12 @@ def equal(
 
         >>> qml.equal(op3, op4, check_trainability=False)
         True
+
+        >>> qml.equal(Controlled(op3, control_wires=1), Controlled(op4, control_wires=1))
+        False
+
+        >>> qml.equal(Controlled(op3, control_wires=1), Controlled(op4, control_wires=1), check_trainability=False)
+        True
     """
 
     # types don't have to be the same type, they just both have to be Observables
@@ -197,6 +204,25 @@ def _equal_operators(
     return getattr(op1, "inverse", False) == getattr(op2, "inverse", False)
 
 
+@_equal.register
+def _equal_controlled(op1: Controlled, op2: Controlled, **kwargs):
+    """Determine whether two Controlled or ControlledOp objects are equal"""
+    # wires are ordered [control wires, operator wires, work wires]
+    # comparing op.wires and op.base.wires (in return) is sufficient to compare all wires
+    if [op1.wires, op1.control_values, op1.arithmetic_depth] != [
+        op2.wires,
+        op2.control_values,
+        op2.arithmetic_depth,
+    ]:
+        return False
+    try:
+        return qml.equal(op1.base, op2.base, **kwargs)
+    except NotImplementedError as e:
+        raise NotImplementedError(
+            f"Unable to compare base operators {op1.base} and {op2.base}."
+        ) from e
+
+
 @_equal.register
 # pylint: disable=unused-argument
 def _equal_tensor(op1: Tensor, op2: Observable, **kwargs):


Test patch:
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
index 90da53a6e..2db196211 100644
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -23,6 +23,7 @@ import pytest
 import pennylane as qml
 from pennylane import numpy as npp
 from pennylane.measurements import _Expectation
+from pennylane.ops.op_math.controlled_class import Controlled
 
 PARAMETRIZED_OPERATIONS_1P_1W = [
     qml.RX,
@@ -1196,3 +1197,87 @@ class TestObservablesComparisons:
 
         with pytest.raises(NotImplementedError, match="Comparison of"):
             qml.equal(dev, dev)
+
+
+class TestControlledComparisons:
+    WIRES = [(5, 5, True), (6, 7, False)]
+
+    @pytest.mark.parametrize("base", PARAMETRIZED_OPERATIONS)
+    def test_controlled_comparison(self, base):
+        """Test that Controlled operators can be compared"""
+        op1 = Controlled(base, control_wires=7, control_values=0)
+        op2 = Controlled(base, control_wires=7, control_values=0)
+        assert qml.equal(op1, op2) == True
+
+    @pytest.mark.parametrize(
+        ("base1", "base2", "res"),
+        [(qml.PauliX(0), qml.PauliX(0), True), (qml.PauliX(0), qml.PauliY(0), False)],
+    )
+    def test_base_operator_comparison(self, base1, base2, res):
+        """Test that equal compares base operators for Controlled operators"""
+        op1 = Controlled(base1, control_wires=1)
+        op2 = Controlled(base2, control_wires=1)
+        assert qml.equal(op1, op2) == res
+
+    @pytest.mark.parametrize(("wire1", "wire2", "res"), WIRES)
+    def test_base_operator_wire_comparison(self, wire1, wire2, res):
+        """Test that equal compares operator wires for Controlled operators"""
+        base1 = qml.PauliX(wire1)
+        base2 = qml.PauliX(wire2)
+        op1 = Controlled(base1, control_wires=1)
+        op2 = Controlled(base2, control_wires=1)
+        assert qml.equal(op1, op2) == res
+
+    @pytest.mark.parametrize(("wire1", "wire2", "res"), WIRES)
+    def test_control_wires_comparison(self, wire1, wire2, res):
+        """Test that equal compares control_wires for Controlled operators"""
+        base1 = qml.Hadamard(0)
+        base2 = qml.Hadamard(0)
+        op1 = Controlled(base1, control_wires=wire1)
+        op2 = Controlled(base2, control_wires=wire2)
+        assert qml.equal(op1, op2) == res
+
+    @pytest.mark.parametrize(("wire1", "wire2", "res"), WIRES)
+    def test_work_wires_comparison(self, wire1, wire2, res):
+        """Test that equal compares work_wires for Controlled operators"""
+        base1 = qml.MultiRZ(1.23, [0, 1])
+        base2 = qml.MultiRZ(1.23, [0, 1])
+        op1 = Controlled(base1, control_wires=2, work_wires=wire1)
+        op2 = Controlled(base2, control_wires=2, work_wires=wire2)
+        assert qml.equal(op1, op2) == res
+
+    def test_mismatched_arithmetic_depth(self):
+        """Test that comparing Controlled operators of mismatched arithmetic depth returns False"""
+        base1 = qml.PauliX(0)
+        base2 = qml.prod(qml.PauliX(0), qml.PauliY(1))
+        op1 = Controlled(base1, control_wires=2)
+        op2 = Controlled(base2, control_wires=2)
+
+        assert op1.arithmetic_depth == 1
+        assert op2.arithmetic_depth == 2
+        assert qml.equal(op1, op2) == False
+
+    def test_comparison_of_base_not_implemented_error(self):
+        """Test that comparing Controlled operators of base operators whose comparison is not yet implemented raises an error"""
+        base = qml.prod(qml.RX(1.2, 0), qml.RY(1.3, 1))
+        op1 = Controlled(base, control_wires=2)
+        op2 = Controlled(base, control_wires=2)
+
+        with pytest.raises(NotImplementedError, match="Unable to compare base operators "):
+            qml.equal(op1, op2)
+
+    @pytest.mark.torch
+    @pytest.mark.jax
+    def test_kwargs_for_base_operator_comparison(self):
+        """Test that setting kwargs check_interface and check_trainability are applied when comparing the base operators"""
+        import torch
+        import jax
+
+        base1 = qml.RX(torch.tensor(1.2), wires=0)
+        base2 = qml.RX(jax.numpy.array(1.2), wires=0)
+
+        op1 = Controlled(base1, control_wires=1)
+        op2 = Controlled(base2, control_wires=1)
+
+        assert qml.equal(op1, op2) == False
+        assert qml.equal(op1, op2, check_interface=False, check_trainability=False) == True