Repository: qiboteam/qibo

Issue:
Extend the IonQ native MS gate to include the partially entangled version
We already include the MS gate in Qibo, but recently, IonQ has introduced the Partially Entangled MS gate in their set of native gates. (https://ionq.com/docs/getting-started-with-native-gates)

It adds the possibility to change the angle of rotation from 0 to pi/2, and they even claim that small rotations are more accurate on hardware. We should consider adding that angle to our already available MS gate. 



Discussion:

Patch:
diff --git a/src/qibo/backends/npmatrices.py b/src/qibo/backends/npmatrices.py
index 1413dbf04..80498321d 100644
--- a/src/qibo/backends/npmatrices.py
+++ b/src/qibo/backends/npmatrices.py
@@ -241,19 +241,29 @@ class NumpyMatrices:
             dtype=self.dtype,
         )
 
-    def MS(self, phi0, phi1):
+    def MS(self, phi0, phi1, theta):
         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],
+                [
+                    self.np.cos(theta / 2),
+                    0,
+                    0,
+                    -1.0j * self.np.conj(plus) * self.np.sin(theta / 2),
+                ],
+                [
+                    0,
+                    self.np.cos(theta / 2),
+                    -1.0j * self.np.conj(minus) * self.np.sin(theta / 2),
+                    0,
+                ],
+                [0, -1.0j * minus * self.np.sin(theta / 2), self.np.cos(theta / 2), 0],
+                [-1.0j * plus * self.np.sin(theta / 2), 0, 0, self.np.cos(theta / 2)],
             ],
             dtype=self.dtype,
-        ) / self.np.sqrt(2)
+        )
 
     @cached_property
     def TOFFOLI(self):
diff --git a/src/qibo/gates/gates.py b/src/qibo/gates/gates.py
index c8a913861..11744621d 100644
--- a/src/qibo/gates/gates.py
+++ b/src/qibo/gates/gates.py
@@ -1374,16 +1374,17 @@ class RZZ(_Rnn_):
 
 
 class MS(ParametrizedGate):
-    """The Mølmer–Sørensen (MS) gate is a two qubit gate native to trapped ions.
+    """The (partially entangling) Mølmer–Sørensen (MS) gate
+    is a two qubit gate native to trapped ions.
 
     Corresponds to the following unitary matrix
 
     .. math::
-        \\frac{1}{\\sqrt{2}} \\, \\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 \\\\
+        \\begin{pmatrix}
+        \\cos(\\theta / 2) & 0 & 0 & -i e^{-i( \\phi_0 +  \\phi_1)} \\sin(\\theta / 2) \\\\
+        0 & \\cos(\\theta / 2) & -i e^{-i( \\phi_0 -  \\phi_1)} \\sin(\\theta / 2) & 0 \\\\
+        0 & -i e^{i( \\phi_0 -  \\phi_1)} \\sin(\\theta / 2) & \\cos(\\theta / 2) & 0 \\\\
+        -i e^{i( \\phi_0 +  \\phi_1)} \\sin(\\theta / 2) & 0 & 0 & \\cos(\\theta / 2) \\\\
         \\end{pmatrix}
 
     Args:
@@ -1391,31 +1392,45 @@ class MS(ParametrizedGate):
         q1 (int): the second qubit to be swapped id number.
         phi0 (float): first qubit's phase.
         phi1 (float): second qubit's phase
+        theta (float, optional): arbitrary angle in the interval
+            :math:`0 \\leq \\theta \\leq \\pi /2`.  If :math:`\\theta \\rightarrow \\pi / 2`,
+            the fully-entangling MS gate is defined. Defaults to :math:`\\pi / 2`.
         trainable (bool): whether gate parameters can be updated using
-            :meth:`qibo.models.circuit.Circuit.set_parameters`
-            (default is ``True``).
+            :meth:`qibo.models.circuit.Circuit.set_parameters`.
+            Defaults to ``True``.
     """
 
     # TODO: Check how this works with QASM.
 
-    def __init__(self, q0, q1, phi0, phi1, trainable=True):
+    def __init__(self, q0, q1, phi0, phi1, theta: float = math.pi / 2, trainable=True):
         super().__init__(trainable)
         self.name = "ms"
         self.draw_label = "MS"
         self.target_qubits = (q0, q1)
 
-        self.parameter_names = ["phi0", "phi1"]
-        self.parameters = phi0, phi1
-        self.nparams = 2
+        if theta < 0.0 or theta > math.pi / 2:
+            raise_error(
+                ValueError,
+                f"Theta is defined in the interval 0 <= theta <= pi/2, but it is {theta}.",
+            )
+
+        self.parameter_names = ["phi0", "phi1", "theta"]
+        self.parameters = phi0, phi1, theta
+        self.nparams = 3
 
         self.init_args = [q0, q1]
-        self.init_kwargs = {"phi0": phi0, "phi1": phi1, "trainable": trainable}
+        self.init_kwargs = {
+            "phi0": phi0,
+            "phi1": phi1,
+            "theta": theta,
+            "trainable": trainable,
+        }
 
     def _dagger(self) -> "Gate":
         """"""
         q0, q1 = self.target_qubits
-        phi0, phi1 = self.parameters
-        return self.__class__(q0, q1, phi0 + math.pi, phi1)
+        phi0, phi1, theta = self.parameters
+        return self.__class__(q0, q1, phi0 + math.pi, phi1, theta)
 
 
 class TOFFOLI(Gate):


Test patch:
diff --git a/tests/test_gates_gates.py b/tests/test_gates_gates.py
index 37fd4e0e9..4a35676bd 100644
--- a/tests/test_gates_gates.py
+++ b/tests/test_gates_gates.py
@@ -447,6 +447,13 @@ def test_rzz(backend):
 def test_ms(backend):
     phi0 = 0.1234
     phi1 = 0.4321
+    theta = np.pi / 2
+
+    with pytest.raises(NotImplementedError):
+        gates.MS(0, 1, phi0=phi0, phi1=phi1, theta=theta).qasm_label
+    with pytest.raises(ValueError):
+        gates.MS(0, 1, phi0=phi0, phi1=phi1, theta=np.pi)
+
     final_state = apply_gates(
         backend,
         [gates.H(0), gates.H(1), gates.MS(0, 1, phi0=phi0, phi1=phi1)],
@@ -467,9 +474,6 @@ def test_ms(backend):
 
     backend.assert_allclose(final_state, target_state)
 
-    with pytest.raises(NotImplementedError):
-        gates.MS(0, 1, phi0=phi0, phi1=phi1).qasm_label
-
 
 @pytest.mark.parametrize("applyx", [False, True])
 def test_toffoli(backend, applyx):
@@ -782,7 +786,7 @@ GATES = [
     ("RXX", (0, 1, 0.1)),
     ("RYY", (0, 1, 0.2)),
     ("RZZ", (0, 1, 0.3)),
-    ("MS", (0, 1, 0.1, 0.2)),
+    ("MS", (0, 1, 0.1, 0.2, 0.3)),
 ]