Repository: PennyLaneAI/pennylane
Pull request #4944 (2023-12-12 20:52:30)

Correctly compare Controlled operators with differing control wire order
**Context:**
As long as the mapping of control wires to control values is the same, the order of control wires doesn't matter when comparing two Controlled operators, but the current implementation of `qml.equal` doesn't consider this. This currently returns False, because the wires differ in order:
```
base1 = qml.PauliX(wires=0)
base2 = qml.PauliX(wires=0)

op1 = qml.ops.op_math.Controlled(base1, control_wires=[1, 2])
op2 = qml.ops.op_math.Controlled(base2, control_wires=[2, 1])
print(qml.equal(op1, op2))
```

**Description of the Change:**
Instead of comparing the operator wires and the control values, we compare the `work_wires` of the two operators, and we compare dictionaries of `{wire1: value1, wire2: value2...}`.

**Benefits:**
The comparison of operators via `qml.equal` is more complete.

**Related GitHub Issues:**
#4842 

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
lillian542: [sc-50266]
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4944?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 [(`a05ae28`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/a05ae286c5925e0c881099cea751193d368779fc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.66% compared to head [(`de7922f`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4944?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.65%.


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


```diff
@@            Coverage Diff             @@
##           master    #4944      +/-   ##
==========================================
- Coverage   99.66%   99.65%   -0.02%     
==========================================
  Files         388      388              
  Lines       35362    35096     -266     
==========================================
- Hits        35245    34976     -269     
- Misses        117      120       +3     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4944?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).


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 41bc8429e..1ad4a167a 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -378,6 +378,10 @@
   done with `qml.ExpvalCost`, but this is the preferred method because `ExpvalCost` is deprecated.
   [(#4896)](https://github.com/PennyLaneAI/pennylane/pull/4896)
 
+* `qml.equal` for `Controlled` operators no longer returns `False` when equivalent but 
+  differently-ordered sets of control wires and control values are compared.
+  [(#4944)](https://github.com/PennyLaneAI/pennylane/pull/4944)
+
 * All PennyLane `Operator` subclasses are automatically tested by `ops.functions.assert_valid` to ensure
   that they follow PennyLane `Operator` standards.
   [(#4922)](https://github.com/PennyLaneAI/pennylane/pull/4922)
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
index d030da4a4..34a1764e0 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -283,11 +283,15 @@ def _equal_prod_and_sum(op1: CompositeOp, op2: CompositeOp, **kwargs):
 @_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,
+    # work wires and control_wire/control_value combinations compared here
+    # op.base.wires compared in return
+    if [
+        dict(zip(op1.control_wires, op1.control_values)),
+        op1.work_wires,
+        op1.arithmetic_depth,
+    ] != [
+        dict(zip(op2.control_wires, op2.control_values)),
+        op2.work_wires,
         op2.arithmetic_depth,
     ]:
         return False


Test patch:
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
index 5c33a54c7..e765f5af7 100644
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -15,7 +15,7 @@
 Unit tests for the equal function.
 Tests are divided by number of parameters and wires different operators take.
 """
-# pylint: disable=too-many-arguments
+# pylint: disable=too-many-arguments, too-many-public-methods
 import itertools
 
 from copy import deepcopy
@@ -1452,6 +1452,36 @@ class TestSymbolicOpComparison:
         op2 = Controlled(base2, control_wires=wire2)
         assert qml.equal(op1, op2) == res
 
+    @pytest.mark.parametrize(("controls1", "controls2"), [([0, 1], [0, 1]), ([1, 1], [1, 0])])
+    def test_control_values_comparison(self, controls1, controls2):
+        """Test that equal compares control values for Controlled operators"""
+        base1 = qml.PauliX(wires=0)
+        base2 = qml.PauliX(wires=0)
+
+        op1 = qml.ops.op_math.Controlled(base1, control_wires=[1, 2], control_values=controls1)
+        op2 = qml.ops.op_math.Controlled(base2, control_wires=[1, 2], control_values=controls2)
+
+        assert qml.equal(op1, op2) == np.allclose(controls1, controls2)
+
+    @pytest.mark.parametrize(
+        ("wires1", "controls1", "wires2", "controls2", "res"),
+        [
+            ([1, 2], [0, 1], [1, 2], [0, 1], True),
+            ([1, 2], [0, 1], [2, 1], [0, 1], False),
+            ([1, 2], [0, 1], [2, 1], [1, 0], True),
+        ],
+    )
+    def test_differing_control_wire_order(self, wires1, controls1, wires2, controls2, res):
+        """Test that equal compares control wires and their respective control values
+        without regard for wire order"""
+        base1 = qml.PauliX(wires=0)
+        base2 = qml.PauliX(wires=0)
+
+        op1 = qml.ops.op_math.Controlled(base1, control_wires=wires1, control_values=controls1)
+        op2 = qml.ops.op_math.Controlled(base2, control_wires=wires2, control_values=controls2)
+
+        assert qml.equal(op1, op2) == res
+
     @pytest.mark.parametrize(("wires1", "wires2", "res"), CONTROL_WIRES_SEQUENCE)
     def test_control_sequence_wires_comparison(self, wires1, wires2, res):
         """Test that equal compares control for ControlledSequence operators"""