Repository: Qiskit/qiskit

Issue:
mapping from  Euler angles to quaternions in mapper
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?
To fix some of the mapper i going to change the 1q optimization from euler angles to quaternions and it should fix the mapper issues that keep getting brought up. 

This is a low priority as most times it works but the correct way to do it is quaternions.  




Discussion:
Here are 2 tiny tests that trigger this error.
```python
qasm_string_1= """OPENQASM 2.0;
include "qelib1.inc";
qreg qr[2];
cx qr[0],qr[1];
rz(0.7) qr[1];
rx(1.570796) qr[1];
"""
```
```python
qasm_string_2 = """OPENQASM 2.0;
include "qelib1.inc";
qreg qr[2];
y qr[0];
h qr[0];
s qr[0];
h qr[0];
"""
```
In both cases, the error occurs during compilation (specifically during `yzy_to_zyz`). To trigger it:
```python
from qiskit import *
import Qconfig
from pprint import pprint
register(Qconfig.APItoken)
circ = load_qasm_string(qasm_string_1)
qobj = compile(circ, 'ibmqx4')
pprint(qobj)
```

Resolving patch:
diff --git a/qiskit/mapper/_mapping.py b/qiskit/mapper/_mapping.py
--- a/qiskit/mapper/_mapping.py
+++ b/qiskit/mapper/_mapping.py
@@ -23,6 +23,7 @@
 from qiskit.mapper import MapperError
 from qiskit.dagcircuit import DAGCircuit
 from qiskit.unroll import DagUnroller, DAGBackend
+from qiskit.mapper._quaternion import quaternion_from_euler
 
 logger = logging.getLogger(__name__)
 
@@ -587,34 +588,6 @@ def swap_mapper(circuit_graph, coupling_graph,
     return dagcircuit_output, initial_layout
 
 
-def test_trig_solution(theta, phi, lamb, xi, theta1, theta2):
-    r"""Test if arguments are a solution to a system of equations.
-
-    .. math::
-       \cos(\phi+\lambda) \cos(\\theta) = \cos(xi) * \cos(\\theta1+\\theta2)
-
-       \sin(\phi+\lambda) \cos(\\theta) = \sin(xi) * \cos(\\theta1-\\theta2)
-
-       \cos(\phi-\lambda) \sin(\\theta) = \cos(xi) * \sin(\\theta1+\\theta2)
-
-       \sin(\phi-\lambda) \sin(\\theta) = \sin(xi) * \sin(-\\theta1+\\theta2)
-
-    Returns the maximum absolute difference between right and left hand sides
-    as a Max symbol. See:
-    http://docs.sympy.org/latest/modules/functions/elementary.html?highlight=max
-    """
-    delta1 = sympy.Abs(sympy.cos(phi + lamb) * sympy.cos(theta) -
-                       sympy.cos(xi) * sympy.cos(theta1 + theta2))
-    delta2 = sympy.Abs(sympy.sin(phi + lamb) * sympy.cos(theta) -
-                       sympy.sin(xi) * sympy.cos(theta1 - theta2))
-    delta3 = sympy.Abs(sympy.cos(phi - lamb) * sympy.sin(theta) -
-                       sympy.cos(xi) * sympy.sin(theta1 + theta2))
-    delta4 = sympy.Abs(sympy.sin(phi - lamb) * sympy.sin(theta) -
-                       sympy.sin(xi) * sympy.sin(-theta1 + theta2))
-
-    return sympy.Max(delta1, delta2, delta3, delta4)
-
-
 def yzy_to_zyz(xi, theta1, theta2, eps=1e-9):
     """Express a Y.Z.Y single qubit gate as a Z.Y.Z gate.
 
@@ -622,94 +595,26 @@ def yzy_to_zyz(xi, theta1, theta2, eps=1e-9):
 
     .. math::
 
-    Ry(2*theta1).Rz(2*xi).Ry(2*theta2) = Rz(2*phi).Ry(2*theta).Rz(2*lambda)
+    Ry(theta1).Rz(xi).Ry(theta2) = Rz(phi).Ry(theta).Rz(lambda)
 
-    for theta, phi, and lambda. This is equivalent to solving the system
-    given in the comment for test_solution. Use eps for comparisons with zero.
+    for theta, phi, and lambda.
 
     Return a solution theta, phi, and lambda.
     """
-    solutions = []  # list of potential solutions
-    # Four cases to avoid singularities
-    # TODO investigate when these can be .is_zero
-    if sympy.Abs(sympy.cos(xi)).evalf() < eps:
-        solutions.append((theta2 - theta1, xi, 0))
-    elif sympy.Abs(sympy.sin(theta1 + theta2)).evalf() < eps:
-        phi_minus_lambda = [
-            sympy.pi / 2,
-            3 * sympy.pi / 2,
-            sympy.pi / 2,
-            3 * sympy.pi / 2]
-        stheta_1 = sympy.asin(sympy.sin(xi) * sympy.sin(-theta1 + theta2))
-        stheta_2 = sympy.asin(-sympy.sin(xi) * sympy.sin(-theta1 + theta2))
-        stheta_3 = sympy.pi - stheta_1
-        stheta_4 = sympy.pi - stheta_2
-        stheta = [stheta_1, stheta_2, stheta_3, stheta_4]
-        phi_plus_lambda = list(map(lambda x:
-                                   sympy.acos(sympy.cos(theta1 + theta2) *
-                                              sympy.cos(xi) / sympy.cos(x)),
-                                   stheta))
-        sphi = [(term[0] + term[1]) / 2 for term in
-                zip(phi_plus_lambda, phi_minus_lambda)]
-        slam = [(term[0] - term[1]) / 2 for term in
-                zip(phi_plus_lambda, phi_minus_lambda)]
-        solutions = list(zip(stheta, sphi, slam))
-    elif sympy.Abs(sympy.cos(theta1 + theta2)).evalf() < eps:
-        phi_plus_lambda = [
-            sympy.pi / 2,
-            3 * sympy.pi / 2,
-            sympy.pi / 2,
-            3 * sympy.pi / 2]
-        stheta_1 = sympy.acos(sympy.sin(xi) * sympy.cos(theta1 - theta2))
-        stheta_2 = sympy.acos(-sympy.sin(xi) * sympy.cos(theta1 - theta2))
-        stheta_3 = -stheta_1
-        stheta_4 = -stheta_2
-        stheta = [stheta_1, stheta_2, stheta_3, stheta_4]
-        phi_minus_lambda = list(map(lambda x:
-                                    sympy.acos(sympy.sin(theta1 + theta2) *
-                                               sympy.cos(xi) / sympy.sin(x)),
-                                    stheta))
-        sphi = [(term[0] + term[1]) / 2 for term in
-                zip(phi_plus_lambda, phi_minus_lambda)]
-        slam = [(term[0] - term[1]) / 2 for term in
-                zip(phi_plus_lambda, phi_minus_lambda)]
-        solutions = list(zip(stheta, sphi, slam))
-    else:
-        phi_plus_lambda = sympy.atan(sympy.sin(xi) * sympy.cos(theta1 - theta2) /
-                                     (sympy.cos(xi) * sympy.cos(theta1 + theta2)))
-        phi_minus_lambda = sympy.atan(sympy.sin(xi) * sympy.sin(-theta1 +
-                                                                theta2) /
-                                      (sympy.cos(xi) * sympy.sin(theta1 +
-                                                                 theta2)))
-        sphi = (phi_plus_lambda + phi_minus_lambda) / 2
-        slam = (phi_plus_lambda - phi_minus_lambda) / 2
-        solutions.append((sympy.acos(sympy.cos(xi) * sympy.cos(theta1 + theta2) /
-                                     sympy.cos(sphi + slam)), sphi, slam))
-        solutions.append((sympy.acos(sympy.cos(xi) * sympy.cos(theta1 + theta2) /
-                                     sympy.cos(sphi + slam + sympy.pi)),
-                          sphi + sympy.pi / 2,
-                          slam + sympy.pi / 2))
-        solutions.append((sympy.acos(sympy.cos(xi) * sympy.cos(theta1 + theta2) /
-                                     sympy.cos(sphi + slam)),
-                          sphi + sympy.pi / 2, slam - sympy.pi / 2))
-        solutions.append((sympy.acos(sympy.cos(xi) * sympy.cos(theta1 + theta2) /
-                                     sympy.cos(sphi + slam + sympy.pi)),
-                          sphi + sympy.pi, slam))
-
-    # Select the first solution with the required accuracy
-    deltas = list(map(lambda x: test_trig_solution(x[0], x[1], x[2],
-                                                   xi, theta1, theta2),
-                      solutions))
-    for delta_sol in zip(deltas, solutions):
-        # TODO investigate when this can be .is_zero
-        if delta_sol[0].evalf() < eps:
-            return delta_sol[1]
-    logger.debug("xi=%s", xi)
-    logger.debug("theta1=%s", theta1)
-    logger.debug("theta2=%s", theta2)
-    logger.debug("solutions=%s", pprint.pformat(solutions))
-    logger.debug("deltas=%s", pprint.pformat(deltas))
-    assert False, "Error! No solution found. This should not happen."
+    Q = quaternion_from_euler([theta1, xi, theta2], 'yzy')
+    euler = Q.to_zyz()
+    P = quaternion_from_euler(euler, 'zyz')
+    # output order different than rotation order
+    out_angles = (euler[1], euler[0], euler[2])
+    abs_inner = abs(P.data.dot(Q.data))
+    if not np.allclose(abs_inner, 1, eps):
+        logger.debug("xi=%s", xi)
+        logger.debug("theta1=%s", theta1)
+        logger.debug("theta2=%s", theta2)
+        logger.debug("solutions=%s", out_angles)
+        logger.debug("abs_inner=%s", abs_inner)
+        raise MapperError('YZY and ZYZ angles do not give same rotation matrix.')
+    return out_angles
 
 
 def compose_u3(theta1, phi1, lambda1, theta2, phi2, lambda2):
@@ -724,10 +629,10 @@ def compose_u3(theta1, phi1, lambda1, theta2, phi2, lambda2):
     Return theta, phi, lambda.
     """
     # Careful with the factor of two in yzy_to_zyz
-    thetap, phip, lambdap = yzy_to_zyz((lambda1 + phi2) / 2,
-                                       theta1 / 2, theta2 / 2)
-    (theta, phi, lamb) = (2 * thetap, phi1 + 2 * phip, lambda2 + 2 * lambdap)
-    return (theta.simplify(), phi.simplify(), lamb.simplify())
+    thetap, phip, lambdap = yzy_to_zyz((lambda1 + phi2),
+                                       theta1, theta2)
+    (theta, phi, lamb) = (thetap, phi1 + phip, lambda2 + lambdap)
+    return (theta, phi, lamb)
 
 
 def cx_cancellation(circuit):
diff --git a/qiskit/mapper/_quaternion.py b/qiskit/mapper/_quaternion.py
new file mode 100644
--- /dev/null
+++ b/qiskit/mapper/_quaternion.py
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2017, IBM.
+#
+# This source code is licensed under the Apache License, Version 2.0 found in
+# the LICENSE.txt file in the root directory of this source tree.
+
+"""
+A module for using quaternions.
+"""
+import math
+import numpy as np
+import scipy.linalg as la
+
+
+class Quaternion:
+    """A class representing a Quaternion.
+    """
+
+    def __init__(self, data):
+        self.data = np.asarray(data, dtype=float)
+
+    def __call__(self, idx):
+        return self.data[idx]
+
+    def __repr__(self):
+        return np.array_str(self.data)
+
+    def __str__(self):
+        return np.array_str(self.data)
+
+    def __mul__(self, r):  # pylint: disable=C0103
+        if isinstance(r, Quaternion):
+            q = self  # pylint: disable=C0103
+            out_data = np.zeros(4, dtype=float)
+            out_data[0] = r(0)*q(0) - r(1)*q(1) - r(2)*q(2) - r(3)*q(3)
+            out_data[1] = r(0)*q(1) + r(1)*q(0) - r(2)*q(3) + r(3)*q(2)
+            out_data[2] = r(0)*q(2) + r(1)*q(3) + r(2)*q(0) - r(3)*q(1)
+            out_data[3] = r(0)*q(3) - r(1)*q(2) + r(2)*q(1) + r(3)*q(0)
+            return Quaternion(out_data)
+        else:
+            raise Exception('Multiplication by other not supported.')
+
+    def norm(self):
+        """ Norm of quaternion.
+        """
+        return la.norm(self.data)
+
+    def normalize(self, inplace=False):
+        """Normalizes a Quaternion to unit length
+        so that it represents a valid roation.
+
+        Args:
+            inplace (bool): Do an inplace normalization.
+
+        Returns:
+            Quaternion: Normalized quaternion.
+        """
+        if inplace:
+            nrm = self.norm()
+            self.data /= nrm
+            return None
+        nrm = self.norm()
+        data_copy = np.array(self.data, copy=True)
+        data_copy /= nrm
+        return Quaternion(data_copy)
+
+    def to_matrix(self):
+        """Converts a unit-length quaternion to a rotation matrix.
+
+        Returns:
+            ndarray: Rotation matrix.
+        """
+        w, x, y, z = self.normalize().data  # pylint: disable=C0103
+        mat = np.array([
+            [1-2*y**2-2*z**2, 2*x*y-2*z*w, 2*x*z+2*y*w],
+            [2*x*y+2*z*w, 1-2*x**2-2*z**2, 2*y*z-2*x*w],
+            [2*x*z-2*y*w, 2*y*z+2*x*w, 1-2*x**2-2*y**2]
+        ], dtype=float)
+        return mat
+
+    def to_zyz(self):
+        """Converts a unit-length quaternion to a sequence
+        of ZYZ Euler angles.
+
+        Returns:
+            ndarray: Array of Euler angles.
+        """
+        mat = self.to_matrix()
+        euler = np.zeros(3, dtype=float)
+        if mat[2, 2] < 1:
+            if mat[2, 2] > -1:
+                euler[0] = math.atan2(mat[1, 2], mat[0, 2])
+                euler[1] = math.acos(mat[2, 2])
+                euler[2] = math.atan2(mat[2, 1], -mat[2, 0])
+            else:
+                euler[0] = -math.atan2(mat[1, 0], mat[1, 1])
+                euler[1] = np.pi
+        else:
+            euler[0] = math.atan2(mat[1, 0], mat[1, 1])
+        return euler
+
+
+def quaternion_from_axis_rotation(angle, axis):
+    """Return quaternion for rotation about given axis.
+
+    Args:
+        angle (float): Angle in radians.
+        axis (str): Axis for rotation
+
+    Returns:
+        Quaternion: Quaternion for axis rotation.
+
+    Raises:
+        ValueError: Invalid input axis.
+    """
+    out = np.zeros(4, dtype=float)
+    if axis == 'x':
+        out[1] = 1
+    elif axis == 'y':
+        out[2] = 1
+    elif axis == 'z':
+        out[3] = 1
+    else:
+        raise ValueError('Invalid axis input.')
+    out *= math.sin(angle/2.0)
+    out[0] = math.cos(angle/2.0)
+    return Quaternion(out)
+
+
+def quaternion_from_euler(angles, order='yzy'):
+    """Generate a quaternion from a set of Euler angles.
+
+    Args:
+        angles (array_like): Array of Euler angles.
+        order (str): Order of Euler rotations.  'yzy' is default.
+
+    Returns:
+        Quaternion: Quaternion representation of Euler rotation.
+    """
+    angles = np.asarray(angles, dtype=float)
+    quat = quaternion_from_axis_rotation(angles[0], order[0])\
+        * (quaternion_from_axis_rotation(angles[1], order[1])
+           * quaternion_from_axis_rotation(angles[2], order[2]))
+    quat.normalize(inplace=True)
+    return quat
+
+
+def _rotm(angle, axis):
+    """Generates a rotation matrix for a given angle and axis.
+
+    Args:
+        angle (float): Roation angle in radians.
+        axis (str): Axis for rotation: 'x', 'y', 'z'
+
+    Returns:
+        ndarray: Rotation matrix.
+
+    Raises:
+        ValueError: Invalid input axis.
+    """
+    direction = np.zeros(3, dtype=float)
+    if axis == 'x':
+        direction[0] = 1
+    elif axis == 'y':
+        direction[1] = 1
+    elif axis == 'z':
+        direction[2] = 1
+    else:
+        raise ValueError('Invalid axis.')
+    direction = np.asarray(direction, dtype=float)
+    sin_angle = math.sin(angle)
+    cos_angle = math.cos(angle)
+    rot = np.diag([cos_angle, cos_angle, cos_angle])
+    rot += np.outer(direction, direction) * (1.0 - cos_angle)
+    direction *= sin_angle
+    rot += np.array([
+        [0, -direction[2], direction[1]],
+        [direction[2], 0, -direction[0]],
+        [-direction[1], direction[0], 0]
+    ])
+    return rot


Test patch:
diff --git a/test/python/test_mapper.py b/test/python/test_mapper.py
--- a/test/python/test_mapper.py
+++ b/test/python/test_mapper.py
@@ -8,12 +8,21 @@
 # pylint: disable=invalid-name,missing-docstring
 
 import unittest
-
-from qiskit import QuantumProgram
-from qiskit import qasm, unroll, mapper
+import qiskit.wrapper
+from qiskit import (QuantumProgram, qasm, unroll, mapper, load_qasm_string)
 from .common import QiskitTestCase
 
 
+class FakeQX4BackEnd(object):
+    """A fake QX4 backend.
+    """
+    def __init__(self):
+        qx4_cmap = [[1, 0], [2, 0], [2, 1], [3, 2], [3, 4], [4, 2]]
+        self.configuration = {'name': 'fake_qx4', 'basis_gates': 'u1,u2,u3,cx,id',
+                              'simulator': False, 'n_qubits': 5,
+                              'coupling_map': qx4_cmap}
+
+
 class MapperTest(QiskitTestCase):
     """Test the mapper."""
 
@@ -200,6 +209,19 @@ def test_already_mapped(self):
 
         self.assertEqual(sorted(cx_qubits), [[3, 4], [3, 14], [5, 4], [9, 8], [12, 11], [13, 4]])
 
+    def test_yzy_zyz_cases(self):
+        """Test mapper function yzy_to_zyz works in previously failed cases.
+
+        See: https://github.com/QISKit/qiskit-terra/issues/607
+        """
+        backend = FakeQX4BackEnd()
+        circ1 = load_qasm_string(yzy_zyz_1)
+        qobj1 = qiskit.wrapper.compile(circ1, backend)
+        self.assertIsInstance(qobj1, dict)
+        circ2 = load_qasm_string(yzy_zyz_2)
+        qobj2 = qiskit.wrapper.compile(circ2, backend)
+        self.assertIsInstance(qobj2, dict)
+
 
 # QASMs expected by the tests.
 EXPECTED_QASM_SYMBOLIC_BINARY = """OPENQASM 2.0;
@@ -266,6 +288,23 @@ def test_already_mapped(self):
 u1((0.3+(-pi))^2) qr[0];
 measure qr[0] -> cr[0];"""
 
+yzy_zyz_1 = """OPENQASM 2.0;
+include "qelib1.inc";
+qreg qr[2];
+cx qr[0],qr[1];
+rz(0.7) qr[1];
+rx(1.570796) qr[1];
+"""
+
+yzy_zyz_2 = """OPENQASM 2.0;
+include "qelib1.inc";
+qreg qr[2];
+y qr[0];
+h qr[0];
+s qr[0];
+h qr[0];
+"""
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/test/python/test_quaternions.py b/test/python/test_quaternions.py
new file mode 100644
--- /dev/null
+++ b/test/python/test_quaternions.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2017, IBM.
+#
+# This source code is licensed under the Apache License, Version 2.0 found in
+# the LICENSE.txt file in the root directory of this source tree.
+
+"""Tests qiskit/mapper/_quaternion"""
+import numpy as np
+import scipy.linalg as la
+from qiskit.mapper._quaternion import (quaternion_from_euler, _rotm)
+from .common import QiskitTestCase
+
+
+class TestQuaternions(QiskitTestCase):
+    """Tests qiskit/mapper_quaternion"""
+
+    def test_random_euler(self):
+        """Quaternion from random Euler rotations."""
+        # Random angles and axes
+        axes = {0: 'x', 1: 'y', 2: 'z'}
+        for _ in range(1000):
+            rnd = 4*np.pi*(np.random.random(3)-0.5)
+            idx = np.random.randint(3, size=3)
+            mat1 = _rotm(rnd[0], axes[idx[0]]).dot(
+                _rotm(rnd[1], axes[idx[1]]).dot(_rotm(rnd[2], axes[idx[2]])))
+            axes_str = ''.join(axes[i] for i in idx)
+            quat = quaternion_from_euler(rnd, axes_str)
+            mat2 = quat.to_matrix()
+            self.assertTrue(np.allclose(mat1, mat2))
+
+    def test_orthogonality(self):
+        """Quaternion rotation matrix orthogonality"""
+        # Check orthogonality of generated roation matrix
+        axes = {0: 'x', 1: 'y', 2: 'z'}
+        for _ in range(1000):
+            rnd = 4*np.pi*(np.random.random(3)-0.5)
+            idx = np.random.randint(3, size=3)
+            axes_str = ''.join(axes[i] for i in idx)
+            quat = quaternion_from_euler(rnd, axes_str)
+            mat = quat.to_matrix()
+            self.assertTrue(np.allclose(mat.dot(mat.T),
+                                        np.identity(3, dtype=float)))
+
+    def test_det(self):
+        """Quaternion det = 1"""
+        # Check det for rotation and not reflection
+        axes = {0: 'x', 1: 'y', 2: 'z'}
+        for _ in range(1000):
+            rnd = 4*np.pi*(np.random.random(3)-0.5)
+            idx = np.random.randint(3, size=3)
+            axes_str = ''.join(axes[i] for i in idx)
+            quat = quaternion_from_euler(rnd, axes_str)
+            mat = quat.to_matrix()
+            self.assertTrue(np.allclose(la.det(mat), 1))
+
+    def test_equiv_quaternions(self):
+        """Different Euler rotations give same quaternion, up to sign."""
+        # Check if euler angles from to_zyz return same quaternion
+        # up to a sign (2pi rotation)
+        rot = {0: 'xyz', 1: 'xyx', 2: 'xzy',
+               3: 'xzx', 4: 'yzx', 5: 'yzy',
+               6: 'yxz', 7: 'yxy', 8: 'zxy',
+               9: 'zxz', 10: 'zyx', 11: 'zyz'}
+
+        for _ in range(1000):
+            rnd = 4*np.pi*(np.random.random(3)-0.5)
+            idx = np.random.randint(12)
+            quat1 = quaternion_from_euler(rnd, rot[idx])
+            euler = quat1.to_zyz()
+            quat2 = quaternion_from_euler(euler, 'zyz')
+            self.assertTrue(np.allclose(abs(quat1.data.dot(quat2.data)), 1))