Repository: qiboteam/qibo

Issue:
Mølmer-Sørenson gate
In order to ease benchmarking and testing algorithms to run on IonQ hardware, it would be very useful to have their native gates already implemented in qibo.
Personally, I'm particularly interested in the parameterized Mølmer-Sørenson gate.
A list and description of their gates can be found at: https://ionq.com/docs/getting-started-with-native-gates




Discussion:

Patch:
diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst
index aa545bebe..b9d0f9894 100644
--- a/doc/source/api-reference/qibo.rst
+++ b/doc/source/api-reference/qibo.rst
@@ -520,6 +520,7 @@ Parametric ZZ interaction (RZZ)
     :members:
     :member-order: bysource
 
+
 Special gates
 ^^^^^^^^^^^^^
 
@@ -551,8 +552,34 @@ Fusion gate
     :members:
     :member-order: bysource
 
+IONQ Native gates
+^^^^^^^^^^^^^^^^^
+
+GPI
+"""
+
+.. autoclass:: qibo.gates.GPI
+    :members:
+    :member-order: bysource
+
+GPI2
+""""
+
+.. autoclass:: qibo.gates.GPI2
+    :members:
+    :member-order: bysource
+
+Mølmer–Sørensen (MS)
+""""""""""""""""""""
+
+.. autoclass:: qibo.gates.MS
+    :members:
+    :member-order: bysource
+
+
 _______________________
 
+
 .. _Channels:
 
 Channels
diff --git a/src/qibo/backends/npmatrices.py b/src/qibo/backends/npmatrices.py
index b36a91068..1413dbf04 100644
--- a/src/qibo/backends/npmatrices.py
+++ b/src/qibo/backends/npmatrices.py
@@ -82,6 +82,16 @@ class NumpyMatrices:
         phase = self.np.exp(0.5j * theta)
         return self.np.array([[self.np.conj(phase), 0], [0, phase]], dtype=self.dtype)
 
+    def GPI(self, phi):
+        phase = self.np.exp(1.0j * phi)
+        return self.np.array([[0, self.np.conj(phase)], [phase, 0]], dtype=self.dtype)
+
+    def GPI2(self, phi):
+        phase = self.np.exp(1.0j * phi)
+        return self.np.array(
+            [[1, -1.0j * self.np.conj(phase)], [-1.0j * phase, 1]], dtype=self.dtype
+        ) / self.np.sqrt(2)
+
     def U1(self, theta):
         phase = self.np.exp(1j * theta)
         return self.np.array([[1, 0], [0, phase]], dtype=self.dtype)
@@ -231,6 +241,20 @@ class NumpyMatrices:
             dtype=self.dtype,
         )
 
+    def MS(self, phi0, phi1):
+        plus = self.np.exp(1.0j * (phi0 + phi1))
+        minus = self.np.exp(1.0j * (phi0 - phi1))
+
+        return self.np.array(
+            [
+                [1, 0, 0, -1.0j * self.np.conj(plus)],
+                [0, 1, -1.0j * self.np.conj(minus), 0],
+                [0, -1.0j * minus, 1, 0],
+                [-1.0j * plus, 0, 0, 1],
+            ],
+            dtype=self.dtype,
+        ) / self.np.sqrt(2)
+
     @cached_property
     def TOFFOLI(self):
         return self.np.array(
diff --git a/src/qibo/gates/gates.py b/src/qibo/gates/gates.py
index 5aa4ea037..a455988cf 100644
--- a/src/qibo/gates/gates.py
+++ b/src/qibo/gates/gates.py
@@ -40,9 +40,12 @@ PARAMETRIZED_GATES = {
     "rx",
     "ry",
     "rz",
+    "gpi",
+    "gpi2",
     "rxx",
     "ryy",
     "rzz",
+    "ms",
     "u1",
     "u2",
     "u3",
@@ -64,6 +67,8 @@ DRAW_LABELS = {
     "rx": "RX",
     "ry": "RY",
     "rz": "RZ",
+    "gpi": "GPI",
+    "gpi2": "GPI2",
     "u1": "U1",
     "u2": "U2",
     "u3": "U3",
@@ -86,6 +91,7 @@ DRAW_LABELS = {
     "rzz": "RZZ",
     "Unitary": "U",
     "fswap": "fx",
+    "ms": "MS",
     "PauliNoiseChannel": "PN",
     "KrausChannel": "K",
     "UnitaryChannel": "U",
@@ -502,6 +508,74 @@ class RZ(_Rn_):
         return 0.5
 
 
+class GPI(ParametrizedGate):
+    """The GPI gate.
+
+    Corresponds to the following unitary matrix
+
+    .. math::
+        \\begin{pmatrix}
+        0 & e^{- i \\phi} \\\\
+        e^{i \\phi} & 0 \\\\
+        \\end{pmatrix}
+
+    Args:
+        q (int): the qubit id number.
+        phi (float): phase.
+        trainable (bool): whether gate parameters can be updated using
+            :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`
+            (default is ``True``).
+    """
+
+    def __init__(self, q, phi, trainable=True):
+        super().__init__(trainable)
+        self.name = "gpi"
+        self.target_qubits = (q,)
+
+        self.parameter_names = "phi"
+        self.parameters = phi
+        self.nparams = 1
+
+        self.init_args = [q]
+        self.init_kwargs = {"phi": phi, "trainable": trainable}
+
+
+class GPI2(ParametrizedGate):
+    """The GPI2 gate.
+
+    Corresponds to the following unitary matrix
+
+    .. math::
+        \\begin{pmatrix}
+        1 & -i e^{- i \\phi} \\\\
+        -i e^{i \\phi} & 1 \\\\
+        \\end{pmatrix}
+
+    Args:
+        q (int): the qubit id number.
+        phi (float): phase.
+        trainable (bool): whether gate parameters can be updated using
+            :meth:`qibo.models.circuit.AbstractCircuit.set_parameters`
+            (default is ``True``).
+    """
+
+    def __init__(self, q, phi, trainable=True):
+        super().__init__(trainable)
+        self.name = "gpi"
+        self.target_qubits = (q,)
+
+        self.parameter_names = "phi"
+        self.parameters = phi
+        self.nparams = 1
+
+        self.init_args = [q]
+        self.init_kwargs = {"phi": phi, "trainable": trainable}
+
+    def _dagger(self) -> "Gate":
+        """"""
+        return self.__class__(self.target_qubits[0], self.parameters[0] + math.pi)
+
+
 class _Un_(ParametrizedGate):
     """Abstract class for defining the U1, U2 and U3 gates.
 
@@ -1245,6 +1319,50 @@ class RZZ(_Rnn_):
         self.name = "rzz"
 
 
+class MS(ParametrizedGate):
+    """The Mølmer–Sørensen (MS) gate is a two qubit gate native to trapped ions.
+
+    Corresponds to the following unitary matrix
+
+    .. math::
+        \\begin{pmatrix}
+        1 & 0 & 0 & -i e^{-i( \\phi_0 +  \\phi_1)} \\\\
+        0 & 1 & -i e^{-i( \\phi_0 -  \\phi_1)} \\\\
+        0 & -i e^{i( \\phi_0 -  \\phi_1)} & 1 & 0 \\\\
+        -i e^{i( \\phi_0 +  \\phi_1)} & 0 & 0 & 1 \\\\
+        \\end{pmatrix}
+
+    Args:
+        q0 (int): the first qubit to be swapped id number.
+        q1 (int): the second qubit to be swapped id number.
+        phi0 (float): first qubit's phase.
+        phi1 (float): second qubit's phase
+        trainable (bool): whether gate parameters can be updated using
+            :meth:`qibo.models.circuit.Circuit.set_parameters`
+            (default is ``True``).
+    """
+
+    # TODO: Check how this works with QASM.
+
+    def __init__(self, q0, q1, phi0, phi1, trainable=True):
+        super().__init__(trainable)
+        self.name = "ms"
+        self.target_qubits = (q0, q1)
+
+        self.parameter_names = ["phi0", "phi1"]
+        self.parameters = phi0, phi1
+        self.nparams = 2
+
+        self.init_args = [q0, q1]
+        self.init_kwargs = {"phi0": phi0, "phi1": phi1, "trainable": trainable}
+
+    def _dagger(self) -> "Gate":
+        """"""
+        q0, q1 = self.target_qubits
+        phi0, phi1 = self.parameters
+        return self.__class__(q0, q1, phi0 + math.pi, phi1)
+
+
 class TOFFOLI(Gate):
     """The Toffoli gate.
 


Test patch:
diff --git a/src/qibo/tests/test_gates_gates.py b/src/qibo/tests/test_gates_gates.py
index 8d439e5ac..82f12bb8c 100644
--- a/src/qibo/tests/test_gates_gates.py
+++ b/src/qibo/tests/test_gates_gates.py
@@ -142,6 +142,32 @@ def test_rz(backend, applyx):
     backend.assert_allclose(final_state, target_state)
 
 
+def test_gpi(backend):
+    phi = 0.1234
+    initial_state = random_state(1)
+    final_state = apply_gates(backend, [gates.GPI(0, phi)], initial_state=initial_state)
+
+    phase = np.exp(1.0j * phi)
+    matrix = np.array([[0, np.conj(phase)], [phase, 0]])
+
+    target_state = matrix.dot(initial_state)
+    backend.assert_allclose(final_state, target_state)
+
+
+def test_gpi2(backend):
+    phi = 0.1234
+    initial_state = random_state(1)
+    final_state = apply_gates(
+        backend, [gates.GPI2(0, phi)], initial_state=initial_state
+    )
+
+    phase = np.exp(1.0j * phi)
+    matrix = np.array([[1, -1.0j * np.conj(phase)], [-1.0j * phase, 1]]) / np.sqrt(2)
+
+    target_state = matrix.dot(initial_state)
+    backend.assert_allclose(final_state, target_state)
+
+
 def test_u1(backend):
     theta = 0.1234
     final_state = apply_gates(backend, [gates.X(0), gates.U1(0, theta)], nqubits=1)
@@ -340,7 +366,6 @@ def test_ryy(backend):
     backend.assert_allclose(final_state, target_state)
 
 
-# @pytest.mark.parametrize("applyx", [True, False])
 def test_rzz(backend):
     theta = 0.1234
     final_state = apply_gates(
@@ -351,6 +376,29 @@ def test_rzz(backend):
     backend.assert_allclose(final_state, target_state)
 
 
+def test_ms(backend):
+    phi0 = 0.1234
+    phi1 = 0.4321
+    final_state = apply_gates(
+        backend,
+        [gates.H(0), gates.H(1), gates.MS(0, 1, phi0=phi0, phi1=phi1)],
+        nqubits=2,
+    )
+    target_state = np.ones_like(final_state) / 2.0
+    plus = np.exp(1.0j * (phi0 + phi1))
+    minus = np.exp(1.0j * (phi0 - phi1))
+
+    matrix = np.eye(4, dtype=target_state.dtype)
+    matrix[3, 0] = -1.0j * plus
+    matrix[0, 3] = -1.0j * np.conj(plus)
+    matrix[2, 1] = -1.0j * minus
+    matrix[1, 2] = -1.0j * np.conj(minus)
+    matrix /= np.sqrt(2)
+    target_state = matrix.dot(target_state)
+
+    backend.assert_allclose(final_state, target_state)
+
+
 @pytest.mark.parametrize("applyx", [False, True])
 def test_toffoli(backend, applyx):
     if applyx:
@@ -632,6 +680,8 @@ GATES = [
     ("RX", (0, 0.1)),
     ("RY", (0, 0.2)),
     ("RZ", (0, 0.3)),
+    ("GPI", (0, 0.1)),
+    ("GPI2", (0, 0.2)),
     ("U1", (0, 0.1)),
     ("U2", (0, 0.2, 0.3)),
     ("U3", (0, 0.1, 0.2, 0.3)),
@@ -645,6 +695,7 @@ GATES = [
     ("RXX", (0, 1, 0.1)),
     ("RYY", (0, 1, 0.2)),
     ("RZZ", (0, 1, 0.3)),
+    ("MS", (0, 1, 0.1, 0.2)),
 ]