Repository: PennyLaneAI/pennylane

Issue:
[unitaryhack] Add the ISWAP operation
*This issue has been tagged for contributions during [unitaryHACK](https://unitaryfund.github.io/unitaryhack/participating-projects.html)*

**Context**
We would like to define a new operation called `ISWAP`, see [further details here](https://qiskit.org/documentation/stubs/qiskit.circuit.library.iSwapGate.html).

**Steps to take**

This would involve the following steps to be completed:

*A. Adding the operation*

1. Adding the `ISWAP` new class for the operation to `pennylane/ops/qubit.py`. Complete its docstring in a similar fashion as for other operations. Make sure the class is accessible through `qml.ISWAP`.
2. Add its decomposition: a definition for how the operation could be decomposed into elementary operations,
3. Add its matrix, eigenvalues and adjoint (see `S` or `T` operations as examples of non-Hermitian unitaries),
4. *Test:* include tests for the operation in the `tests/ops/test_qubit_ops.py` file.

*B. Adding `default.qubit` support*

5. Add support for the operation in the `default.qubit` device,
6. *Test:* test the operation with `default.qubit` in the `tests/devices/test_default_qubit.py` file.

*Bonus:* Add support to the interface-specific `default.qubit` devices too: `default.qubit.autograd` and `default.qubit.tf`. This will require the operation to be defined in a framework-specific way. These devices inherit the supported operations from `default.qubit`, so if this step is not tackled, the `ISWAP` can be removed from the `operations` dictionary for `default.qubit.autograd` and `default.qubit.tf`.


Discussion:

Resolving patch:
diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py
--- a/pennylane/devices/default_qubit.py
+++ b/pennylane/devices/default_qubit.py
@@ -105,6 +105,7 @@ class DefaultQubit(QubitDevice):
         "SX",
         "CNOT",
         "SWAP",
+        "ISWAP",
         "CSWAP",
         "Toffoli",
         "CY",
diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py
--- a/pennylane/ops/qubit.py
+++ b/pennylane/ops/qubit.py
@@ -533,6 +533,53 @@ def _controlled(self, wire):
         CSWAP(wires=wire + self.wires)
 
 
+class ISWAP(Operation):
+    r"""ISWAP(wires)
+    The i-swap operator
+
+    .. math:: ISWAP = \begin{bmatrix}
+            1 & 0 & 0 & 0 \\
+            0 & 0 & i & 0\\
+            0 & i & 0 & 0\\
+            0 & 0 & 0 & 1
+        \end{bmatrix}.
+
+    **Details:**
+
+    * Number of wires: 2
+    * Number of parameters: 0
+
+    Args:
+        wires (Sequence[int]): the wires the operation acts on
+    """
+    num_params = 0
+    num_wires = 2
+    par_domain = None
+
+    @classmethod
+    def _matrix(cls, *params):
+        return np.array([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]])
+
+    @classmethod
+    def _eigvals(cls, *params):
+        return np.array([1j, -1j, 1, 1])
+
+    @staticmethod
+    def decomposition(wires):
+        decomp_ops = [
+            S(wires=wires[0]),
+            S(wires=wires[1]),
+            Hadamard(wires=wires[0]),
+            CNOT(wires=[wires[0], wires[1]]),
+            CNOT(wires=[wires[1], wires[0]]),
+            Hadamard(wires=wires[1]),
+        ]
+        return decomp_ops
+
+    def adjoint(self):
+        return ISWAP(wires=self.wires).inv()
+
+
 class CSWAP(Operation):
     r"""CSWAP(wires)
     The controlled-swap operator
@@ -2982,6 +3029,7 @@ def adjoint(self):
     "CZ",
     "CY",
     "SWAP",
+    "ISWAP",
     "CSWAP",
     "Toffoli",
     "RX",


Test patch:
diff --git a/pennylane/devices/tests/test_gates.py b/pennylane/devices/tests/test_gates.py
--- a/pennylane/devices/tests/test_gates.py
+++ b/pennylane/devices/tests/test_gates.py
@@ -64,6 +64,7 @@
     "Rot": qml.Rot(0, 0, 0, wires=[0]),
     "S": qml.S(wires=[0]),
     "SWAP": qml.SWAP(wires=[0, 1]),
+    "ISWAP": qml.ISWAP(wires=[0, 1]),
     "T": qml.T(wires=[0]),
     "SX": qml.SX(wires=[0]),
     "Toffoli": qml.Toffoli(wires=[0, 1, 2]),
@@ -90,6 +91,7 @@
 T = np.diag([1, np.exp(1j * np.pi / 4)])
 SX = 0.5 * np.array([[1 + 1j, 1 - 1j], [1 - 1j, 1 + 1j]])
 SWAP = np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])
+ISWAP = np.array([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]])
 CNOT = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
 CZ = np.diag([1, 1, 1, -1])
 CY = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]])
@@ -167,7 +169,7 @@
     (qml.RZ, rz),
 ]
 # list of all non-parametrized two-qubit gates
-two_qubit = [(qml.CNOT, CNOT), (qml.SWAP, SWAP), (qml.CZ, CZ), (qml.CY, CY)]
+two_qubit = [(qml.CNOT, CNOT), (qml.SWAP, SWAP), (qml.ISWAP, ISWAP), (qml.CZ, CZ), (qml.CY, CY)]
 # list of all parametrized two-qubit gates
 two_qubit_param = [(qml.CRX, crx), (qml.CRY, cry), (qml.CRZ, crz)]
 two_qubit_multi_param = [(qml.CRot, crot)]
diff --git a/tests/devices/test_default_qubit.py b/tests/devices/test_default_qubit.py
--- a/tests/devices/test_default_qubit.py
+++ b/tests/devices/test_default_qubit.py
@@ -204,7 +204,29 @@ def test_apply_operation_single_wire_no_parameters_inverse(
         ),
     ]
 
-    @pytest.mark.parametrize("operation,input,expected_output", test_data_two_wires_no_parameters)
+    test_data_iswap = [
+        (qml.ISWAP, [1, 0, 0, 0], [1, 0, 0, 0]),
+        (qml.ISWAP, [0, 0, 1, 0], [0, 1j, 0, 0]),
+        (
+            qml.ISWAP,
+            [1 / math.sqrt(2), 0, -1 / math.sqrt(2), 0],
+            [1 / math.sqrt(2), -1j / math.sqrt(2), 0, 0],
+        ),
+    ]
+
+    test_data_iswap_inv = [
+        (qml.ISWAP, [1, 0, 0, 0], [1, 0, 0, 0]),
+        (qml.ISWAP, [0, 0, 1, 0], [0, -1j, 0, 0]),
+        (
+            qml.ISWAP,
+            [1 / math.sqrt(2), 0, -1 / math.sqrt(2), 0],
+            [1 / math.sqrt(2), 1j / math.sqrt(2), 0, 0],
+        ),
+    ]
+
+    all_two_wires_no_parameters = test_data_two_wires_no_parameters + test_data_iswap
+
+    @pytest.mark.parametrize("operation,input,expected_output", all_two_wires_no_parameters)
     def test_apply_operation_two_wires_no_parameters(
         self, qubit_device_2_wires, tol, operation, input, expected_output
     ):
@@ -218,7 +240,9 @@ def test_apply_operation_two_wires_no_parameters(
             qubit_device_2_wires._state.flatten(), np.array(expected_output), atol=tol, rtol=0
         )
 
-    @pytest.mark.parametrize("operation,input,expected_output", test_data_two_wires_no_parameters)
+    all_two_wires_no_parameters_inv = test_data_two_wires_no_parameters + test_data_iswap_inv
+
+    @pytest.mark.parametrize("operation,input,expected_output", all_two_wires_no_parameters_inv)
     def test_apply_operation_two_wires_no_parameters_inverse(
         self, qubit_device_2_wires, tol, operation, input, expected_output
     ):
@@ -229,7 +253,10 @@ def test_apply_operation_two_wires_no_parameters_inverse(
         qubit_device_2_wires.apply([operation(wires=[0, 1]).inv()])
 
         assert np.allclose(
-            qubit_device_2_wires._state.flatten(), np.array(expected_output), atol=tol, rtol=0
+            qubit_device_2_wires._state.flatten(),
+            np.array(expected_output),
+            atol=tol,
+            rtol=0,
         )
 
     test_data_three_wires_no_parameters = [
diff --git a/tests/gate_data.py b/tests/gate_data.py
--- a/tests/gate_data.py
+++ b/tests/gate_data.py
@@ -22,6 +22,7 @@
 # Two qubit gates
 CNOT = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])  #: CNOT gate
 SWAP = np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])  #: SWAP gate
+ISWAP = np.array([[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]])  #: ISWAP gate
 CZ = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]])  #: CZ gate
 S = np.array([[1, 0], [0, 1j]])  #: Phase Gate
 T = np.array([[1, 0], [0, cmath.exp(1j * np.pi / 4)]])  #: T Gate
diff --git a/tests/ops/test_qubit_ops.py b/tests/ops/test_qubit_ops.py
--- a/tests/ops/test_qubit_ops.py
+++ b/tests/ops/test_qubit_ops.py
@@ -36,6 +36,7 @@
     StateOneProjector,
     CNOT,
     SWAP,
+    ISWAP,
     CZ,
     S,
     T,
@@ -336,6 +337,7 @@ def test_hermitian_exceptions(self):
 NON_PARAMETRIZED_OPERATIONS = [
     (qml.CNOT, CNOT),
     (qml.SWAP, SWAP),
+    (qml.ISWAP, ISWAP),
     (qml.CZ, CZ),
     (qml.S, S),
     (qml.T, T),
@@ -367,6 +369,7 @@ def test_matrices(self, ops, mat, tol):
             qml.CZ(wires=[0, 1]),
             qml.CY(wires=[0, 1]),
             qml.SWAP(wires=[0, 1]),
+            qml.ISWAP(wires=[0, 1]),
             qml.CSWAP(wires=[0, 1, 2]),
             qml.PauliRot(0.123, "Y", wires=0),
             qml.Rot(0.123, 0.456, 0.789, wires=0),
@@ -646,6 +649,42 @@ def test_CRot_decomposition(self, tol, phi, theta, omega, monkeypatch):
 
         assert np.allclose(decomposed_matrix, op.matrix, atol=tol, rtol=0)
 
+    def test_ISWAP_decomposition(self, tol):
+        """Tests that the decomposition of the ISWAP gate is correct"""
+        op = qml.ISWAP(wires=[0, 1])
+        res = op.decomposition(op.wires)
+
+        assert len(res) == 6
+
+        assert res[0].wires == Wires([0])
+        assert res[1].wires == Wires([1])
+        assert res[2].wires == Wires([0])
+        assert res[3].wires == Wires([0, 1])
+        assert res[4].wires == Wires([1, 0])
+        assert res[5].wires == Wires([1])
+
+        assert res[0].name == "S"
+        assert res[1].name == "S"
+        assert res[2].name == "Hadamard"
+        assert res[3].name == "CNOT"
+        assert res[4].name == "CNOT"
+        assert res[5].name == "Hadamard"
+
+        mats = []
+        for i in reversed(res):
+            if i.wires == Wires([1]):
+                mats.append(np.kron(np.eye(2), i.matrix))
+            elif i.wires == Wires([0]):
+                mats.append(np.kron(i.matrix, np.eye(2)))
+            elif i.wires == Wires([1, 0]) and i.name == "CNOT":
+                mats.append(np.array([[1, 0, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0]]))
+            else:
+                mats.append(i.matrix)
+
+        decomposed_matrix = np.linalg.multi_dot(mats)
+
+        assert np.allclose(decomposed_matrix, op.matrix, atol=tol, rtol=0)
+
     def test_phase_shift(self, tol):
         """Test phase shift is correct"""
 
@@ -877,6 +916,13 @@ def test_qubit_unitary_not_matrix_exception(self, U):
         with pytest.raises(ValueError, match="must be a square matrix"):
             qml.QubitUnitary(U, wires=0).matrix
 
+    def test_iswap_eigenval(self):
+        """Tests that the ISWAP eigenvalue matches the numpy eigenvalues of the ISWAP matrix"""
+        op = qml.ISWAP(wires=[0, 1])
+        exp = np.linalg.eigvals(op.matrix)
+        res = op.eigvals
+        assert np.allclose(res, exp)
+
     @pytest.mark.parametrize("phi", [-0.1, 0.2, 0.5])
     def test_controlled_phase_shift_matrix_and_eigvals(self, phi):
         """Tests that the ControlledPhaseShift operation calculates the correct matrix and