Repository: PennyLaneAI/pennylane
Pull request #3471 (2022-12-05 23:01:24)

Equal for SymbolicOp subclasses
**Context:**
The `qml.equal` function cannot compare subclasses of `SymbolicOp` (`Adjoint`, `SProd`, `Exp`, `Pow`).

**Description of the Change:**
Now it can.

**Possible Drawbacks:**
If `arithmetic_depth` is greater than 1, and operators are nested the same way, it can compares them correctly. If they are nested in a different order, it returns False, even if the order of the nesting is not significant. For example:

```
nested_op = qml.pow(qml.pow(base, 2), 3)
nested_op2 = qml.pow(qml.pow(base, 3), 2)

qml.equal(nested_op, nested_op) 
>>> True

qml.equal(nested_op, nested_op2)
>>> False
```
Not sure if it would be better to raise a `NotImplementedError` for `arithmetic_depth` greater than 1, or to leave this (since it can correctly identify that identical operators are equal) and note in the docstring that the function assumes a mismatch in the nesting order means the operators aren't equivalent.

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.
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3471?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3471](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3471?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (c4ab575) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/168392b8ff5a07ead7b865a9bb669e5c59bf53f9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (168392b) will **decrease** coverage by `0.03%`.
> The diff coverage is `50.00%`.

```diff
@@            Coverage Diff             @@
##           master    #3471      +/-   ##
==========================================
- Coverage   99.69%   99.66%   -0.04%     
==========================================
  Files         307      308       +1     
  Lines       27023    27182     +159     
==========================================
+ Hits        26941    27090     +149     
- Misses         82       92      +10     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3471?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/3471/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9mdW5jdGlvbnMvZXF1YWwucHk=) | `89.81% <50.00%> (-9.08%)` | :arrow_down: |
| [pennylane/\_qubit\_device.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3471/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19xdWJpdF9kZXZpY2UucHk=) | `99.68% <0.00%> (-0.01%)` | :arrow_down: |
| [pennylane/gradients/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3471/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2dyYWRpZW50cy9fX2luaXRfXy5weQ==) | `100.00% <0.00%> (ø)` | |
| [pennylane/measurements/measurements.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3471/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmVtZW50cy9tZWFzdXJlbWVudHMucHk=) | `100.00% <0.00%> (ø)` | |
| [pennylane/gradients/gradient\_transform.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3471/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2dyYWRpZW50cy9ncmFkaWVudF90cmFuc2Zvcm0ucHk=) | `100.00% <0.00%> (ø)` | |
| [pennylane/gradients/spsa\_gradient.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3471/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2dyYWRpZW50cy9zcHNhX2dyYWRpZW50LnB5) | `100.00% <0.00%> (ø)` | |

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 9afc67d8a..444821ff7 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -286,6 +286,9 @@
 * Replace (almost) all instances of `with QuantumTape()` with `QuantumScript` construction.
   [(#3454)](https://github.com/PennyLaneAI/pennylane/pull/3454)
 
+* Extended the `qml.equal` function to `Pow`, `SProd`, `Exp` and `Adjoint` objects.
+  [(#3471)](https://github.com/PennyLaneAI/pennylane/pull/3471)
+
 * Adds support for devices disregarding observable grouping indices in Hamiltonians through
   the optional `use_grouping` attribute.
   [(#3456)](https://github.com/PennyLaneAI/pennylane/pull/3456)
@@ -293,6 +296,7 @@
 * Reduce usage of `MeasurementProcess.return_type`. Use `isinstance` checks instead.
   [(#3399)](https://github.com/PennyLaneAI/pennylane/pull/3399)
   
+
 <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 a926b9b62..d0714b85f 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -24,7 +24,7 @@ from pennylane.measurements.classical_shadow import ShadowExpvalMP
 from pennylane.measurements.mutual_info import MutualInfoMP
 from pennylane.measurements.vn_entropy import VnEntropyMP
 from pennylane.operation import Observable, Operator, Tensor
-from pennylane.ops import Controlled, Hamiltonian
+from pennylane.ops import Hamiltonian, Controlled, Pow, Adjoint, Exp, SProd
 
 
 def equal(
@@ -39,17 +39,22 @@ def equal(
 
     .. Warning::
 
-        The equal function does **not** check if the matrix representation
-        of a :class:`~.Hermitian` observable is equal to an equivalent
-        observable expressed in terms of Pauli matrices, or as a
-        linear combination of Hermitians.
-        To do so would require the matrix form of Hamiltonians and Tensors
-        be calculated, which may drastically increase runtime.
+        The ``qml.equal`` function is based on a comparison of the type and attributes
+        of the measurement or operator, not a mathematical representation. While
+        comparisons between some classes, such as ``Tensor`` and ``Hamiltonian``, are
+        supported, mathematically equivalent operators defined via different classes
+        may return False when compared via ``qml.equal``.
+
+        To be more thorough would require the matrix forms to be calculated, which may
+        drastically increase runtime.
+
+    .. Warning::
 
         The kwargs ``check_interface`` and ``check_trainability`` can only be set when
-        comparing ``Operation`` objects. Comparisons of ``MeasurementProcess`` or ``Observable``
-        objects will use the defualt value of ``True`` for both, regardless of what the user
-        specifies when calling the function.
+        comparing ``Operation`` objects. Comparisons of ``MeasurementProcess``
+        or ``Observable`` objects will use the defualt value of ``True`` for both, regardless
+        of what the user specifies when calling the function. For subclasses of ``SymbolicOp``
+        with an ``Operation`` as a base, the kwargs will be applied to the base comparison.
 
     Args:
         op1 (.Operator or .MeasurementProcess): First object to compare
@@ -95,14 +100,13 @@ def equal(
     >>> 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. These arguments are, however, not used
-        for comparing ``MeasurementProcess``, ``Hamiltonian`` or ``Tensor`` objects. Additionally, they are
-        applied only to the base operator of `Controlled` operators.
+        You can use the optional arguments to get more specific results. Additionally, they are
+        applied when comparing the base of ``SymbolicOp`` operators such as ``Controlled``, ``Pow``,
+        ``SProd``, etc., if the base is an ``Operation``. These arguments are, however, not used
+        for comparing ``MeasurementProcess``, ``Hamiltonian`` or ``Tensor`` objects.
 
         Consider the following comparisons:
 
@@ -223,6 +227,41 @@ def _equal_controlled(op1: Controlled, op2: Controlled, **kwargs):
         ) from e
 
 
+@_equal.register
+# pylint: disable=unused-argument
+def _equal_pow(op1: Pow, op2: Pow, **kwargs):
+    """Determine whether two Pow objects are equal"""
+    if op1.z != op2.z:
+        return False
+    return qml.equal(op1.base, op2.base)
+
+
+@_equal.register
+# pylint: disable=unused-argument
+def _equal_adjoint(op1: Adjoint, op2: Adjoint, **kwargs):
+    """Determine whether two Adjoint objects are equal"""
+    # first line of top-level equal function already confirms both are Adjoint - only need to compare bases
+    return qml.equal(op1.base, op2.base)
+
+
+@_equal.register
+# pylint: disable=unused-argument
+def _equal_exp(op1: Exp, op2: Exp, **kwargs):
+    """Determine whether two Exp objects are equal"""
+    if op1.coeff != op2.coeff:
+        return False
+    return qml.equal(op1.base, op2.base)
+
+
+@_equal.register
+# pylint: disable=unused-argument
+def _equal_sprod(op1: SProd, op2: SProd, **kwargs):
+    """Determine whether two SProd objects are equal"""
+    if op1.scalar != op2.scalar:
+        return False
+    return qml.equal(op1.base, op2.base)
+
+
 @_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 75c54f23e..8509d9dc0 100644
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -1106,12 +1106,16 @@ class TestEqual:
     def test_equal_with_nested_operators_raises_error(self):
         """Test that the equal method with two operators with the same arithmetic depth (>0) raises
         an error."""
+        op1 = qml.PauliY(0)
+        op2 = qml.PauliZ(1)
+        prod = qml.prod(op1, op2)
+
         with pytest.raises(
             NotImplementedError,
             match="Comparison of operators with an arithmetic"
             + " depth larger than 0 is not yet implemented.",
         ):
-            qml.equal(qml.adjoint(qml.RX(1.2, 0)), qml.adjoint(qml.RX(1.2, 0)))
+            qml.equal(prod, prod)
 
     def test_equal_same_inversion(self):
         """Test operations are equal if they are both inverted."""
@@ -1199,35 +1203,85 @@ class TestObservablesComparisons:
             qml.equal(dev, dev)
 
 
-class TestControlledComparisons:
+class TestSymbolicOpComparison:
+    """Test comparison for subclasses of SymbolicOp"""
+
     WIRES = [(5, 5, True), (6, 7, False)]
 
+    BASES = [
+        (qml.PauliX(0), qml.PauliX(0), True),
+        (qml.PauliX(0) @ qml.PauliY(1), qml.PauliX(0) @ qml.PauliY(1), True),
+        (qml.CRX(1.23, [0, 1]), qml.CRX(1.23, [0, 1]), True),
+        (qml.CRX(1.23, [1, 0]), qml.CRX(1.23, [0, 1]), False),
+        (qml.PauliY(1), qml.PauliY(0), False),
+        (qml.PauliX(1), qml.PauliY(1), False),
+        (qml.PauliX(0) @ qml.PauliY(1), qml.PauliZ(1) @ qml.PauliY(0), False),
+    ]
+
+    PARAMS = [(1.23, 1.23, True), (5, 5, True), (2, -2, False), (1.2, 1, False)]
+
+    def test_mismatched_arithmetic_depth(self):
+        """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)
+
+        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 SymbolicOps 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 bases"""
+        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 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"""
         op1 = Controlled(base, control_wires=7, control_values=0)
         op2 = Controlled(base, control_wires=7, control_values=0)
-        assert qml.equal(op1, op2) == True
+        assert qml.equal(op1, op2)
+
+    @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"""
+        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(
         ("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):
+    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)
         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"""
@@ -1238,7 +1292,7 @@ class TestControlledComparisons:
         assert qml.equal(op1, op2) == res
 
     @pytest.mark.parametrize(("wire1", "wire2", "res"), WIRES)
-    def test_work_wires_comparison(self, wire1, wire2, res):
+    def test_controlled_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])
@@ -1246,38 +1300,36 @@ class TestControlledComparisons:
         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 jax
-        import torch
-
-        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)
+    @pytest.mark.parametrize("base", PARAMETRIZED_OPERATIONS)
+    def test_adjoint_comparison(self, base):
+        """Test that equal compares two objects of the Adjoint class"""
+        op1 = qml.adjoint(base)
+        op2 = qml.adjoint(base)
+        op3 = qml.adjoint(qml.PauliX(15))
 
-        assert qml.equal(op1, op2) == False
-        assert qml.equal(op1, op2, check_interface=False, check_trainability=False) == True
+        assert qml.equal(op1, op2)
+        assert not qml.equal(op1, op3)
+
+    @pytest.mark.parametrize(("base1", "base2", "bases_match"), BASES)
+    @pytest.mark.parametrize(("param1", "param2", "params_match"), PARAMS)
+    def test_pow_comparison(self, base1, base2, bases_match, param1, param2, params_match):
+        """Test that equal compares two objects of the Pow class"""
+        op1 = qml.pow(base1, param1)
+        op2 = qml.pow(base2, param2)
+        assert qml.equal(op1, op2) == (bases_match and params_match)
+
+    @pytest.mark.parametrize(("base1", "base2", "bases_match"), BASES)
+    @pytest.mark.parametrize(("param1", "param2", "params_match"), PARAMS)
+    def test_exp_comparison(self, base1, base2, bases_match, param1, param2, params_match):
+        """Test that equal compares two objects of the Exp class"""
+        op1 = qml.exp(base1, param1)
+        op2 = qml.exp(base2, param2)
+        assert qml.equal(op1, op2) == (bases_match and params_match)
+
+    @pytest.mark.parametrize(("base1", "base2", "bases_match"), BASES)
+    @pytest.mark.parametrize(("param1", "param2", "params_match"), PARAMS)
+    def test_s_prod_comparison(self, base1, base2, bases_match, param1, param2, params_match):
+        """Test that equal compares two objects of the SProd class"""
+        op1 = qml.s_prod(param1, base1)
+        op2 = qml.s_prod(param2, base2)
+        assert qml.equal(op1, op2) == (bases_match and params_match)