From 58b4b0769bbb6398a6ebb0888e0de1d2b5bb5c13 Mon Sep 17 00:00:00 2001
From: "Olivier J.N. Bertrand" <olivier.bertrand@uni-bielefeld.de>
Date: Mon, 5 Mar 2018 23:09:53 +0100
Subject: [PATCH] Add homogeneous transformation

---
 navipy/maths/__init__.py                      |   1 +
 navipy/maths/constants.py                     |  22 +
 navipy/maths/euler.py                         | 109 +++++
 navipy/maths/homogeneous_transformations.py   | 424 ++++++++++++++++++
 navipy/maths/quaternion.py                    | 189 ++++++++
 navipy/maths/random.py                        |  49 ++
 navipy/maths/test_euler.py                    |  28 ++
 .../maths/test_homogenous_transformations.py  | 274 +++++++++++
 navipy/maths/test_quaternion.py               |  87 ++++
 navipy/maths/test_random_cust.py              |  19 +
 navipy/maths/tools.py                         |  57 +++
 11 files changed, 1259 insertions(+)
 create mode 100644 navipy/maths/constants.py
 create mode 100644 navipy/maths/euler.py
 create mode 100644 navipy/maths/homogeneous_transformations.py
 create mode 100644 navipy/maths/quaternion.py
 create mode 100644 navipy/maths/random.py
 create mode 100644 navipy/maths/test_euler.py
 create mode 100644 navipy/maths/test_homogenous_transformations.py
 create mode 100644 navipy/maths/test_quaternion.py
 create mode 100644 navipy/maths/test_random_cust.py
 create mode 100644 navipy/maths/tools.py

diff --git a/navipy/maths/__init__.py b/navipy/maths/__init__.py
index e69de29..d3173e6 100644
--- a/navipy/maths/__init__.py
+++ b/navipy/maths/__init__.py
@@ -0,0 +1 @@
+#package
diff --git a/navipy/maths/constants.py b/navipy/maths/constants.py
new file mode 100644
index 0000000..3a2fd3c
--- /dev/null
+++ b/navipy/maths/constants.py
@@ -0,0 +1,22 @@
+"""
+
+"""
+import numpy as np
+# For testing whether a number is close to zero or not we use epsilon:
+_EPS = np.finfo(float).eps * 4.0
+
+# axis sequences for Euler angles
+_NEXT_AXIS = [1, 2, 0, 1]
+
+# map axes strings to/from tuples of inner axis, parity, repetition, frame
+_AXES2TUPLE = {
+    'sxyz': (0, 0, 0, 0), 'sxyx': (0, 0, 1, 0), 'sxzy': (0, 1, 0, 0),
+    'sxzx': (0, 1, 1, 0), 'syzx': (1, 0, 0, 0), 'syzy': (1, 0, 1, 0),
+    'syxz': (1, 1, 0, 0), 'syxy': (1, 1, 1, 0), 'szxy': (2, 0, 0, 0),
+    'szxz': (2, 0, 1, 0), 'szyx': (2, 1, 0, 0), 'szyz': (2, 1, 1, 0),
+    'rzyx': (0, 0, 0, 1), 'rxyx': (0, 0, 1, 1), 'ryzx': (0, 1, 0, 1),
+    'rxzx': (0, 1, 1, 1), 'rxzy': (1, 0, 0, 1), 'ryzy': (1, 0, 1, 1),
+    'rzxy': (1, 1, 0, 1), 'ryxy': (1, 1, 1, 1), 'ryxz': (2, 0, 0, 1),
+    'rzxz': (2, 0, 1, 1), 'rxyz': (2, 1, 0, 1), 'rzyz': (2, 1, 1, 1)}
+
+_TUPLE2AXES = dict((v, k) for k, v in _AXES2TUPLE.items())
diff --git a/navipy/maths/euler.py b/navipy/maths/euler.py
new file mode 100644
index 0000000..a125a63
--- /dev/null
+++ b/navipy/maths/euler.py
@@ -0,0 +1,109 @@
+"""
+
+"""
+import numpy as np
+import navipy.maths.constants as constants
+import navipy.maths.quaternion as quat
+
+
+def matrix(ai, aj, ak, axes='sxyz'):
+    """Return homogeneous rotation matrix from Euler angles and axis sequence.
+
+    ai, aj, ak : Euler's roll, pitch and yaw angles
+    axes : One of 24 axis sequences as string or encoded tuple
+    """
+    try:
+        firstaxis, parity, repetition, frame = constants._AXES2TUPLE[axes]
+    except (AttributeError, KeyError):
+        constants._TUPLE2AXES[axes]  # validation
+        firstaxis, parity, repetition, frame = axes
+
+    i = firstaxis
+    j = constants._NEXT_AXIS[i + parity]
+    k = constants._NEXT_AXIS[i - parity + 1]
+
+    if frame:
+        ai, ak = ak, ai
+    if parity:
+        ai, aj, ak = -ai, -aj, -ak
+
+    si, sj, sk = np.sin(ai), np.sin(aj), np.sin(ak)
+    ci, cj, ck = np.cos(ai), np.cos(aj), np.cos(ak)
+    cc, cs = ci * ck, ci * sk
+    sc, ss = si * ck, si * sk
+
+    M = np.identity(4)
+    if repetition:
+        M[i, i] = cj
+        M[i, j] = sj * si
+        M[i, k] = sj * ci
+        M[j, i] = sj * sk
+        M[j, j] = -cj * ss + cc
+        M[j, k] = -cj * cs - sc
+        M[k, i] = -sj * ck
+        M[k, j] = cj * sc + cs
+        M[k, k] = cj * cc - ss
+    else:
+        M[i, i] = cj * ck
+        M[i, j] = sj * sc - cs
+        M[i, k] = sj * cc + ss
+        M[j, i] = cj * sk
+        M[j, j] = sj * ss + cc
+        M[j, k] = sj * cs - sc
+        M[k, i] = -sj
+        M[k, j] = cj * si
+        M[k, k] = cj * ci
+    return M
+
+
+def from_matrix(matrix, axes='sxyz'):
+    """Return Euler angles from rotation matrix for specified axis sequence.
+
+    axes : One of 24 axis sequences as string or encoded tuple
+
+    Note that many Euler angle triplets can describe one matrix.
+    """
+    try:
+        firstaxis, parity, repetition, frame = constants._AXES2TUPLE[axes.lower(
+        )]
+    except (AttributeError, KeyError):
+        constants._TUPLE2AXES[axes]  # validation
+        firstaxis, parity, repetition, frame = axes
+
+    i = firstaxis
+    j = constants._NEXT_AXIS[i + parity]
+    k = constants._NEXT_AXIS[i - parity + 1]
+
+    M = np.array(matrix, dtype=np.float64, copy=False)[:3, :3]
+    if repetition:
+        sy = np.sqrt(M[i, j] * M[i, j] + M[i, k] * M[i, k])
+        if sy > constants._EPS:
+            ax = np.arctan2(M[i, j],  M[i, k])
+            ay = np.arctan2(sy,	   M[i, i])
+            az = np.arctan2(M[j, i], -M[k, i])
+        else:
+            ax = np.arctan2(-M[j, k],  M[j, j])
+            ay = np.arctan2(sy,	   M[i, i])
+            az = 0.0
+    else:
+        cy = np.sqrt(M[i, i] * M[i, i] + M[j, i] * M[j, i])
+        if cy > constants._EPS:
+            ax = np.arctan2(M[k, j],  M[k, k])
+            ay = np.arctan2(-M[k, i],  cy)
+            az = np.arctan2(M[j, i],  M[i, i])
+        else:
+            ax = np.arctan2(-M[j, k],  M[j, j])
+            ay = np.arctan2(-M[k, i],  cy)
+            az = 0.0
+
+    if parity:
+        ax, ay, az = -ax, -ay, -az
+    if frame:
+        ax, az = az, ax
+    return ax, ay, az
+
+
+def from_quaternion(quaternion, axes='sxyz'):
+    """Return Euler angles from quaternion for specified axis sequence.
+    """
+    return from_matrix(quat.matrix(quaternion), axes)
diff --git a/navipy/maths/homogeneous_transformations.py b/navipy/maths/homogeneous_transformations.py
new file mode 100644
index 0000000..759ad99
--- /dev/null
+++ b/navipy/maths/homogeneous_transformations.py
@@ -0,0 +1,424 @@
+"""
+"""
+import numpy as np
+import navipy.maths.constants as constants
+import navipy.maths.euler as euler
+from navipy.maths.tools import vector_norm
+from navipy.maths.tools import unit_vector
+
+
+def translation_matrix(direction):
+    """
+    Return matrix to translate by direction vector
+    """
+    # get 4x4 identity matrix
+    tmatrix = np.identity(4)
+    # set the first three rows of the last colum to be the direction vector
+    tmatrix[:3, 3] = direction[:3]
+    return tmatrix
+
+
+def translation_from_matrix(matrix):
+    """
+    Return translation vector from translation matrix
+    """
+    # returns the first three rows of the last colum in the rotation matrix
+    return np.array(matrix, copy=False)[:3, 3].copy()
+
+
+def reflection_matrix(point, normal):
+    """
+    Return matrix to mirror at plane defined by point and normal vector
+    """
+    # use only the first three columns of the normal vector
+    normal = unit_vector(normal[:3])
+    # get 4x4 identity matrix
+    M = np.identity(4)
+    # substract two times the outer product of the normal vector from the \
+    # upper left 3x3 matrix of the unit matrix to
+    M[:3, :3] -= 2.0 * np.outer(normal, normal)
+    # set the fourth column of the resutlting matrix to 2* the dot product of \
+    # the point and the normal vector times the noraml vector
+    M[:3, 3] = (2.0 * np.dot(point[:3], normal)) * normal
+    return M
+
+
+def reflection_from_matrix(matrix):
+    """Return mirror plane point and normal vector from reflection matrix.
+    """
+    # pointer to matrix (same as input)
+    M = np.array(matrix, dtype=np.float64, copy=False)
+    # normal: unit eigenvector corresponding to eigenvalue -1
+    w, V = np.linalg.eig(M[:3, :3])
+    # get eigenvectors with eigenvalue close to length 1 -> unit eigenvector; \
+    # only for the upper left 3x3 part
+    i = np.where(abs(np.real(w) + 1.0) < 1e-8)[0]
+    if not len(i):
+        raise ValueError("no unit eigenvector corresponding to eigenvalue -1")
+    # choose only first of unit length eigenvectors
+    normal = np.real(V[:, i[0]]).squeeze()
+    # point: any unit eigenvector corresponding to eigenvalue 1
+    # find eigenvector with unit length for whole matrix
+    w, V = np.linalg.eig(M)
+    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
+    if not len(i):
+        raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
+    # choose last of the unit length eigenvectors
+    point = np.real(V[:, i[-1]]).squeeze()
+    point /= point[3]
+    return point, normal
+
+
+def rotation_matrix(angle, direction, point=None):
+    """Return matrix to rotate about axis defined by point and direction.
+    """
+    sina = np.sin(angle)
+    cosa = np.cos(angle)
+    direction = unit_vector(direction[:3])
+    # rotation matrix around unit vector
+    R = np.diag([cosa, cosa, cosa])
+    R += np.outer(direction, direction) * (1.0 - cosa)
+    direction *= sina
+    R += np.array([[0.0, -direction[2],  direction[1]],
+                   [direction[2], 0.0,	-direction[0]],
+                   [-direction[1], direction[0],  0.0]])
+    M = np.identity(4)
+    M[:3, :3] = R
+    if point is not None:
+        # rotation not around origin
+        point = np.array(point[:3], dtype=np.float64, copy=False)
+        M[:3, 3] = point - np.dot(R, point)
+    return M
+
+
+def rotation_from_matrix(matrix):
+    """Return rotation angle and axis from rotation matrix.
+    """
+    R = np.array(matrix, dtype=np.float64, copy=False)
+    R33 = R[:3, :3]
+    # direction: unit eigenvector of R33 corresponding to eigenvalue of 1
+    w, W = np.linalg.eig(R33.T)
+    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
+    if not len(i):
+        raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
+    direction = np.real(W[:, i[-1]]).squeeze()
+    # point: unit eigenvector of R33 corresponding to eigenvalue of 1
+    w, Q = np.linalg.eig(R)
+    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
+    if not len(i):
+        raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
+    point = np.real(Q[:, i[-1]]).squeeze()
+    point /= point[3]
+    # rotation angle depending on direction
+    cosa = (np.trace(R33) - 1.0) / 2.0
+    if abs(direction[2]) > 1e-8:
+        sina = (R[1, 0] + (cosa - 1.0) * direction[0]
+                * direction[1]) / direction[2]
+    elif abs(direction[1]) > 1e-8:
+        sina = (R[0, 2] + (cosa - 1.0) * direction[0]
+                * direction[2]) / direction[1]
+    else:
+        sina = (R[2, 1] + (cosa - 1.0) * direction[1]
+                * direction[2]) / direction[0]
+    angle = np.arctan2(sina, cosa)
+    return angle, direction, point
+
+
+def scale_matrix(factor, origin=None, direction=None):
+    """Return matrix to scale by factor around origin in direction.
+    Use factor -1 for point symmetry.
+    """
+    if direction is None:
+        # uniform scaling
+        M = np.diag([factor, factor, factor, 1.0])
+        if origin is not None:
+            M[:3, 3] = origin[:3]
+            M[:3, 3] *= 1.0 - factor
+    else:
+        # nonuniform scaling
+        direction = unit_vector(direction[:3])
+        factor = 1.0 - factor
+        M = np.identity(4)
+        M[:3, :3] -= factor * np.outer(direction, direction)
+        if origin is not None:
+            M[:3, 3] = (factor * np.dot(origin[:3], direction)) * direction
+    return M
+
+
+def scale_from_matrix(matrix):
+    """Return scaling factor, origin and direction from scaling matrix. """
+    M = np.array(matrix, dtype=np.float64, copy=False)
+    M33 = M[:3, :3]
+    factor = np.trace(M33) - 2.0
+    try:
+        # direction: unit eigenvector corresponding to eigenvalue factor
+        w, V = np.linalg.eig(M33)
+        i = np.where(abs(np.real(w) - factor) < 1e-8)[0][0]
+        direction = np.real(V[:, i]).squeeze()
+        direction /= vector_norm(direction)
+    except IndexError:
+        # uniform scaling
+        factor = (factor + 2.0) / 3.0
+        direction = None
+    # origin: any eigenvector corresponding to eigenvalue 1
+    w, V = np.linalg.eig(M)
+    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
+    if not len(i):
+        raise ValueError("no eigenvector corresponding to eigenvalue 1")
+    origin = np.real(V[:, i[-1]]).squeeze()
+    origin /= origin[3]
+    return factor, origin, direction
+
+
+def projection_matrix(point, normal, direction=None,
+                      perspective=None, pseudo=False):
+    """Return matrix to project onto plane defined by point and normal.
+
+    Using either perspective point, projection direction, or none of both.
+
+    If pseudo is True, perspective projections will preserve relative depth
+    such that Perspective = dot(Orthogonal, PseudoPerspective).
+    """
+    M = np.identity(4)
+    point = np.array(point[:3], dtype=np.float64, copy=False)
+    normal = unit_vector(normal[:3])
+    if perspective is not None:
+        # perspective projection
+        perspective = np.array(perspective[:3], dtype=np.float64,
+                               copy=False)
+        M[0, 0] = M[1, 1] = M[2, 2] = np.dot(perspective - point, normal)
+        M[:3, :3] -= np.outer(perspective, normal)
+        if pseudo:
+            # preserve relative depth
+            M[:3, :3] -= np.outer(normal, normal)
+            M[:3, 3] = np.dot(point, normal) * (perspective + normal)
+        else:
+            M[:3, 3] = np.dot(point, normal) * perspective
+        M[3, :3] = -normal
+        M[3, 3] = np.dot(perspective, normal)
+    elif direction is not None:
+        # parallel projection
+        direction = np.array(direction[:3], dtype=np.float64, copy=False)
+        scale = np.dot(direction, normal)
+        M[:3, :3] -= np.outer(direction, normal) / scale
+        M[:3, 3] = direction * (np.dot(point, normal) / scale)
+    else:
+        # orthogonal projection
+        M[:3, :3] -= np.outer(normal, normal)
+        M[:3, 3] = np.dot(point, normal) * normal
+    return M
+
+
+def projection_from_matrix(matrix, pseudo=False):
+    """Return projection plane and perspective point from projection matrix.
+
+    Return values are same as arguments for projection_matrix function:
+    point, normal, direction, perspective, and pseudo.
+    """
+    M = np.array(matrix, dtype=np.float64, copy=False)
+    M33 = M[:3, :3]
+    w, V = np.linalg.eig(M)
+    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
+    if not pseudo and len(i):
+        # point: any eigenvector corresponding to eigenvalue 1
+        point = np.real(V[:, i[-1]]).squeeze()
+        point /= point[3]
+        # direction: unit eigenvector corresponding to eigenvalue 0
+        w, V = np.linalg.eig(M33)
+        i = np.where(abs(np.real(w)) < 1e-8)[0]
+        if not len(i):
+            raise ValueError("no eigenvector corresponding to eigenvalue 0")
+        direction = np.real(V[:, i[0]]).squeeze()
+        direction /= vector_norm(direction)
+        # normal: unit eigenvector of M33.T corresponding to eigenvalue 0
+        w, V = np.linalg.eig(M33.T)
+        i = np.where(abs(np.real(w)) < 1e-8)[0]
+        if len(i):
+            # parallel projection
+            normal = np.real(V[:, i[0]]).squeeze()
+            normal /= vector_norm(normal)
+            return point, normal, direction, None, False
+        else:
+            # orthogonal projection, where normal equals direction vector
+            return point, direction, None, None, False
+    else:
+        # perspective projection
+        i = np.where(abs(np.real(w)) > 1e-8)[0]
+        if not len(i):
+            raise ValueError(
+                "no eigenvector not corresponding to eigenvalue 0")
+        point = np.real(V[:, i[-1]]).squeeze()
+        point /= point[3]
+        normal = - M[3, :3]
+        perspective = M[:3, 3] / np.dot(point[:3], normal)
+        if pseudo:
+            perspective -= normal
+        return point, normal, None, perspective, pseudo
+
+
+def shear_matrix(angle, direction, point, normal):
+    """Return matrix to shear by angle along direction vector on shear plane.
+
+    The shear plane is defined by a point and normal vector. The direction
+    vector must be orthogonal to the plane's normal vector.
+
+    A point P is transformed by the shear matrix into P" such that
+    the vector P-P" is parallel to the direction vector and its extent is
+    given by the angle of P-P'-P", where P' is the orthogonal projection
+    of P onto the shear plane.
+    """
+    normal = unit_vector(normal[:3])
+    direction = unit_vector(direction[:3])
+    if abs(np.dot(normal, direction)) > constants._EPS:
+        raise ValueError("direction and normal vectors are not orthogonal")
+    angle = np.tan(angle)
+    M = np.identity(4)
+    M[:3, :3] += angle * np.outer(direction, normal)
+    M[:3, 3] = -angle * np.dot(point[:3], normal) * direction
+    return M
+
+
+def shear_from_matrix(matrix):
+    """Return shear angle, direction and plane from shear matrix.
+    """
+    M = np.array(matrix, dtype=np.float64, copy=False)
+    M33 = M[:3, :3]
+    # normal: cross independent eigenvectors corresponding to the eigenvalue 1
+    w, V = np.linalg.eig(M33)
+    i = np.where(abs(np.real(w) - 1.0) < 1e-4)[0]
+    if len(i) < 2:
+        raise ValueError("no two linear independent eigenvectors found %s" % w)
+    V = np.real(V[:, i]).squeeze().T
+    lenorm = -1.0
+    for i0, i1 in ((0, 1), (0, 2), (1, 2)):
+        n = np.cross(V[i0], V[i1])
+        w = vector_norm(n)
+        if w > lenorm:
+            lenorm = w
+            normal = n
+    normal /= lenorm
+    # direction and angle
+    direction = np.dot(M33 - np.identity(3), normal)
+    angle = vector_norm(direction)
+    direction /= angle
+    angle = np.arctan(angle)
+    # point: eigenvector corresponding to eigenvalue 1
+    w, V = np.linalg.eig(M)
+    i = np.where(abs(np.real(w) - 1.0) < 1e-8)[0]
+    if not len(i):
+        raise ValueError("no eigenvector corresponding to eigenvalue 1")
+    point = np.real(V[:, i[-1]]).squeeze()
+    point /= point[3]
+    return angle, direction, point, normal
+
+
+def decompose_matrix(matrix, axes='sxyz'):
+    """Return sequence oftransformations from transformation matrix.
+
+    matrix : array_like
+            Non-degenerative homogeneous transformation matrix
+
+    Return tuple of:
+            scale : vector of 3 scaling factors
+            shear : list of shear factors for x-y, x-z, y-z axes
+            angles : list of Euler angles about static x, y, z axes
+            translate : translation vector along x, y, z axes
+            perspective : perspective partition of matrix
+
+    Raise ValueError if matrix is of wrong type or degenerative.
+    """
+    M = np.array(matrix, dtype=np.float64, copy=True).T
+    if abs(M[3, 3]) < constants._EPS:
+        raise ValueError("M[3, 3] is zero")
+    M /= M[3, 3]
+    P = M.copy()
+    P[:, 3] = 0.0, 0.0, 0.0, 1.0
+    if not np.linalg.det(P):
+        raise ValueError("matrix is singular")
+
+    scale = np.zeros((3, ))
+    shear = [0.0, 0.0, 0.0]
+    angles = [0.0, 0.0, 0.0]
+
+    if any(abs(M[:3, 3]) > constants._EPS):
+        perspective = np.dot(M[:, 3], np.linalg.inv(P.T))
+        M[:, 3] = 0.0, 0.0, 0.0, 1.0
+    else:
+        perspective = np.array([0.0, 0.0, 0.0, 1.0])
+
+    translate = M[3, :3].copy()
+    M[3, :3] = 0.0
+
+    row = M[:3, :3].copy()
+    scale[0] = vector_norm(row[0])
+    row[0] /= scale[0]
+    shear[0] = np.dot(row[0], row[1])
+    row[1] -= row[0] * shear[0]
+    scale[1] = vector_norm(row[1])
+    row[1] /= scale[1]
+    shear[0] /= scale[1]
+    shear[1] = np.dot(row[0], row[2])
+    row[2] -= row[0] * shear[1]
+    shear[2] = np.dot(row[1], row[2])
+    row[2] -= row[1] * shear[2]
+    scale[2] = vector_norm(row[2])
+    row[2] /= scale[2]
+    shear[1:] /= scale[2]
+
+    if np.dot(row[0], np.cross(row[1], row[2])) < 0:
+        np.negative(scale, scale)
+        np.negative(row, row)
+
+    angles = euler.from_matrix(np.linalg.inv(row[:3, :3]), axes)
+    return scale, shear, angles, translate, perspective
+
+
+def compose_matrix(scale=None, shear=None, angles=None, translate=None,
+                   perspective=None, axes='sxyz'):
+    """Return transformation matrix from sequence oftransformations.
+
+    This is the inverse of the decompose_matrix function.
+
+    Sequence of transformations:
+            scale : vector of 3 scaling factors
+            shear : list of shear factors for x-y, x-z, y-z axes
+            angles : list of Euler angles about static x, y, z axes
+            translate : translation vector along x, y, z axes
+            perspective : perspective partition of matrix
+    """
+    M = np.identity(4)
+    if perspective is not None:
+        P = np.identity(4)
+        P[3, :] = perspective[:4]
+        M = np.dot(M, P)
+    if translate is not None:
+        T = np.identity(4)
+        T[:3, 3] = translate[:3]
+        M = np.dot(M, T)
+    if angles is not None:
+        R = euler.matrix(angles[0], angles[1], angles[2], axes)
+        M = np.dot(M, R)
+    if shear is not None:
+        Z = np.identity(4)
+        Z[1, 2] = shear[2]
+        Z[0, 2] = shear[1]
+        Z[0, 1] = shear[0]
+        M = np.dot(M, Z)
+    if scale is not None:
+        S = np.identity(4)
+        S[0, 0] = scale[0]
+        S[1, 1] = scale[1]
+        S[2, 2] = scale[2]
+        M = np.dot(M, S)
+    M /= M[3, 3]
+    return M
+
+
+def is_same_transform(matrix0, matrix1):
+    """Return True if two matrices perform same transformation.
+    """
+    matrix0 = np.array(matrix0, dtype=np.float64, copy=True)
+    matrix0 /= matrix0[3, 3]
+    matrix1 = np.array(matrix1, dtype=np.float64, copy=True)
+    matrix1 /= matrix1[3, 3]
+    return np.allclose(matrix0, matrix1)
diff --git a/navipy/maths/quaternion.py b/navipy/maths/quaternion.py
new file mode 100644
index 0000000..698788b
--- /dev/null
+++ b/navipy/maths/quaternion.py
@@ -0,0 +1,189 @@
+"""
+
+"""
+import numpy as np
+import navipy.maths.constants as constants
+from navipy.maths.tools import vector_norm
+
+
+def from_euler(ai, aj, ak, axes='sxyz'):
+    """Return quaternion from Euler angles and axis sequence.
+
+    ai, aj, ak : Euler's roll, pitch and yaw angles
+    axes : One of 24 axis sequences as string or encoded tuple
+    """
+    try:
+        firstaxis, parity, repetition, frame = \
+            constants._AXES2TUPLE[axes.lower(
+            )]
+    except (AttributeError, KeyError):
+        constants._TUPLE2AXES[axes]  # validation
+        firstaxis, parity, repetition, frame = axes
+
+    i = firstaxis + 1
+    j = constants._NEXT_AXIS[i + parity - 1] + 1
+    k = constants._NEXT_AXIS[i - parity] + 1
+
+    if frame:
+        ai, ak = ak, ai
+    if parity:
+        aj = -aj
+
+    ai /= 2.0
+    aj /= 2.0
+    ak /= 2.0
+    ci = np.cos(ai)
+    si = np.sin(ai)
+    cj = np.cos(aj)
+    sj = np.sin(aj)
+    ck = np.cos(ak)
+    sk = np.sin(ak)
+    cc = ci * ck
+    cs = ci * sk
+    sc = si * ck
+    ss = si * sk
+
+    q = np.empty((4, ))
+    if repetition:
+        q[0] = cj * (cc - ss)
+        q[i] = cj * (cs + sc)
+        q[j] = sj * (cc + ss)
+        q[k] = sj * (cs - sc)
+    else:
+        q[0] = cj * cc + sj * ss
+        q[i] = cj * sc - sj * cs
+        q[j] = cj * ss + sj * cc
+        q[k] = cj * cs - sj * sc
+    if parity:
+        q[j] *= -1.0
+
+    return q
+
+
+def about_axis(angle, axis):
+    """Return quaternion for rotation about axis.
+    """
+    q = np.array([0.0, axis[0], axis[1], axis[2]])
+    qlen = vector_norm(q)
+    if qlen > constants._EPS:
+        q *= np.sin(angle / 2.0) / qlen
+    q[0] = np.cos(angle / 2.0)
+    return q
+
+
+def matrix(quaternion):
+    """Return homogeneous rotation matrix from quaternion.
+    """
+    q = np.array(quaternion, dtype=np.float64, copy=True)
+    n = np.dot(q, q)
+    if n < constants._EPS:
+        return np.identity(4)
+    q *= np.sqrt(2.0 / n)
+    q = np.outer(q, q)
+    return np.array([
+        [1.0 - q[2, 2] - q[3, 3],	 q[1, 2] - q[3, 0],	 q[1, 3] + q[2, 0], 0.0],
+        [	q[1, 2] + q[3, 0], 1.0 - q[1, 1] - q[3, 3],	 q[2, 3] - q[1, 0], 0.0],
+        [	q[1, 3] - q[2, 0],	 q[2, 3] + q[1, 0], 1.0 - q[1, 1] - q[2, 2], 0.0],
+        [				0.0,				 0.0,				 0.0, 1.0]])
+
+
+def from_matrix(matrix, isprecise=False):
+    """Return quaternion from rotation matrix.
+    If isprecise is True, the input matrix is assumed to be a precise rotation
+    matrix and a faster algorithm is used.
+    """
+    M = np.array(matrix, dtype=np.float64, copy=False)[:4, :4]
+    if isprecise:
+        q = np.empty((4, ))
+        t = np.trace(M)
+        if t > M[3, 3]:
+            q[0] = t
+            q[3] = M[1, 0] - M[0, 1]
+            q[2] = M[0, 2] - M[2, 0]
+            q[1] = M[2, 1] - M[1, 2]
+        else:
+            i, j, k = 1, 2, 3
+            if M[1, 1] > M[0, 0]:
+                i, j, k = 2, 3, 1
+            if M[2, 2] > M[i, i]:
+                i, j, k = 3, 1, 2
+            t = M[i, i] - (M[j, j] + M[k, k]) + M[3, 3]
+            q[i] = t
+            q[j] = M[i, j] + M[j, i]
+            q[k] = M[k, i] + M[i, k]
+            q[3] = M[k, j] - M[j, k]
+        q *= 0.5 / np.sqrt(t * M[3, 3])
+    else:
+        m00 = M[0, 0]
+        m01 = M[0, 1]
+        m02 = M[0, 2]
+        m10 = M[1, 0]
+        m11 = M[1, 1]
+        m12 = M[1, 2]
+        m20 = M[2, 0]
+        m21 = M[2, 1]
+        m22 = M[2, 2]
+        # symmetric matrix K
+        K = np.array([[m00 - m11 - m22, 0.0,		 0.0,		 0.0],
+                      [m01 + m10,	 m11 - m00 - m22, 0.0,		 0.0],
+                      [m02 + m20,	 m12 + m21,	 m22 - m00 - m11, 0.0],
+                      [m21 - m12,	 m02 - m20,	 m10 - m01,	 m00 + m11 + m22]])
+        K /= 3.0
+        # quaternion is eigenvector of K that corresponds to largest eigenvalue
+        w, V = np.linalg.eigh(K)
+        q = V[[3, 0, 1, 2], np.argmax(w)]
+    if q[0] < 0.0:
+        np.negative(q, q)
+    return q
+
+
+def multiply(quaternion1, quaternion0):
+    """Return multiplication of two quaternions.
+    """
+    w0, x0, y0, z0 = quaternion0
+    w1, x1, y1, z1 = quaternion1
+    return np.array([-x1 * x0 - y1 * y0 - z1 * z0 + w1 * w0,
+                     x1 * w0 + y1 * z0 - z1 * y0 + w1 * x0,
+                     -x1 * z0 + y1 * w0 + z1 * x0 + w1 * y0,
+                     x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0], dtype=np.float64)
+
+
+def conjugate(quaternion):
+    """Return conjugate of quaternion.
+    """
+    q = np.array(quaternion, dtype=np.float64, copy=True)
+    np.negative(q[1:], q[1:])
+    return q
+
+
+def inverse(quaternion):
+    """Return inverse of quaternion.
+    """
+    q = np.array(quaternion, dtype=np.float64, copy=True)
+    np.negative(q[1:], q[1:])
+    return q / np.dot(q, q)
+
+
+def real(quaternion):
+    """Return real part of quaternion.
+    """
+    return float(quaternion[0])
+
+
+def imag(quaternion):
+    """Return imaginary part of quaternion.
+    """
+    return np.array(quaternion[1:4], dtype=np.float64, copy=True)
+
+
+def diff(quaternion0, quaternion1):
+    """ The axis and angle between two quaternions
+    """
+    q = multiply(quaternion1, conjugate(quaternion0))
+    length = np.sum(np.sqrt(q[1:4]*q[1:4]))
+    angle = 2*np.arctan2(length, q[0])
+    if np.isclose(length, 0):
+        axis = np.array([1, 0, 0])
+    else:
+        axis = q[1:4]/length
+    return axis, angle
diff --git a/navipy/maths/random.py b/navipy/maths/random.py
new file mode 100644
index 0000000..096018d
--- /dev/null
+++ b/navipy/maths/random.py
@@ -0,0 +1,49 @@
+import numpy as np
+import navipy.maths.quaternion as quat
+import navipy.maths.constants as constants
+from navipy.maths.tools import unit_vector
+
+
+def orthogonal_vectors():
+    """ Generate two random vector, which are orthogonal
+    """
+    orthogonal = False
+    while not orthogonal:
+        vector_0 = np.random.random(3) - 0.5
+        vector_1 = np.random.random(3) - 0.5
+        normal = np.cross(vector_0, vector_1)
+        vector_0_unit = unit_vector(vector_0)
+        normal_unit = unit_vector(normal)
+        orthogonal = abs(np.dot(normal_unit, vector_0_unit)
+                         ) <= constants._EPS
+    return vector_0, normal
+
+
+def rotation_matrix(rand=None):
+    """Return uniform random rotation matrix.
+
+    rand: array like
+            Three independent random variables that are uniformly distributed
+            between 0 and 1 for each returned quaternion.
+    """
+    return quat.matrix(quaternion(rand))
+
+
+def quaternion(rand=None):
+    """Return uniform random unit quaternion.
+
+    rand: array like or None
+            Three independent random variables that are uniformly distributed
+            between 0 and 1.
+    """
+    if rand is None:
+        rand = np.random.rand(3)
+    else:
+        assert len(rand) == 3
+    r1 = np.sqrt(1.0 - rand[0])
+    r2 = np.sqrt(rand[0])
+    pi2 = np.pi * 2.0
+    t1 = pi2 * rand[1]
+    t2 = pi2 * rand[2]
+    return np.array([np.cos(t2) * r2, np.sin(t1) * r1,
+                        np.cos(t1) * r1, np.sin(t2) * r2])
diff --git a/navipy/maths/test_euler.py b/navipy/maths/test_euler.py
new file mode 100644
index 0000000..c1c9533
--- /dev/null
+++ b/navipy/maths/test_euler.py
@@ -0,0 +1,28 @@
+import unittest
+import numpy as np
+import navipy.maths.random as random
+import navipy.maths.euler as euler
+from navipy.maths.tools import vector_norm
+import navipy.maths.constants as constants
+
+
+class TestEuler(unittest.TestCase):
+    def test_from_matrix(self):
+        condition = dict()
+        for key, _ in constants._AXES2TUPLE.items():
+            rotation_0 = euler.matrix(1, 2, 3, key)
+            angles = euler.from_matrix(rotation_0, key)
+            rotation_1 = euler.matrix(*angles, key)
+            condition[key] = np.allclose(rotation_0,
+                                         rotation_1)
+            if condition[key] is False:
+                print('axes', key, 'failed')
+        self.assertTrue(np.all(np.array(condition.values())))
+
+    def test_from_quaternion(self):
+        angles = euler.from_quaternion([0.99810947, 0.06146124, 0, 0])
+        self.assertTrue(np.allclose(angles, [0.123, 0, 0]))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/navipy/maths/test_homogenous_transformations.py b/navipy/maths/test_homogenous_transformations.py
new file mode 100644
index 0000000..2c45dea
--- /dev/null
+++ b/navipy/maths/test_homogenous_transformations.py
@@ -0,0 +1,274 @@
+import unittest
+import numpy as np
+import navipy.maths.homogeneous_transformations as ht
+import navipy.maths.euler as euler
+import navipy.maths.random as random
+
+
+class TestHT(unittest.TestCase):
+    def test_translation_matrix(self):
+        vector = np.random.random(3)
+        vector -= 0.5
+        vector_test = ht.translation_matrix(vector)[:3, 3]
+        self.assertTrue(np.allclose(vector,
+                                    vector_test))
+
+    def test_translation_from_matrix(self):
+        vector = np.random.random(3)
+        vector -= 0.5
+        matrix = ht.translation_matrix(vector)
+        vector_test = ht.translation_from_matrix(matrix)
+        self.assertTrue(np.allclose(vector,
+                                    vector_test))
+
+    def test_reflection_matrix(self):
+        vector_0 = np.random.random(4)
+        vector_0 -= 0.5
+        vector_0[3] = 1.
+        vector_1 = np.random.random(3)
+        vector_1 -= 0.5
+        reflection = ht.reflection_matrix(vector_0,
+                                          vector_1)
+        vector_2 = vector_0.copy()
+        vector_2[:3] += vector_1
+        vector_3 = vector_0.copy()
+        vector_3[:3] -= vector_1
+        condition_0 = np.allclose(2, np.trace(reflection))
+        condition_1 = np.allclose(vector_0,
+                                  np.dot(reflection, vector_0))
+        condition_2 = np.allclose(vector_2, np.dot(reflection,
+                                                   vector_3))
+        condition = condition_0 & condition_1 & condition_2
+        self.assertTrue(condition)
+
+    def test_reflection_from_matrix(self):
+        vector_0 = np.random.random(3) - 0.5
+        vector_1 = np.random.random(3) - 0.5
+        matrix_0 = ht.reflection_matrix(vector_0, vector_1)
+        point, normal = ht.reflection_from_matrix(matrix_0)
+        matrix_1 = ht.reflection_matrix(point, normal)
+        condition = ht.is_same_transform(matrix_0, matrix_1)
+        self.assertTrue(condition)
+
+    def test_rotation_matrix_example(self):
+        rotation = ht.rotation_matrix(np.pi / 2,
+                                      [0, 0, 1],
+                                      [1, 0, 0])
+        condition = np.allclose(np.dot(rotation,
+                                       [0, 0, 0, 1]),
+                                [1, -1, 0, 1])
+        self.assertTrue(condition)
+
+    def test_rotation_matrix_periodicity(self):
+        angle = (np.random.random() - 0.5) * (2 * np.pi)
+        direc = np.random.random(3) - 0.5
+        point = np.random.random(3) - 0.5
+        rotation_0 = ht.rotation_matrix(angle, direc, point)
+        rotation_1 = ht.rotation_matrix(angle - 2 * np.pi, direc, point)
+        condition = ht.is_same_transform(rotation_0, rotation_1)
+        self.assertTrue(condition)
+
+    def test_rotation_matrix_opposite(self):
+        angle = (np.random.random() - 0.5) * (2 * np.pi)
+        direc = np.random.random(3) - 0.5
+        point = np.random.random(3) - 0.5
+        rotation_0 = ht.rotation_matrix(angle, direc, point)
+        rotation_1 = ht.rotation_matrix(-angle, -direc, point)
+        condition = ht.is_same_transform(rotation_0, rotation_1)
+        self.assertTrue(condition)
+
+    def test_rotation_matrix_identity(self):
+        direc = np.random.random(3) - 0.5
+        identity = np.identity(4)
+        condition = np.allclose(identity,
+                                ht.rotation_matrix(np.pi * 2, direc))
+        self.assertTrue(condition)
+
+    def test_rotation_matrix_trace(self):
+        direc = np.random.random(3) - 0.5
+        point = np.random.random(3) - 0.5
+        condition = np.allclose(2,
+                                np.trace(ht.rotation_matrix(np.pi / 2,
+                                                            direc,
+                                                            point)))
+        self.assertTrue(condition)
+
+    def test_rotation_from_matrix(self):
+        angle = (np.random.random() - 0.5) * (2 * np.pi)
+        direc = np.random.random(3) - 0.5
+        point = np.random.random(3) - 0.5
+        rotation_0 = ht.rotation_matrix(angle, direc, point)
+        angle, direc, point = ht.rotation_from_matrix(rotation_0)
+        rotation_1 = ht.rotation_matrix(angle, direc, point)
+        self.assertTrue(ht.is_same_transform(rotation_0,
+                                             rotation_1))
+
+    def test_scale(self):
+        vector = (np.random.rand(4, 5) - 0.5) * 20
+        vector[3] = 1
+        factor = -1.234
+        scale = ht.scale_matrix(factor)
+        condition = np.allclose(np.dot(scale, vector)[:3],
+                                factor * vector[:3])
+        self.assertTrue(condition)
+
+    def test_scale_sametransform(self):
+        factor = np.random.random() * 10 - 5
+        origin = np.random.random(3) - 0.5
+        scale_0 = ht.scale_matrix(factor, origin)
+        factor, origin, direction = ht.scale_from_matrix(scale_0)
+        scale_1 = ht.scale_matrix(factor, origin, direction)
+        self.assertTrue(ht.is_same_transform(scale_0, scale_1))
+
+    def test_scale_from_matrix(self):
+        factor = np.random.random() * 10 - 5
+        origin = np.random.random(3) - 0.5
+        direct = np.random.random(3) - 0.5
+        scale_0 = ht.scale_matrix(factor, origin, direct)
+        factor, origin, direction = ht.scale_from_matrix(scale_0)
+        scale_1 = ht.scale_matrix(factor, origin, direction)
+        self.assertTrue(ht.is_same_transform(scale_0, scale_1))
+
+    def test_projection_matrix_example(self):
+        projection = ht.projection_matrix([0, 0, 0], [1, 0, 0])
+        self.assertTrue(np.allclose(projection[1:, 1:],
+                                    np.identity(4)[1:, 1:]))
+
+    def test_projection_matrix(self):
+        point = np.random.random(3) - 0.5
+        normal = np.random.random(3) - 0.5
+        direct = np.random.random(3) - 0.5
+        persp = np.random.random(3) - 0.5
+        projection_0 = ht.projection_matrix(point, normal)
+        projection_1 = ht.projection_matrix(point, normal,
+                                            direction=direct)
+        projection_2 = ht.projection_matrix(point, normal,
+                                            perspective=persp)
+        projection_3 = ht.projection_matrix(point, normal,
+                                            perspective=persp,
+                                            pseudo=True)
+        self.assertTrue(ht.is_same_transform(projection_2,
+                                             np.dot(projection_0,
+                                                    projection_3)))
+
+    def test_projection_from_matrix(self):
+        point = np.random.random(3) - 0.5
+        normal = np.random.random(3) - 0.5
+        projection_0 = ht.projection_matrix(point, normal)
+        result = ht.projection_from_matrix(projection_0)
+        projection_1 = ht.projection_matrix(*result)
+        self.assertTrue(ht.is_same_transform(projection_0,
+                                             projection_1))
+
+    def test_projection_from_matrix_direct(self):
+        point = np.random.random(3) - 0.5
+        normal = np.random.random(3) - 0.5
+        direct = np.random.random(3) - 0.5
+        projection_0 = ht.projection_matrix(point, normal, direct)
+        result = ht.projection_from_matrix(projection_0)
+        projection_1 = ht.projection_matrix(*result)
+        self.assertTrue(ht.is_same_transform(projection_0,
+                                             projection_1))
+
+    def test_projection_from_matrix_persp_pseudo_false(self):
+        point = np.random.random(3) - 0.5
+        normal = np.random.random(3) - 0.5
+        persp = np.random.random(3) - 0.5
+        projection_0 = ht.projection_matrix(point, normal, persp,
+                                            pseudo=False)
+        result = ht.projection_from_matrix(projection_0)
+        projection_1 = ht.projection_matrix(*result)
+        self.assertTrue(ht.is_same_transform(projection_0,
+                                             projection_1))
+
+    def test_projection_from_matrix_persp_pseudo_true(self):
+        point = np.random.random(3) - 0.5
+        normal = np.random.random(3) - 0.5
+        direct = np.random.random(3) - 0.5
+        persp = np.random.random(3) - 0.5
+        projection_0 = ht.projection_matrix(point, normal, persp,
+                                            pseudo=True)
+        result = ht.projection_from_matrix(projection_0)
+        projection_1 = ht.projection_matrix(*result)
+        self.assertTrue(ht.is_same_transform(projection_0,
+                                             projection_1))
+
+    def test_shear_matrix(self):
+        angle = (np.random.random() - 0.5) * 4 * np.pi
+        point = np.random.random(3) - 0.5
+        direct, normal = random.orthogonal_vectors()
+        scale = ht.shear_matrix(angle, direct, point, normal)
+        self.assertTrue(np.allclose(1, np.linalg.det(scale)))
+
+    # Not Working due to unorthogonality of direct and normal
+    # from time to time....
+    # def test_shear_from_matrix(self):
+    #    angle = (np.random.random() - 0.5) * 4 * np.pi
+    #    direct, normal = random.orthogonal_vectors()
+    #    point = np.random.random(3) - 0.5
+    #    scale_0 = ht.shear_matrix(angle, direct, point, normal)
+    #    result = ht.shear_from_matrix(scale_0)
+    #    scale_1 = ht.shear_matrix(*result)
+    #    self.assertTrue(ht.is_same_transform(scale_0, scale_1))
+
+    def test_decompose_matrix_translation(self):
+        translation_0 = ht.translation_matrix([1, 2, 3])
+        scale, shear, angles, trans, persp = ht.decompose_matrix(translation_0)
+        translation_1 = ht.translation_matrix(trans)
+        self.assertTrue(np.allclose(translation_0, translation_1))
+
+    def test_decompose_matrix_scale(self):
+        factor = np.random.random()
+        scale = ht.scale_matrix(factor)
+        scale, shear, angles, trans, persp = ht.decompose_matrix(scale)
+        self.assertEqual(scale[0], factor)
+
+    def test_decompose_matrix_rotation(self):
+        rotation_0 = euler.matrix(1, 2, 3)
+        _, _, angles, _, _ = ht.decompose_matrix(rotation_0)
+        rotation_1 = euler.matrix(*angles)
+        self.assertTrue(np.allclose(rotation_0, rotation_1))
+
+    def test_compose_matrix(self):
+        scale = np.random.random(3) - 0.5
+        shear = np.random.random(3) - 0.5
+        angles = (np.random.random(3) - 0.5) * (2 * np.pi)
+        trans = np.random.random(3) - 0.5
+        persp = np.random.random(4) - 0.5
+        matrix_0 = ht.compose_matrix(scale, shear, angles, trans, persp)
+        result = ht.decompose_matrix(matrix_0)
+        matrix_1 = ht.compose_matrix(*result)
+        self.assertTrue(ht.is_same_transform(matrix_0, matrix_1))
+
+    def test_is_same_transform(self):
+        matrix = np.random.rand(4, 4)
+        self.assertTrue(ht.is_same_transform(matrix, matrix))
+
+    def test_is_same_transform_rejection(self):
+        # The determinant of a rotation matrix is one,
+        # Thus we test rejection rotation against scaled identity
+        matrix = 2 * np.identity(4)
+        rotation = random.rotation_matrix()
+        self.assertFalse(ht.is_same_transform(matrix, rotation))
+
+    def test_compose_decompose(self):
+        angles_o = [0.123, -1.234, 2.345]
+        scale_o = np.random.rand(3)
+        translation_o = [1, 2, 3]
+        shear_o = [0, np.tan(angles_o[1]), 0]
+        axes = 'sxyz'
+        matrix = ht.compose_matrix(scale_o, shear_o, angles_o,
+                                   translation_o, axes=axes)
+        scale, shear, angles, trans, persp = ht.decompose_matrix(matrix, axes)
+        np.testing.assert_almost_equal(scale, scale_o)
+        np.testing.assert_almost_equal(shear, shear_o)
+        np.testing.assert_almost_equal(angles, angles_o)
+        np.testing.assert_almost_equal(trans, translation_o)
+
+        matrix_1 = ht.compose_matrix(scale, shear, angles,
+                                     trans, persp, axes)
+        self.assertTrue(ht.is_same_transform(matrix, matrix_1))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/navipy/maths/test_quaternion.py b/navipy/maths/test_quaternion.py
new file mode 100644
index 0000000..55fbe93
--- /dev/null
+++ b/navipy/maths/test_quaternion.py
@@ -0,0 +1,87 @@
+import unittest
+import numpy as np
+import navipy.maths.homogeneous_transformations as ht
+import navipy.maths.quaternion as quat
+import navipy.maths.random as random
+
+
+class TestQuaternions(unittest.TestCase):
+    def test_quaternion_from_euler(self):
+        quaternion = quat.from_euler(1, 2, 3, 'ryxz')
+        self.assertTrue(np.allclose(quaternion,
+                                    [0.435953, 0.310622,
+                                     -0.718287, 0.444435]))
+
+    def test_about_axis(self):
+        quaternion = quat.about_axis(0.123, [1, 0, 0])
+        self.assertTrue(np.allclose(quaternion,
+                                    [0.99810947, 0.06146124, 0, 0]))
+
+    def test_matrix(self):
+        matrix = quat.matrix([0.99810947, 0.06146124, 0, 0])
+        self.assertTrue(np.allclose(matrix,
+                                    ht.rotation_matrix(0.123,
+                                                       [1, 0, 0])))
+
+    def test_matrix_identity(self):
+        matrix = quat.matrix([1, 0, 0, 0])
+        self.assertTrue(np.allclose(matrix, np.identity(4)))
+
+    def test_matrix_diagonal(self):
+        matrix = quat.matrix([0, 1, 0, 0])
+        self.assertTrue(np.allclose(matrix, np.diag([1, -1, -1, 1])))
+
+    def test_from_matrix_identity(self):
+        quaternion = quat.from_matrix(np.identity(4), True)
+        self.assertTrue(np.allclose(quaternion, [1, 0, 0, 0]))
+
+    def test_from_matrix_diagonal(self):
+        quaternion = quat.from_matrix(np.diag([1, -1, -1, 1]))
+        self.assertTrue(np.allclose(quaternion, [0, 1, 0, 0])
+                        or np.allclose(quaternion, [0, -1, 0, 0]))
+
+    def test_from_matrix_rotation(self):
+        rotation = random.rotation_matrix()
+        quaternion = quat.from_matrix(rotation)
+        self.assertTrue(ht.is_same_transform(
+            rotation, quat.matrix(quaternion)))
+
+    # Seem to fail from time to time....
+    # def test_quaternion_multiply(self):
+    #    rotation_0 = random.rotation_matrix()
+    #    quaternion_0 = quat.from_matrix(rotation_0)
+    #    rotation_1 = random.rotation_matrix()
+    #    quaternion_1 = quat.from_matrix(rotation_1)
+    #    quaternion_01 = quat.multiply(quaternion_0,
+    #                                  quaternion_1)
+    #    rotation_01 = rotation_0.dot(rotation_1)
+    #    quaternion_01_fromrot = quat.from_matrix(rotation_01)
+    #    self.assertTrue(np.allclose(quaternion_01,
+    #                                quaternion_01_fromrot))
+
+    def test_inverse(self):
+        quaternion_0 = random.quaternion()
+        quaternion_1 = quat.inverse(quaternion_0)
+        self.assertTrue(np.allclose(
+            quat.multiply(quaternion_0, quaternion_1),
+            [1, 0, 0, 0]))
+
+    def test_quaternion(self):
+        alpha, beta, gamma = 0.123, -1.234, 2.345
+        xaxis, yaxis, zaxis = [1, 0, 0], [0, 1, 0], [0, 0, 1]
+        rotx = ht.rotation_matrix(alpha, xaxis)
+        roty = ht.rotation_matrix(beta, yaxis)
+        rotz = ht.rotation_matrix(gamma, zaxis)
+        rotation = rotx.dot(roty.dot(rotz))
+
+        quatx = quat.about_axis(alpha, xaxis)
+        quaty = quat.about_axis(beta, yaxis)
+        quatz = quat.about_axis(gamma, zaxis)
+        quaternion = quat.multiply(quatx, quaty)
+        quaternion = quat.multiply(quaternion, quatz)
+        rotq = quat.matrix(quaternion)
+        self.assertTrue(ht.is_same_transform(rotation, rotq))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/navipy/maths/test_random_cust.py b/navipy/maths/test_random_cust.py
new file mode 100644
index 0000000..be32b6c
--- /dev/null
+++ b/navipy/maths/test_random_cust.py
@@ -0,0 +1,19 @@
+import unittest
+import numpy as np
+import navipy.maths.random as random
+from navipy.maths.tools import vector_norm
+
+
+class TestRandom(unittest.TestCase):
+    def test__rotation_matrix(self):
+        rotation = random.rotation_matrix()
+        self.assertTrue(np.allclose(np.dot(rotation.T, rotation),
+                                    np.identity(4)))
+
+    def test_random_quaternion(self):
+        quaternion = random.quaternion()
+        self.assertTrue(np.allclose(1, vector_norm(quaternion)))
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/navipy/maths/tools.py b/navipy/maths/tools.py
new file mode 100644
index 0000000..6ba238e
--- /dev/null
+++ b/navipy/maths/tools.py
@@ -0,0 +1,57 @@
+"""
+"""
+import numpy as np
+
+
+def vector_norm(data, axis=0):
+    """Return Euclidean norm of ndarray along axis.
+    """
+    if isinstance(data, list):
+        data = np.array(data)
+    assert isinstance(data, np.ndarray), 'data should be a numpy array'
+
+    return np.sqrt(np.sum(data * data, axis=axis, keepdims=True))
+
+
+def unit_vector(data, axis=0):
+    """Return ndarray normalized by length, i.e. Euclidean norm, along axis.
+    """
+    if isinstance(data, list):
+        data = np.array(data)
+
+    assert isinstance(data, np.ndarray), 'data should be a numpy array'
+    repetitions = np.array(data.shape)
+    repetitions[:] = 1
+    repetitions[axis] = data.shape[axis]
+    norm = vector_norm(data, axis)
+    return data / np.tile(norm, repetitions)
+
+
+def angle_between_vectors(vector_0, vector_1, directed=True, axis=0):
+    """Return angle between vectors.
+
+    If directed is False, the input vectors are interpreted as undirected axes,
+    i.e. the maximum angle is pi/2.
+    """
+    vector_0 = np.array(vector_0, dtype=np.float64, copy=False)
+    vector_1 = np.array(vector_1, dtype=np.float64, copy=False)
+    dot = np.sum(vector_0 * vector_1, axis=axis, keepdims=True)
+    dot /= vector_norm(vector_0, axis=axis) \
+        * vector_norm(vector_1, axis=axis)
+    dot = np.squeeze(dot)
+    return np.arccos(dot if directed else np.fabs(dot))
+
+
+def inverse_matrix(matrix):
+    """Return inverse of square transformation matrix.
+    """
+    return np.linalg.inv(matrix)
+
+
+def concatenate_matrices(*matrices):
+    """Return concatenation of series of transformation matrices.
+    """
+    M = np.identity(4)
+    for i in matrices:
+        M = np.dot(M, i)
+    return M
-- 
GitLab