Repository: PennyLaneAI/pennylane
Pull request #4829 (2023-11-12 21:13:59)

Implement custom comparison for ControlledSequence
**Context:**

Currently, `qml.equal` does not have a registered function that compares two `ControlledSequence` objects correctly. This PR addresses this issue.

**Description of the Change:**

Registers a new overloaded implementation of `_equal` for arguments of type `ControlledSequence`. For two objects of this type, `qml.equal` compares if:

- their control properties are the same
- their base's are the same to the requested tolerances and parameter checks.

For example:

``` python
op1 = qml.ControlledSequence(qml.PauliX(0), control=1)
op2 = qml.ControlledSequence(qml.PauliX(0), control=1)
qml.equal(op1, op2)

>>> True
```

**Benefits:**

Correct comparison of two `ControlledSequence` objects is now possible.

**Possible Drawbacks:**

None.

**Related GitHub Issues:**

Closes: #4797


EmilianoG-byte: Hi!

I have based this PR on an older commit 970d30c252c148f2bcf8ff14793f6ad349b55cc7 (from 26.10) since the newest commit was throwing some import errors when I pulled from master, and this was one of the last ones were the CI checks were passing. I see the one only conflict is with the changelog, so hopefully doing this way was ok :)

Let me know what you think about my changes!

rmoyard: Hello @EmilianoG-byte , thanks for your contribution! I will give an initial review of your PR this afternoon.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4829?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 [(`1cea8c6`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/1cea8c6bc6463a28c1a5ce1361d9050cc7202d48?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.65% compared to head [(`5148e27`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4829?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.64%.
> Report is 1 commits behind head on master.


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


```diff
@@            Coverage Diff             @@
##           master    #4829      +/-   ##
==========================================
- Coverage   99.65%   99.64%   -0.01%     
==========================================
  Files         381      381              
  Lines       34329    34072     -257     
==========================================
- Hits        34209    33951     -258     
- Misses        120      121       +1     
```



</details>

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

EmilianoG-byte: > This looks good @EmilianoG-byte, thanks for the thorough tests! 🚀

Thank you 😃 @lillian542 !!
EmilianoG-byte: Hi @rmoyard, I made the changes we discussed 😊
Let me know if some of them were still up for discussion or if they are okay!
rmoyard: Hi @EmilianoG-byte, I am glad you solved the issue! For the second part, in general you are welcome to use it but I also like when it is tested separated. It makes it easier to refactor if needed.
Alex-Preciado: Awesome! Thank you so much @EmilianoG-byte. Merging this now 😎

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 0d832209c..5d4b56cfb 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -24,6 +24,9 @@
 * Updates to some relevant Pytests to enable its use as a suite of benchmarks.
   [(#4703)](https://github.com/PennyLaneAI/pennylane/pull/4703)
 
+* The function ``qml.equal`` now supports ``ControlledSequence`` operators.
+  [(#4829)](https://github.com/PennyLaneAI/pennylane/pull/4829)
+
 * Added `__iadd__` method to PauliSentence, which enables inplace-addition using `+=`, we no longer need to perform a copy, leading to performance improvements.
   [(#4662)](https://github.com/PennyLaneAI/pennylane/pull/4662) 
 
@@ -147,6 +150,7 @@ This release contains contributions from (in alphabetical order):
 
 Amintor Dusko,
 Lillian Frederiksen,
+Emiliano Godinez Ramirez,
 Ankit Khandelwal,
 Christina Lee,
 Anurav Modak,
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
index f6a513ac2..ed1a8fc62 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -28,6 +28,7 @@ from pennylane.measurements.counts import CountsMP
 from pennylane.pulse.parametrized_evolution import ParametrizedEvolution
 from pennylane.operation import Observable, Operator, Tensor
 from pennylane.ops import Hamiltonian, Controlled, Pow, Adjoint, Exp, SProd, CompositeOp
+from pennylane.templates.subroutines import ControlledSequence
 
 
 def equal(
@@ -243,6 +244,18 @@ def _equal_controlled(op1: Controlled, op2: Controlled, **kwargs):
     return qml.equal(op1.base, op2.base, **kwargs)
 
 
+@_equal.register
+def _equal_controlled_sequence(op1: ControlledSequence, op2: ControlledSequence, **kwargs):
+    """Determine whether two ControlledSequences are equal"""
+    if [op1.wires, op1.arithmetic_depth] != [
+        op2.wires,
+        op2.arithmetic_depth,
+    ]:
+        return False
+
+    return qml.equal(op1.base, op2.base, **kwargs)
+
+
 @_equal.register
 # pylint: disable=unused-argument
 def _equal_pow(op1: Pow, op2: Pow, **kwargs):


Test patch:
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
index 679bddfa9..06baddccf 100644
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -18,14 +18,17 @@ Tests are divided by number of parameters and wires different operators take.
 # pylint: disable=too-many-arguments
 import itertools
 
+from copy import deepcopy
 import numpy as np
 import pytest
 
+
 import pennylane as qml
 from pennylane import numpy as npp
 from pennylane.measurements import ExpectationMP
 from pennylane.measurements.probs import ProbabilityMP
 from pennylane.ops.op_math import SymbolicOp, Controlled
+from pennylane.templates.subroutines import ControlledSequence
 
 PARAMETRIZED_OPERATIONS_1P_1W = [
     qml.RX,
@@ -1316,6 +1319,11 @@ class TestSymbolicOpComparison:
     """Test comparison for subclasses of SymbolicOp"""
 
     WIRES = [(5, 5, True), (6, 7, False)]
+    CONTROL_WIRES_SEQUENCE = [
+        ([1, 2], [1, 2], True),
+        ([1, 2], [1, 3], False),
+        ([1, 2, 3], [3, 2, 1], False),
+    ]
 
     BASES = [
         (qml.PauliX(0), qml.PauliX(0), True),
@@ -1333,6 +1341,7 @@ class TestSymbolicOpComparison:
         """Test that comparing SymoblicOp 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)
 
@@ -1340,6 +1349,13 @@ class TestSymbolicOpComparison:
         assert op2.arithmetic_depth == 2
         assert qml.equal(op1, op2) is False
 
+        op1 = ControlledSequence(base1, control=2)
+        op2 = ControlledSequence(base2, control=2)
+
+        assert op1.arithmetic_depth == 1
+        assert op2.arithmetic_depth == 2
+        assert qml.equal(op1, op2) is False
+
     def test_comparison_of_base_not_implemented_returns_false(self):
         """Test that comparing SymbolicOps of base operators whose comparison is not yet implemented returns False"""
         base = SymbolicOp(qml.RX(1.2, 0))
@@ -1348,6 +1364,11 @@ class TestSymbolicOpComparison:
 
         assert not qml.equal(op1, op2)
 
+        op1 = ControlledSequence(base, control=2)
+        op2 = ControlledSequence(base, control=2)
+
+        assert not qml.equal(op1, op2)
+
     @pytest.mark.torch
     @pytest.mark.jax
     def test_kwargs_for_base_operator_comparison(self):
@@ -1364,6 +1385,12 @@ class TestSymbolicOpComparison:
         assert not qml.equal(op1, op2)
         assert qml.equal(op1, op2, check_interface=False, check_trainability=False)
 
+        op1 = ControlledSequence(base1, control=1)
+        op2 = ControlledSequence(base2, control=1)
+
+        assert not qml.equal(op1, op2)
+        assert qml.equal(op1, op2, check_interface=False, check_trainability=False)
+
     @pytest.mark.parametrize("base", PARAMETRIZED_OPERATIONS)
     def test_controlled_comparison(self, base):
         """Test that Controlled operators can be compared"""
@@ -1371,6 +1398,19 @@ class TestSymbolicOpComparison:
         op2 = Controlled(base, control_wires=7, control_values=0)
         assert qml.equal(op1, op2)
 
+    @pytest.mark.parametrize("base", PARAMETRIZED_OPERATIONS)
+    def test_controlled_sequence_comparison(self, base):
+        """Test that ControlledSequence operators can be compared"""
+        op1 = ControlledSequence(base, control=7)
+        op2 = ControlledSequence(base, control=7)
+        assert qml.equal(op1, op2)
+
+    @pytest.mark.parametrize("base", PARAMETRIZED_OPERATIONS)
+    def test_controlled_sequence_deepcopy_comparison(self, base):
+        """Test that equal is compatible with deepcopy"""
+        op1 = ControlledSequence(base, control=7)
+        assert qml.equal(op1, deepcopy(op1))
+
     @pytest.mark.parametrize(("wire1", "wire2", "res"), WIRES)
     def test_controlled_base_operator_wire_comparison(self, wire1, wire2, res):
         """Test that equal compares operator wires for Controlled operators"""
@@ -1380,14 +1420,27 @@ class TestSymbolicOpComparison:
         op2 = Controlled(base2, control_wires=1)
         assert qml.equal(op1, op2) == res
 
-    @pytest.mark.parametrize(
-        ("base1", "base2", "res"),
-        [(qml.PauliX(0), qml.PauliX(0), True), (qml.PauliX(0), qml.PauliY(0), False)],
-    )
+    @pytest.mark.parametrize(("wire1", "wire2", "res"), WIRES)
+    def test_controlled_sequence_base_operator_wire_comparison(self, wire1, wire2, res):
+        """Test that equal compares operator wires for ControlledSequence operators"""
+        base1 = qml.PauliX(wire1)
+        base2 = qml.PauliX(wire2)
+        op1 = ControlledSequence(base1, control=1)
+        op2 = ControlledSequence(base2, control=1)
+        assert qml.equal(op1, op2) == res
+
+    @pytest.mark.parametrize(("base1", "base2", "res"), BASES)
     def test_controlled_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)
+        op1 = Controlled(base1, control_wires=2)
+        op2 = Controlled(base2, control_wires=2)
+        assert qml.equal(op1, op2) == res
+
+    @pytest.mark.parametrize(("base1", "base2", "res"), BASES)
+    def test_controlled_sequence_base_operator_comparison(self, base1, base2, res):
+        """Test that equal compares base operators for ControlledSequence operators"""
+        op1 = ControlledSequence(base1, control=2)
+        op2 = ControlledSequence(base2, control=2)
         assert qml.equal(op1, op2) == res
 
     @pytest.mark.parametrize(("wire1", "wire2", "res"), WIRES)
@@ -1399,6 +1452,15 @@ class TestSymbolicOpComparison:
         op2 = Controlled(base2, control_wires=wire2)
         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"""
+        base1 = qml.Hadamard(0)
+        base2 = qml.Hadamard(0)
+        op1 = ControlledSequence(base1, control=wires1)
+        op2 = ControlledSequence(base2, control=wires2)
+        assert qml.equal(op1, op2) == res
+
     @pytest.mark.parametrize(("wire1", "wire2", "res"), WIRES)
     def test_controlled_work_wires_comparison(self, wire1, wire2, res):
         """Test that equal compares work_wires for Controlled operators"""