Repository: qiboteam/qibo

Issue:
Add CY gate
In qibo the `CY` gate is currently missing, as the clifford simulator #1076 will implement this, it could be useful to have it also for standard state simulation.

Discussion:

Patch:
diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst
index d76ac5507..1f4c99fe9 100644
--- a/doc/source/api-reference/qibo.rst
+++ b/doc/source/api-reference/qibo.rst
@@ -565,6 +565,13 @@ Controlled-NOT (CNOT)
     :members:
     :member-order: bysource
 
+Controlled-Y (CY)
+"""""""""""""""""""""
+
+.. autoclass:: qibo.gates.CY
+    :members:
+    :member-order: bysource
+
 Controlled-phase (CZ)
 """""""""""""""""""""
 
diff --git a/src/qibo/backends/__init__.py b/src/qibo/backends/__init__.py
index 14bfaa169..4b25c71bb 100644
--- a/src/qibo/backends/__init__.py
+++ b/src/qibo/backends/__init__.py
@@ -100,6 +100,7 @@ class QiboMatrices:
         self.S = self.matrices.S
         self.SDG = self.matrices.SDG
         self.CNOT = self.matrices.CNOT
+        self.CY = self.matrices.CY
         self.CZ = self.matrices.CZ
         self.CSX = self.matrices.CSX
         self.CSXDG = self.matrices.CSXDG
diff --git a/src/qibo/backends/npmatrices.py b/src/qibo/backends/npmatrices.py
index 490489833..366c466cd 100644
--- a/src/qibo/backends/npmatrices.py
+++ b/src/qibo/backends/npmatrices.py
@@ -121,6 +121,13 @@ class NumpyMatrices:
             [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]], dtype=self.dtype
         )
 
+    @cached_property
+    def CY(self):
+        return self.np.array(
+            [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]],
+            dtype=self.dtype,
+        )
+
     @cached_property
     def CZ(self):
         return self.np.array(
diff --git a/src/qibo/gates/gates.py b/src/qibo/gates/gates.py
index f289b33c6..0e94225fc 100644
--- a/src/qibo/gates/gates.py
+++ b/src/qibo/gates/gates.py
@@ -178,6 +178,15 @@ class Y(Gate):
     def qasm_label(self):
         return "y"
 
+    @Gate.check_controls
+    def controlled_by(self, *q):
+        """Fall back to CY if there is only one control."""
+        if len(q) == 1:
+            gate = CY(q[0], self.target_qubits[0])
+        else:
+            gate = super().controlled_by(*q)
+        return gate
+
     def basis_rotation(self):
         from qibo import matrices  # pylint: disable=C0415
 
@@ -972,6 +981,49 @@ class CNOT(Gate):
         return [self.__class__(q0, q1)]
 
 
+class CY(Gate):
+    """The Controlled-:math:`Y` gate.
+
+    Corresponds to the following unitary matrix
+
+    .. math::
+        \\begin{pmatrix}
+        1 & 0 & 0 & 0 \\\\
+        0 & 1 & 0 & 0 \\\\
+        0 & 0 & 0 & -i \\\\
+        0 & 0 & i & 0 \\\\
+        \\end{pmatrix}
+
+    Args:
+        q0 (int): the control qubit id number.
+        q1 (int): the target qubit id number.
+    """
+
+    def __init__(self, q0, q1):
+        super().__init__()
+        self.name = "cy"
+        self.draw_label = "Y"
+        self.control_qubits = (q0,)
+        self.target_qubits = (q1,)
+        self.init_args = [q0, q1]
+        self.clifford = True
+        self.unitary = True
+
+    @property
+    def qasm_label(self):
+        return "cy"
+
+    def decompose(self) -> List[Gate]:
+        """Decomposition of :math:`\\text{CY}` gate.
+
+        Decompose :math:`\\text{CY}` gate into :class:`qibo.gates.SDG` in
+        the target qubit, followed by :class:`qibo.gates.CNOT`, followed
+        by a :class:`qibo.gates.S` in the target qubit.
+        """
+        q0, q1 = self.init_args
+        return [SDG(q1), CNOT(q0, q1), S(q1)]
+
+
 class CZ(Gate):
     """The Controlled-Phase gate.
 


Test patch:
diff --git a/tests/test_gates_abstract.py b/tests/test_gates_abstract.py
index efeb527d3..05c4b9a92 100644
--- a/tests/test_gates_abstract.py
+++ b/tests/test_gates_abstract.py
@@ -228,10 +228,13 @@ def test_one_qubit_rotations_controlled_by(gatename, params):
     assert gate.parameters == params
 
 
-def test_cnot_and_cz_init():
+def test_cnot_and_cy_and_cz_init():
     gate = gates.CNOT(0, 1)
     assert gate.target_qubits == (1,)
     assert gate.control_qubits == (0,)
+    gate = gates.CY(4, 7)
+    assert gate.target_qubits == (7,)
+    assert gate.control_qubits == (4,)
     gate = gates.CZ(3, 2)
     assert gate.target_qubits == (2,)
     assert gate.control_qubits == (3,)
diff --git a/tests/test_gates_density_matrix.py b/tests/test_gates_density_matrix.py
index cd0c025c5..c40825099 100644
--- a/tests/test_gates_density_matrix.py
+++ b/tests/test_gates_density_matrix.py
@@ -78,14 +78,17 @@ def test_one_qubit_gates(backend, gatename, gatekwargs):
 @pytest.mark.parametrize("gatename", ["H", "X", "Y", "Z", "S", "SDG", "T", "TDG"])
 def test_controlled_by_one_qubit_gates(backend, gatename):
     nqubits = 2
-    initial_rho = random_density_matrix(2**nqubits, backend=backend)
+    initial_rho = random_density_matrix(2**nqubits, seed=1, backend=backend)
     gate = getattr(gates, gatename)(1).controlled_by(0)
-    final_rho = apply_gates(backend, [gate], 2, initial_rho)
+    final_rho = apply_gates(backend, [gate], 2, np.copy(initial_rho))
 
     matrix = backend.to_numpy(backend.matrix(getattr(gates, gatename)(1)))
     cmatrix = np.eye(4, dtype=matrix.dtype)
     cmatrix[2:, 2:] = matrix
-    target_rho = np.einsum("ab,bc,cd->ad", cmatrix, initial_rho, cmatrix.conj().T)
+    cmatrix = backend.cast(cmatrix, dtype=cmatrix.dtype)
+    target_rho = np.einsum(
+        "ab,bc,cd->ad", cmatrix, initial_rho, np.transpose(np.conj(cmatrix))
+    )
     backend.assert_allclose(final_rho, target_rho)
 
 
@@ -93,6 +96,7 @@ def test_controlled_by_one_qubit_gates(backend, gatename):
     "gatename,gatekwargs",
     [
         ("CNOT", {}),
+        ("CY", {}),
         ("CZ", {}),
         ("SWAP", {}),
         ("CRX", {"theta": 0.123}),
@@ -111,8 +115,10 @@ def test_two_qubit_gates(backend, gatename, gatekwargs):
     gate = getattr(gates, gatename)(0, 1, **gatekwargs)
     final_rho = apply_gates(backend, [gate], 2, initial_rho)
 
-    matrix = backend.to_numpy(gate.matrix(backend))
-    target_rho = np.einsum("ab,bc,cd->ad", matrix, initial_rho, matrix.conj().T)
+    matrix = gate.matrix(backend)
+    target_rho = np.einsum(
+        "ab,bc,cd->ad", matrix, initial_rho, np.transpose(np.conj(matrix))
+    )
     backend.assert_allclose(final_rho, target_rho, atol=PRECISION_TOL)
 
 
diff --git a/tests/test_gates_gates.py b/tests/test_gates_gates.py
index 957d1e9c2..8bb0e190e 100644
--- a/tests/test_gates_gates.py
+++ b/tests/test_gates_gates.py
@@ -478,6 +478,56 @@ def test_cnot(backend, applyx):
     assert gates.CNOT(0, 1).unitary
 
 
+@pytest.mark.parametrize("seed_observable", list(range(1, 10 + 1)))
+@pytest.mark.parametrize("seed_state", list(range(1, 10 + 1)))
+@pytest.mark.parametrize("controlled_by", [False, True])
+def test_cy(backend, controlled_by, seed_state, seed_observable):
+    nqubits = 2
+    initial_state = random_statevector(2**nqubits, seed=seed_state, backend=backend)
+    matrix = np.array(
+        [
+            [1, 0, 0, 0],
+            [0, 1, 0, 0],
+            [0, 0, 0, -1j],
+            [0, 0, 1j, 0],
+        ]
+    )
+    matrix = backend.cast(matrix, dtype=matrix.dtype)
+
+    target_state = np.dot(matrix, initial_state)
+    # test decomposition
+    final_state_decompose = apply_gates(
+        backend,
+        gates.CY(0, 1).decompose(),
+        nqubits=nqubits,
+        initial_state=initial_state,
+    )
+
+    if controlled_by:
+        gate = gates.Y(1).controlled_by(0)
+    else:
+        gate = gates.CY(0, 1)
+
+    final_state = apply_gates(backend, [gate], initial_state=initial_state)
+
+    assert gate.name == "cy"
+
+    backend.assert_allclose(final_state, target_state)
+
+    # testing random expectation value due to global phase difference
+    observable = random_hermitian(2**nqubits, seed=seed_observable, backend=backend)
+    backend.assert_allclose(
+        np.transpose(np.conj(final_state_decompose))
+        @ observable
+        @ final_state_decompose,
+        np.transpose(np.conj(target_state)) @ observable @ target_state,
+    )
+
+    assert gates.CY(0, 1).qasm_label == "cy"
+    assert gates.CY(0, 1).clifford
+    assert gates.CY(0, 1).unitary
+
+
 @pytest.mark.parametrize("seed_observable", list(range(1, 10 + 1)))
 @pytest.mark.parametrize("seed_state", list(range(1, 10 + 1)))
 @pytest.mark.parametrize("controlled_by", [False, True])