diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index d76ac5507a..1f4c99fe95 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 14bfaa169d..4b25c71bbb 100644 --- a/src/qibo/backends/__init__.py +++ b/src/qibo/backends/__init__.py @@ -100,6 +100,7 @@ def create(self, dtype): 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 490489833c..366c466cd7 100644 --- a/src/qibo/backends/npmatrices.py +++ b/src/qibo/backends/npmatrices.py @@ -121,6 +121,13 @@ def CNOT(self): [[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 f289b33c66..0e94225fca 100644 --- a/src/qibo/gates/gates.py +++ b/src/qibo/gates/gates.py @@ -178,6 +178,15 @@ def __init__(self, q): 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 @@ def decompose(self, *free, use_toffolis: bool = True) -> List[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. diff --git a/tests/test_gates_abstract.py b/tests/test_gates_abstract.py index efeb527d3f..05c4b9a92b 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 cd0c025c5d..c40825099c 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 957d1e9c20..8bb0e190ea 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])