Repository: PennyLaneAI/pennylane

Issue:
Function for determining Operator equality
In order to determine if two `Operator`'s are equal, we currently have to manually compare different properties like:

```python
assert op1.__class__ is op2.__class__
assert all( qml.math.allclose(d1, d2) for d1, d2 in zip(op1.data, op2.data))
assert op1.wires == op2.wires
for kwarg in op1.hyperparameters:
    assert op1.hyperparameters[kwarg] == op2.hyperparameters[kwarg]
```

We currently don't want to override the dunder magic method `__eq__`, so we want a new function `qml.equal` that determines the equality of two operations. By using a function, we can also have control over what "equal" exactly means.

This function should be located in a new file`pennylane/ops/functions/equal.py`.

The function should take two arguments `op1` and `op2`.

The function can also accept a number of keyword arguments:
* `rtol=1e-5`: relative tolerance for equality for numerical data;
* `atol=1e-9`: absolute tolerance for equality for numerical data;
* `equal_interface=True`: whether or not parameters of different interfaces are considered equal. The keyword name is not fixed;
* `equal_trainability=True`: whether parameters marked as trainable should be equal to parameters marked as non-trainable. The keyword name is not fixed.

```pycon
>>> op1 = qml.RX(torch.tensor(1.2), wires=0)
>>> op2 = qml.RX(jax.numpy.array(1.2), wires=0)
>>> qml.equal(op1, op2)
False
>>> qml.equal(op1, op2, equal_interface=False)
True

>>> op3 = qml.RX(np.array(1.2, requires_grad=True), wires=0)
>>> op4 = qml.RX(np.array(1.2, requires_grad=False), wires=0)
>>> qml.equal(op3, op4)
False
>>> qml.equal(op3, op4, equal_trainability=False)
True
```

This new function should also be tested in a new file `./tests/ops/functions/test_equal.py`. There should be a test case for all parametrized operations in PennyLane making sure that `qml.equal` works as expected (`pytest`'s `parametrize` can help here with brevity).

For objects that inherit from `Observable` like Pauli Operators, `Tensor`, `Hermitian`, and `Hamiltonian`, we can defer to `op1.compare(op2)`.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#2651](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (05ccc37) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/3344c77e39354df70b7ca989a65f5fe982b63479?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (3344c77) will **decrease** coverage by `40.78%`.
> The diff coverage is `18.18%`.

```diff
@@             Coverage Diff             @@
##           master    #2651       +/-   ##
===========================================
- Coverage   99.58%   58.79%   -40.79%     
===========================================
  Files         245      246        +1     
  Lines       19724    19746       +22     
===========================================
- Hits        19642    11610     -8032     
- Misses         82     8136     +8054     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651?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/2651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9mdW5jdGlvbnMvZXF1YWwucHk=) | `14.28% <14.28%> (ø)` | |
| [pennylane/ops/functions/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9mdW5jdGlvbnMvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/interfaces/jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvamF4LnB5) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/jax\_jit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvamF4X2ppdC5weQ==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/tensorflow.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvdGVuc29yZmxvdy5weQ==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/interfaces/tensorflow\_autograph.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2ludGVyZmFjZXMvdGVuc29yZmxvd19hdXRvZ3JhcGgucHk=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_jax.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF9qYXgucHk=) | `0.00% <0.00%> (-96.67%)` | :arrow_down: |
| [pennylane/devices/default\_qubit\_tf.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdF90Zi5weQ==) | `0.00% <0.00%> (-92.76%)` | :arrow_down: |
| [pennylane/qchem/openfermion\_obs.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3FjaGVtL29wZW5mZXJtaW9uX29icy5weQ==) | `7.26% <0.00%> (-92.74%)` | :arrow_down: |
| [pennylane/hf/hamiltonian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2hmL2hhbWlsdG9uaWFuLnB5) | `8.13% <0.00%> (-91.87%)` | :arrow_down: |
| ... and [189 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [3344c77...05ccc37](https://codecov.io/gh/PennyLaneAI/pennylane/pull/2651?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

ankit27kh: Hey @rmoyard, can you take a look?
There are tests for the missing lines but they are not running as they require multiple interfaces at once to properly test the function.
I am not sure how to use multiple interfaces at once where it'll not just skip the test because the it only has one interface at a time when running.
rmoyard: Hey @ankit27kh, I am not responsible for this one you should ask @puzzleshark for a review.

To answer your questions you have to use the pytest marker `@pytest.mark.all_interfaces` for having an environment that supports all interfaces. You can find more details in the [testing doc](https://pennylane.readthedocs.io/en/latest/development/guide/tests.html)
ankit27kh: @puzzleshark can you take a look?

puzzleshark: Nice job! @ankit27kh. Thanks for the unitary hack contribution!

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 273b653b9..ab05ff0e7 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -42,6 +42,9 @@
 
 <h3>Improvements</h3>
 
+* Adds a new function to compare operators. `qml.equal` can be used to compare equality of parametric operators taking into account their interfaces and trainability.
+  [(#2651)](https://github.com/PennyLaneAI/pennylane/pull/2651)
+
 <h3>Breaking changes</h3>
 
 <h3>Deprecations</h3>
@@ -52,4 +55,4 @@
 
 This release contains contributions from (in alphabetical order):
 
-Ixchel Meza Chavez, Moritz Willmann
+Ankit Khandelwal, Ixchel Meza Chavez, Moritz Willmann
diff --git a/pennylane/ops/functions/__init__.py b/pennylane/ops/functions/__init__.py
index e934ec8c4..16e58de93 100644
--- a/pennylane/ops/functions/__init__.py
+++ b/pennylane/ops/functions/__init__.py
@@ -17,3 +17,4 @@ This module contains functions that act on operators and tapes.
 from .eigvals import eigvals
 from .generator import generator
 from .matrix import matrix
+from .equal import equal
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
new file mode 100644
index 000000000..8241a516d
--- /dev/null
+++ b/pennylane/ops/functions/equal.py
@@ -0,0 +1,89 @@
+# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+This module contains the qml.equal function.
+"""
+# pylint: disable=too-many-arguments,too-many-return-statements
+import pennylane as qml
+
+
+def equal(op1, op2, check_interface=True, check_trainability=True, rtol=1e-5, atol=1e-9):
+    r"""Function for determining operator equality.
+
+    Args:
+        op1 (.Operator): First operator to compare
+        op2 (.Operator): Second operator to compare
+        check_interface (bool, optional): Whether to compare interfaces. Default: `True`
+        check_trainability (bool, optional): Whether to compare trainability status. Default: `True`
+        rtol (float, optional): Relative tolerance for parameters
+        atol (float, optional): Absolute tolerance for parameters
+
+    Returns:
+        bool: `True` if the operators are equal, else `False`
+
+    **Example**
+
+    Given two operators, ``qml.equal`` determines their equality:
+
+    >>> op1 = qml.RX(np.array(.12), wires=0)
+    >>> op2 = qml.RY(np.array(1.23), wires=0)
+    >>> qml.equal(op1, op1), qml.equal(op1, op2)
+    True False
+
+    .. details::
+        :title: Usage Details
+
+        You can use the optional arguments to get more specific results.
+
+        Consider the following comparisons:
+
+        >>> op1 = qml.RX(torch.tensor(1.2), wires=0)
+        >>> op2 = qml.RX(jax.numpy.array(1.2), wires=0)
+        >>> qml.equal(op1, op2)
+        False
+
+        >>> qml.equal(op1, op2, check_interface=False, check_trainability=False)
+        True
+
+        >>> op3 = qml.RX(np.array(1.2, requires_grad=True), wires=0)
+        >>> op4 = qml.RX(np.array(1.2, requires_grad=False), wires=0)
+        >>> qml.equal(op3, op4)
+        False
+
+        >>> qml.equal(op3, op4, check_trainability=False)
+        True
+    """
+    if op1.__class__ is not op2.__class__:
+        return False
+    if not all(
+        qml.math.allclose(d1, d2, rtol=rtol, atol=atol) for d1, d2 in zip(op1.data, op2.data)
+    ):
+        return False
+    if not op1.wires == op2.wires:
+        return False
+    for kwarg in op1.hyperparameters:
+        if not op1.hyperparameters[kwarg] == op2.hyperparameters[kwarg]:
+            return False
+
+    if check_trainability:
+        for params_1, params_2 in zip(op1.data, op2.data):
+            if qml.math.requires_grad(params_1) != qml.math.requires_grad(params_2):
+                return False
+
+    if check_interface:
+        for params_1, params_2 in zip(op1.data, op2.data):
+            if qml.math.get_interface(params_1) != qml.math.get_interface(params_2):
+                return False
+
+    return True


Test patch:
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
new file mode 100644
index 000000000..f2ec63ade
--- /dev/null
+++ b/tests/ops/functions/test_equal.py
@@ -0,0 +1,834 @@
+# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Unit tests for the equal function.
+Tests are divided by number of parameters and wires different operators take.
+"""
+import pytest
+import numpy as np
+from pennylane import numpy as npp
+import pennylane as qml
+import itertools
+
+
+PARAMETRIZED_OPERATIONS_1P_1W = [
+    qml.RX,
+    qml.RY,
+    qml.RZ,
+    qml.PhaseShift,
+    qml.U1,
+]
+
+PARAMETRIZED_OPERATIONS_1P_2W = [
+    qml.IsingXX,
+    qml.IsingYY,
+    qml.IsingZZ,
+    qml.IsingXY,
+    qml.ControlledPhaseShift,
+    qml.CRX,
+    qml.CRY,
+    qml.CRZ,
+    qml.SingleExcitation,
+    qml.SingleExcitationPlus,
+    qml.SingleExcitationMinus,
+]
+
+
+PARAMETRIZED_OPERATIONS_3P_1W = [
+    qml.Rot,
+    qml.U3,
+]
+
+
+PARAMETRIZED_OPERATIONS_1P_4W = [
+    qml.DoubleExcitation,
+    qml.DoubleExcitationPlus,
+    qml.DoubleExcitationMinus,
+]
+
+PARAMETRIZED_OPERATIONS_Remaining = [
+    qml.PauliRot,
+    qml.QubitUnitary,
+    qml.DiagonalQubitUnitary,
+    qml.ControlledQubitUnitary,
+]
+PARAMETRIZED_OPERATIONS_2P_1W = [qml.U2]
+PARAMETRIZED_OPERATIONS_1P_3W = [qml.MultiRZ]
+PARAMETRIZED_OPERATIONS_3P_2W = [qml.CRot]
+
+
+PARAMETRIZED_OPERATIONS = [
+    qml.RX(0.123, wires=0),
+    qml.RY(1.434, wires=0),
+    qml.RZ(2.774, wires=0),
+    qml.PauliRot(0.123, "Y", wires=0),
+    qml.IsingXX(0.123, wires=[0, 1]),
+    qml.IsingYY(0.123, wires=[0, 1]),
+    qml.IsingZZ(0.123, wires=[0, 1]),
+    qml.IsingXY(0.123, wires=[0, 1]),
+    qml.Rot(0.123, 0.456, 0.789, wires=0),
+    qml.PhaseShift(2.133, wires=0),
+    qml.ControlledPhaseShift(1.777, wires=[0, 2]),
+    qml.MultiRZ(0.112, wires=[1, 2, 3]),
+    qml.CRX(0.836, wires=[2, 3]),
+    qml.CRY(0.721, wires=[2, 3]),
+    qml.CRZ(0.554, wires=[2, 3]),
+    qml.U1(0.123, wires=0),
+    qml.U2(3.556, 2.134, wires=0),
+    qml.U3(2.009, 1.894, 0.7789, wires=0),
+    qml.CRot(0.123, 0.456, 0.789, wires=[0, 1]),
+    qml.QubitUnitary(np.eye(2) * 1j, wires=0),
+    qml.DiagonalQubitUnitary(np.array([1.0, 1.0j]), wires=1),
+    qml.ControlledQubitUnitary(np.eye(2) * 1j, wires=[0], control_wires=[2]),
+    qml.SingleExcitation(0.123, wires=[0, 3]),
+    qml.SingleExcitationPlus(0.123, wires=[0, 3]),
+    qml.SingleExcitationMinus(0.123, wires=[0, 3]),
+    qml.DoubleExcitation(0.123, wires=[0, 1, 2, 3]),
+    qml.DoubleExcitationPlus(0.123, wires=[0, 1, 2, 3]),
+    qml.DoubleExcitationMinus(0.123, wires=[0, 1, 2, 3]),
+]
+PARAMETRIZED_OPERATIONS_COMBINATIONS = list(
+    itertools.combinations(
+        PARAMETRIZED_OPERATIONS,
+        2,
+    )
+)
+
+
+class TestEqual:
+    @pytest.mark.parametrize("ops", PARAMETRIZED_OPERATIONS_COMBINATIONS)
+    def test_equal_simple_diff_op(self, ops):
+        """Test different operators return False"""
+        assert not qml.equal(ops[0], ops[1], check_trainability=False, check_interface=False)
+
+    @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS)
+    def test_equal_simple_same_op(self, op1):
+        """Test same operators return True"""
+        assert qml.equal(op1, op1, check_trainability=False, check_interface=False)
+
+    @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS_1P_1W)
+    def test_equal_simple_op_1p1w(self, op1):
+        """Test changing parameter or wire returns False"""
+        wire = 0
+        param = 0.123
+        assert qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param * 2, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=wire + 1),
+            check_trainability=False,
+            check_interface=False,
+        )
+
+    @pytest.mark.all_interfaces
+    @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS_1P_1W)
+    def test_equal_op_1p1w(self, op1):
+        """Test optional arguments are working"""
+        wire = 0
+
+        import torch
+        import tensorflow as tf
+        import jax
+
+        param_torch = torch.tensor(0.123)
+        param_tf = tf.Variable(0.123)
+        param_jax = jax.numpy.array(0.123)
+        param_qml = npp.array(0.123)
+        param_np = np.array(0.123)
+
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+    @pytest.mark.all_interfaces
+    @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS_1P_2W)
+    def test_equal_op_1p2w(self, op1):
+        """Test optional arguments are working"""
+        wire = [0, 1]
+
+        import torch
+        import tensorflow as tf
+        import jax
+
+        param_torch = torch.tensor(0.123)
+        param_tf = tf.Variable(0.123)
+        param_jax = jax.numpy.array(0.123)
+        param_qml = npp.array(0.123)
+        param_np = np.array(0.123)
+
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+    @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS_1P_2W)
+    def test_equal_simple_op_1p2w(self, op1):
+        """Test changing parameter or wire returns False"""
+        wire = [0, 1]
+        param = 0.123
+        assert qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param * 2, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=[w + 1 for w in wire]),
+            check_trainability=False,
+            check_interface=False,
+        )
+
+    @pytest.mark.all_interfaces
+    @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS_1P_4W)
+    def test_equal_op_1p4w(self, op1):
+        """Test optional arguments are working"""
+        wire = [0, 1, 2, 3]
+
+        import torch
+        import tensorflow as tf
+        import jax
+
+        param_torch = torch.tensor(0.123)
+        param_tf = tf.Variable(0.123)
+        param_jax = jax.numpy.array(0.123)
+        param_qml = npp.array(0.123)
+        param_np = np.array(0.123)
+
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+    @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS_1P_4W)
+    def test_equal_simple_op_1p4w(self, op1):
+        """Test changing parameter or wire returns False"""
+        wire = [0, 1, 2, 3]
+        param = 0.123
+        assert qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param * 2, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=[w + 1 for w in wire]),
+            check_trainability=False,
+            check_interface=False,
+        )
+
+    @pytest.mark.all_interfaces
+    @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS_3P_1W)
+    def test_equal_op_3p1w(self, op1):
+        """Test optional arguments are working"""
+        wire = 0
+
+        import torch
+        import tensorflow as tf
+        import jax
+
+        param_torch = torch.tensor([1, 2, 3])
+        param_tf = tf.Variable([1, 2, 3])
+        param_jax = jax.numpy.array([1, 2, 3])
+        param_qml = npp.array([1, 2, 3])
+        param_np = np.array([1, 2, 3])
+
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1[0], p1[1], p1[2], wires=wire),
+                op1(p2[0], p2[1], p2[2], wires=wire),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1[0], p1[1], p1[2], wires=wire),
+                op1(p2[0], p2[1], p2[2], wires=wire),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(*param_qml, wires=wire),
+            op1(*param_qml_1, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(*param_qml, wires=wire),
+            op1(*param_qml_1, wires=wire),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+    @pytest.mark.parametrize("op1", PARAMETRIZED_OPERATIONS_3P_1W)
+    def test_equal_simple_op_3p1w(self, op1):
+        """Test changing parameter or wire returns False"""
+        wire = 0
+        param = [0.123] * 3
+        assert qml.equal(
+            op1(*param, wires=wire),
+            op1(*param, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(*param, wires=wire),
+            op1(*param, wires=wire + 1),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(*param, wires=wire),
+            op1(param[0] * 2, param[1], param[2], wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+
+    @pytest.mark.all_interfaces
+    def test_equal_op_remaining(self):
+        """Test optional arguments are working"""
+        wire = 0
+
+        import torch
+        import tensorflow as tf
+        import jax
+
+        param_torch = torch.tensor([1, 2])
+        param_tf = tf.Variable([1, 2])
+        param_jax = jax.numpy.array([1, 2])
+        param_qml = npp.array([1, 2])
+        param_np = np.array([1, 2])
+
+        op1 = PARAMETRIZED_OPERATIONS_2P_1W[0]
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1[0], p1[1], wires=wire),
+                op1(p2[0], p2[1], wires=wire),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1[0], p1[1], wires=wire),
+                op1(p2[0], p2[1], wires=wire),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(*param_qml, wires=wire),
+            op1(*param_qml_1, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(*param_qml, wires=wire),
+            op1(*param_qml_1, wires=wire),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+        wire = [1, 2, 3]
+        param_torch = torch.tensor(1)
+        param_tf = tf.Variable(1)
+        param_jax = jax.numpy.array(1)
+        param_qml = npp.array(1)
+        param_np = np.array(1)
+
+        op1 = PARAMETRIZED_OPERATIONS_1P_3W[0]
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+        wire = [1, 2]
+        param_torch = torch.tensor([1, 2, 3])
+        param_tf = tf.Variable([1, 2, 3])
+        param_jax = jax.numpy.array([1, 2, 3])
+        param_qml = npp.array([1, 2, 3])
+        param_np = np.array([1, 2, 3])
+
+        op1 = PARAMETRIZED_OPERATIONS_3P_2W[0]
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1[0], p1[1], p1[2], wires=wire),
+                op1(p2[0], p2[1], p2[2], wires=wire),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1[0], p1[1], p1[2], wires=wire),
+                op1(p2[0], p2[1], p2[2], wires=wire),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(*param_qml, wires=wire),
+            op1(*param_qml_1, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(*param_qml, wires=wire),
+            op1(*param_qml_1, wires=wire),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+        wire = 0
+        param_torch = torch.tensor(1)
+        param_tf = tf.Variable(1)
+        param_jax = jax.numpy.array(1)
+        param_qml = npp.array(1)
+        param_np = np.array(1)
+
+        op1 = PARAMETRIZED_OPERATIONS_Remaining[0]
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1, "Y", wires=wire),
+                op1(p2, "Y", wires=wire),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1, "Y", wires=wire),
+                op1(p2, "Y", wires=wire),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(param_qml, "Y", wires=wire),
+            op1(param_qml_1, "Y", wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param_qml, "Y", wires=wire),
+            op1(param_qml_1, "Y", wires=wire),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+        wire = 0
+        param_torch = torch.tensor([[1, 0], [0, 1]]) * 1j
+        param_tf = tf.Variable([[1, 0], [0, 1]], dtype=tf.complex64) * 1j
+        param_jax = jax.numpy.eye(2) * 1j
+        param_qml = npp.eye(2) * 1j
+        param_np = np.eye(2) * 1j
+
+        op1 = PARAMETRIZED_OPERATIONS_Remaining[1]
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+        wire = 0
+        param_torch = torch.tensor([1.0, 1.0j])
+        param_tf = tf.Variable([1.0 + 0j, 1.0j])
+        param_jax = jax.numpy.array([1.0, 1.0j])
+        param_qml = npp.array([1.0, 1.0j])
+        param_np = np.array([1.0, 1.0j])
+
+        op1 = PARAMETRIZED_OPERATIONS_Remaining[2]
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1, wires=wire),
+                op1(p2, wires=wire),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param_qml, wires=wire),
+            op1(param_qml_1, wires=wire),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+        wire = 0
+        param_torch = torch.tensor([[1, 0], [0, 1]]) * 1j
+        param_tf = tf.Variable([[1, 0], [0, 1]], dtype=tf.complex64) * 1j
+        param_jax = jax.numpy.eye(2) * 1j
+        param_qml = npp.eye(2) * 1j
+        param_np = np.eye(2) * 1j
+
+        op1 = PARAMETRIZED_OPERATIONS_Remaining[3]
+        param_list = [param_qml, param_torch, param_jax, param_tf, param_np]
+        for p1, p2 in itertools.combinations(param_list, 2):
+            assert qml.equal(
+                op1(p1, wires=wire, control_wires=wire + 1),
+                op1(p2, wires=wire, control_wires=wire + 1),
+                check_trainability=False,
+                check_interface=False,
+            )
+            assert not qml.equal(
+                op1(p1, wires=wire, control_wires=wire + 1),
+                op1(p2, wires=wire, control_wires=wire + 1),
+                check_trainability=False,
+                check_interface=True,
+            )
+
+        param_qml_1 = param_qml.copy()
+        param_qml_1.requires_grad = False
+        assert qml.equal(
+            op1(param_qml, wires=wire, control_wires=wire + 1),
+            op1(param_qml_1, wires=wire, control_wires=wire + 1),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param_qml, wires=wire, control_wires=wire + 1),
+            op1(param_qml_1, wires=wire, control_wires=wire + 1),
+            check_trainability=True,
+            check_interface=False,
+        )
+
+    def test_equal_simple_op_remaining(self):
+        """Test changing parameter or wire returns False"""
+        wire = 0
+        param = [0.123] * 2
+        op1 = PARAMETRIZED_OPERATIONS_2P_1W[0]
+        assert qml.equal(
+            op1(*param, wires=wire),
+            op1(*param, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(*param, wires=wire),
+            op1(*param, wires=wire + 1),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(*param, wires=wire),
+            op1(param[0] * 2, param[1], wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+
+        wire = [1, 2, 3]
+        param = 0.123
+        op1 = PARAMETRIZED_OPERATIONS_1P_3W[0]
+        assert qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param * 2, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=[w + 1 for w in wire]),
+            check_trainability=False,
+            check_interface=False,
+        )
+
+        wire = [1, 2]
+        param = [0.123] * 3
+        op1 = PARAMETRIZED_OPERATIONS_3P_2W[0]
+        assert qml.equal(
+            op1(*param, wires=wire),
+            op1(*param, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(*param, wires=wire),
+            op1(*param, wires=[w + 1 for w in wire]),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(*param, wires=wire),
+            op1(param[0] * 2, param[1], param[2], wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+
+        wire = 0
+        param = 0.123
+        op1 = PARAMETRIZED_OPERATIONS_Remaining[0]
+        assert qml.equal(
+            op1(param, "Y", wires=wire),
+            op1(param, "Y", wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, "Y", wires=wire),
+            op1(param * 2, "Y", wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, "Y", wires=wire),
+            op1(param, "Y", wires=wire + 1),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, "Y", wires=wire),
+            op1(param, "Z", wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+
+        wire = 0
+        param = np.eye(2) * 1j
+        op1 = PARAMETRIZED_OPERATIONS_Remaining[1]
+        assert qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param * 2, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=wire + 1),
+            check_trainability=False,
+            check_interface=False,
+        )
+
+        wire = 0
+        param = np.array([1.0, 1.0j])
+        op1 = PARAMETRIZED_OPERATIONS_Remaining[2]
+        assert qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param * 2, wires=wire),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire),
+            op1(param, wires=wire + 1),
+            check_trainability=False,
+            check_interface=False,
+        )
+
+        wire = 0
+        param = np.eye(2) * 1j
+        op1 = PARAMETRIZED_OPERATIONS_Remaining[3]
+        assert qml.equal(
+            op1(param, wires=wire, control_wires=wire + 1),
+            op1(param, wires=wire, control_wires=wire + 1),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire, control_wires=wire + 1),
+            op1(param * 2, wires=wire, control_wires=wire + 1),
+            check_trainability=False,
+            check_interface=False,
+        )
+        assert not qml.equal(
+            op1(param, wires=wire, control_wires=wire + 1),
+            op1(param, wires=wire + 2, control_wires=wire + 1),
+            check_trainability=False,
+            check_interface=False,
+        )