Repository: PennyLaneAI/pennylane
Pull request #4902 (2023-11-30 09:48:51)

Compare QuantumScript  classes with qml.equal
**Context:**
Implement comparison between QuantumScript classes with qml.equal

**Description of the Change:**

**Benefits:**
One unified interface to compare things together, now support QuantumScript

**Possible Drawbacks:**

**Related GitHub Issues:** https://github.com/PennyLaneAI/pennylane/issues/4894


minhtriet: `tests/tape/test_qscript.py` are identical and `pennylane/ops/functions/equal.py` are mainly different in the last lines. I think PyCharm changes the spaces inside these files
minhtriet: @albi3ro :)
minhtriet: ready for round 2 @albi3ro :)
minhtriet: The `codecov/project` test seemed to freeze, but https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4902 said CI Passed

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 1c6636e54..442410e91 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -181,6 +181,9 @@
   Additionally, the relevant tests have been organized and placed in a new file, `tests/ops/test_meta.py` .
   [(#4789)](https://github.com/PennyLaneAI/pennylane/pull/4789)
 
+* `qml.equal` now supports comparison of `QuantumScript` objects
+  [#4902](https://github.com/PennyLaneAI/pennylane/pull/4902)
+
 <h4>Better support for batching</h4>
 
 * `default.qubit` now can evolve already batched states with `ParametrizedEvolution`
@@ -432,6 +435,7 @@ Ali Asadi,
 Gabriel Bottrill,
 Thomas Bromley,
 Astral Cai,
+Minh Chau,
 Isaac De Vlugt,
 Amintor Dusko,
 Lillian Frederiksen,
@@ -447,4 +451,4 @@ Mudit Pandey,
 Matthew Silverman,
 Jay Soni,
 David Wierichs,
-Justin Woodring,
+Justin Woodring
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
index df1fde2cc..450c41393 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -29,11 +29,12 @@ 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
+from pennylane.tape import QuantumTape
 
 
 def equal(
-    op1: Union[Operator, MeasurementProcess],
-    op2: Union[Operator, MeasurementProcess],
+    op1: Union[Operator, MeasurementProcess, QuantumTape],
+    op2: Union[Operator, MeasurementProcess, QuantumTape],
     check_interface=True,
     check_trainability=True,
     rtol=1e-5,
@@ -62,8 +63,8 @@ def equal(
         comparison.
 
     Args:
-        op1 (.Operator or .MeasurementProcess): First object to compare
-        op2 (.Operator or .MeasurementProcess): Second object to compare
+        op1 (.Operator or .MeasurementProcess or .QuantumTape): First object to compare
+        op2 (.Operator or .MeasurementProcess or .QuantumTape): Second object to compare
         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.
@@ -104,6 +105,12 @@ def equal(
     False
     >>> qml.equal(qml.classical_shadow(wires=[0,1]), qml.classical_shadow(wires=[0,1]))
     True
+    >>> tape1 = qml.tape.QuantumScript([qml.RX(1.2, wires=0)], [qml.expval(qml.PauliZ(0))])
+    >>> tape2 = qml.tape.QuantumScript([qml.RX(1.2 + 1e-6, wires=0)], [qml.expval(qml.PauliZ(0))])
+    >>> qml.equal(tape1, tape2, tol=0, atol=1e-7)
+    False
+    >>> qml.equal(tape1, tape2, tol=0, atol=1e-5)
+    True
 
     .. details::
         :title: Usage Details
@@ -168,6 +175,49 @@ def _equal(
     raise NotImplementedError(f"Comparison of {type(op1)} and {type(op2)} not implemented")
 
 
+@_equal.register
+def _equal_circuit(
+    op1: qml.tape.QuantumScript,
+    op2: qml.tape.QuantumScript,
+    check_interface=True,
+    check_trainability=True,
+    rtol=1e-5,
+    atol=1e-9,
+):
+    # operations
+    if len(op1.operations) != len(op2.operations):
+        return False
+    for comparands in zip(op1.operations, op2.operations):
+        if not qml.equal(
+            comparands[0],
+            comparands[1],
+            check_interface=check_interface,
+            check_trainability=check_trainability,
+            rtol=rtol,
+            atol=atol,
+        ):
+            return False
+    # measurements
+    if len(op1.measurements) != len(op2.measurements):
+        return False
+    for comparands in zip(op1.measurements, op2.measurements):
+        if not qml.equal(
+            comparands[0],
+            comparands[1],
+            check_interface=check_interface,
+            check_trainability=check_trainability,
+            rtol=rtol,
+            atol=atol,
+        ):
+            return False
+
+    if op1.shots != op2.shots:
+        return False
+    if op1.trainable_params != op2.trainable_params:
+        return False
+    return True
+
+
 @_equal.register
 def _equal_operators(
     op1: Operator,


Test patch:
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
index 06baddccf..61a501056 100644
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -1798,3 +1798,51 @@ class TestParametrizedEvolutionComparisons:
 
         assert qml.equal(ev1(params1, t), ev2(params2, t))
         assert not qml.equal(ev1(params1, t), ev3(params3, t))
+
+
+class TestQuantumScriptComparisons:
+    tape1 = qml.tape.QuantumScript(
+        [qml.PauliX(0), qml.RX(1.2, wires=0)], [qml.expval(qml.PauliZ(0))], shots=10
+    )
+    tape2 = qml.tape.QuantumScript([qml.PauliX(0)], [qml.expval(qml.PauliZ(0))], shots=10)
+    tape3 = qml.tape.QuantumScript([qml.PauliX(0)], [qml.expval(qml.PauliZ(0))], shots=None)
+    tape4 = qml.tape.QuantumScript(
+        [qml.PauliX(0), qml.RX(1.2 + 1e-6, wires=0)], [qml.expval(qml.PauliZ(0))], shots=10
+    )
+    tape5 = qml.tape.QuantumScript(
+        [qml.PauliX(0)],
+        [qml.expval(qml.PauliZ(0))],
+        shots=10,
+        trainable_params=2,
+    )
+    tape6 = qml.tape.QuantumScript([qml.PauliX(0)], [qml.expval(qml.PauliX(0))], shots=10)
+    tape7 = qml.tape.QuantumScript(
+        [qml.PauliX(0)],
+        [qml.expval(qml.PauliZ(0)), qml.expval(qml.PauliX(0))],
+        shots=10,
+    )
+
+    @pytest.mark.parametrize("tape, other_tape", [(tape2, tape7), (tape2, tape6)])
+    def test_non_equal_measurement_comparison(self, tape, other_tape):
+        assert qml.equal(tape, other_tape) is False
+
+    @pytest.mark.parametrize("tape, other_tape", [(tape2, tape3)])
+    def test_non_equal_shot_comparison(self, tape, other_tape):
+        assert qml.equal(tape, other_tape) is False
+
+    @pytest.mark.parametrize("tape, other_tape", [(tape2, tape2)])
+    def test_equal_comparison(self, tape, other_tape):
+        assert qml.equal(tape, other_tape)
+
+    @pytest.mark.parametrize("tape, other_tape", [(tape1, tape2)])
+    def test_non_equal_operators_comparison(self, tape, other_tape):
+        assert qml.equal(tape, other_tape) is False
+
+    @pytest.mark.parametrize("tape, other_tape", [(tape1, tape4)])
+    def test_different_tolerances_comparison(self, tape, other_tape):
+        assert qml.equal(tape, other_tape, atol=1e-5)
+        assert qml.equal(tape, other_tape, rtol=0, atol=1e-7) is False
+
+    @pytest.mark.parametrize("tape, other_tape", [(tape2, tape5)])
+    def test_non_equal_training_params_comparison(self, tape, other_tape):
+        assert qml.equal(tape, other_tape) is False