"""
Rotations contains all rotation like MoveGenerator classes.

.. inheritance-diagram:: fullrmc.Generators.Rotations
    :parts: 1


+-----------------------------------------------------+-----------------------------------------------------+
|.. figure:: randomRotation.png                       |.. figure:: randomRotationAboutAxis.png              |
|   :width: 375px                                     |   :width: 375px                                     |
|   :height: 300px                                    |   :height: 300px                                    |
|   :align: left                                      |   :align: left                                      |
|                                                     |                                                     |
|   Random rotation axis and angle generated and      |   Random rotation generated about a pre-defined axis|
|   applied on a Tetrahydrofuran molecule. Solid      |   or one of the symmetry axes of the Tetrahydrofuran|
|   colours are of the origin molecule position while |   molecule. Solid colours are of the origin molecule|
|   fading ones are of the rotated molecule.          |   position while fading ones are of the rotated     |
|   (:class:`RotationGenerator`)                      |   molecule.                                         |
|                                                     |   (:class:`RotationAboutAxisGenerator`              |
|                                                     |   :class:`RotationAboutSymmetryAxisGenerator`)      |
+-----------------------------------------------------+-----------------------------------------------------+
|.. figure:: orientationGenerator.png                 |                                                     |
|   :width: 375px                                     |                                                     |
|   :height: 300px                                    |                                                     |
|   :align: left                                      |                                                     |
|                                                     |                                                     |
|   Random orientation of hexane molecule to [1,1,1]  |                                                     |
|   axis with maximumOffsetAngle of 10 degrees is     |                                                     |
|   generated. First principal axis of hexane molecule|                                                     |
|   is considered as groupAxis. Solid colors are of   |                                                     |
|   original molecule while fading ones are of the    |                                                     |
|   oriented one. (:class:`OrientationGenerator`)     |                                                     |
|                                                     |                                                     |
+-----------------------------------------------------+-----------------------------------------------------+

.. raw:: html

    <iframe width="560" height="315"
    src="https://www.youtube.com/embed/-clLvYiaC8w?rel=0"
    frameborder="0" allowfullscreen>
    </iframe>

"""
# standard libraries imports
from __future__ import print_function
import re

# external libraries imports
import numpy as np

# fullrmc imports
from ..Globals import INT_TYPE, FLOAT_TYPE, PI, PRECISION, LOGGER
from ..Globals import str, long, unicode, bytes, basestring, range, xrange, maxint
from ..Core.Collection import is_number, is_integer, get_path, generate_random_float, get_principal_axis, get_rotation_matrix, orient, generate_vectors_in_solid_angle, generate_random_vector
from ..Core.MoveGenerator import MoveGenerator, PathGenerator


class RotationGenerator(MoveGenerator):
    """
    Generate random rotational moves upon groups of atoms.
    Only groups of more than one atom are accepted.

    :Parameters:
        #. group (None, Group): The group instance.
        #. amplitude (number): The maximum rotation angle allowed in degrees.
           It must be strictly bigger than 0 and strictly smaller than 360.

    .. code-block:: python

        # import fullrmc modules
        from fullrmc.Engine import Engine
        from fullrmc.Generators.Rotations import RotationGenerator

        # create engine
        ENGINE = Engine(path='my_engine.rmc')

        # set pdb file
        ENGINE.set_pdb('system.pdb')

        # Add constraints ...
        # Re-define groups if needed ...
        # Re-define groups selector if needed ...

        # set moves generators to random rotations.
        # Maximum rotation amplitude is set to 5 degrees to all defined groups
        for g in ENGINE.groups:
            if len(g) >1:
                g.set_move_generator( RotationGenerator(amplitude=5) )

    """
    def __init__(self, group=None, amplitude=2):
        super(RotationGenerator, self).__init__(group=group)
        # set amplitude
        self.set_amplitude(amplitude)

    def _codify__(self, name='generator', group=None, addDependencies=True):
        assert isinstance(name, basestring), LOGGER.error("name must be a string")
        assert re.match('[a-zA-Z_][a-zA-Z0-9_]*$', name) is not None, LOGGER.error("given name '%s' can't be used as a variable name"%name)
        dependencies = 'from fullrmc.Generators import Rotations'
        code         = []
        if addDependencies:
            code.append(dependencies)
        code.append("{name} = Rotations.RotationGenerator\
(group={group}, amplitude={amplitude})".format(name=name, group=group, amplitude=self.amplitude*FLOAT_TYPE(180)/PI))
        # return
        return [dependencies], '\n'.join(code)

    @property
    def amplitude(self):
        """ Maximum allowed angle of rotation in rad."""
        return self.__amplitude

    def set_amplitude(self, amplitude):
        """
        Set maximum rotation angle in degrees and transforms it to rad.

        :Parameters:
            #. amplitude (number): the maximum allowed rotation angle in
               degrees. It must be strictly bigger than 0 and strictly smaller
               than 360.
        """
        assert is_number(amplitude), LOGGER.error("rotation amplitude must be a number")
        amplitude = float(amplitude)
        assert amplitude>0, LOGGER.error("rotation amplitude must be bigger than 0 deg.")
        assert amplitude<360, LOGGER.error("rotation amplitude must be smaller than 360 deg.")
        # convert to radian and store amplitude
        self.__amplitude = FLOAT_TYPE(PI*amplitude/180.)

    def check_group(self, group):
        """
        Check the generator's group.

        :Parameters:
            #. group (Group): the Group instance.
        """
        if len(group.indexes)<=1:
            return False, "At least two atoms needed in a group to perform rotation."
        else:
            return True, ""

    def transform_coordinates(self, coordinates, argument=None):
        """
        Rotate coordinates.

        :Parameters:
            #. coordinates (np.ndarray): The coordinates on which to apply
               the rotation.
            #. argument (object): Any python object. Not used in this
               generator.

        :Returns:
            #. coordinates (np.ndarray): The new coordinates after applying
               the rotation.
        """
        if coordinates.shape[0]<=1:
            # atoms where removed, fall back to random translation
            return coordinates+generate_random_vector(minAmp=self.__amplitude[0],
                                                      maxAmp=self.__amplitude[1])
        else:
            # get rotation axis
            n = 0
            while n<PRECISION:
                rotationAxis = 1-2*np.random.random(3)
                n = np.linalg.norm(rotationAxis)
            rotationAxis /= n
            # get rotation angle
            rotationAngle = (1-2*generate_random_float())*self.amplitude
            # get rotation matrix
            rotationMatrix = get_rotation_matrix(rotationAxis, rotationAngle)
            # get atoms group center
            center = np.sum(coordinates, 0)/coordinates.shape[0]
            # translate to origin
            rotatedCoordinates = coordinates-center
            # rotate
            for idx in range(rotatedCoordinates.shape[0]):
                rotatedCoordinates[idx,:] = np.dot( rotationMatrix, rotatedCoordinates[idx,:])
            # translate back to center and return rotated coordinates
            return np.array(rotatedCoordinates+center, dtype=FLOAT_TYPE)


class RotationAboutAxisGenerator(RotationGenerator):
    """
    Generates random rotational moves upon groups of atoms about a
    pre-defined axis.

    :Parameters:
        #. group (None, Group): The group instance.
        #. amplitude (number): The maximum allowed rotation angle in degrees.
           It must be strictly bigger than 0 and strictly smaller than 360.
        #. axis (list,set,tuple,numpy.ndarray): The rotational axis vector.

    .. code-block:: python

        # import fullrmc modules
        from fullrmc.Engine import Engine
        from fullrmc.Generators.Rotations import RotationAboutAxisGenerator

        # create engine
        ENGINE = Engine(path='my_engine.rmc')

        # set pdb file
        ENGINE.set_pdb('system.pdb')

        # Add constraints ...
        # Re-define groups if needed ...
        # Re-define groups selector if needed ...

        # set moves generators to random rotations about (1,1,1) a pre-defined axis.
        # Maximum rotation amplitude is set to 5 degrees to all defined groups
        for g in ENGINE.groups:
            if len(g) >1:
                g.set_move_generator( RotationAboutAxisGenerator(amplitude=5, axis=(1,1,1)) )

    """

    def __init__(self, group=None, amplitude=2, axis=(1,0,0)):
        super(RotationAboutAxisGenerator, self).__init__(group=group, amplitude=amplitude)
        # set amplitude
        self.set_axis(axis)

    def _codify__(self, name='generator', group=None, addDependencies=True):
        assert isinstance(name, basestring), LOGGER.error("name must be a string")
        assert re.match('[a-zA-Z_][a-zA-Z0-9_]*$', name) is not None, LOGGER.error("given name '%s' can't be used as a variable name"%name)
        dependencies = 'from fullrmc.Generators import Rotations'
        code         = []
        if addDependencies:
            code.append(dependencies)
        code.append("{name} = Rotations.RotationAboutAxisGenerator\
(group={group}, amplitude={amplitude}, axis={axis})"\
.format(name=name, group=group, amplitude=self.amplitude*FLOAT_TYPE(180)/PI, axis=list(self.axis)))
        # return
        return [dependencies], '\n'.join(code)

    @property
    def axis(self):
        """ Rotation axis vector."""
        return self.__axis

    def check_group(self, group):
        """
        Check the generator's group.

        :Parameters:
            #. group (Group): The Group instance.
        """
        return True, ""

    def set_axis(self, axis):
        """
        Set the axis along which the rotation will be performed.

        :Parameters:
            #. axis (list,set,tuple,numpy.ndarray): The rotation axis vector.
        """
        assert isinstance(axis, (list,set,tuple,np.ndarray)), LOGGER.error("axis must be a list")
        axis = list(axis)
        assert len(axis)==3, LOGGER.error("axis list must have 3 items")
        for pos in axis:
            assert is_number(pos), LOGGER.error("axis items must be numbers")
        axis = [FLOAT_TYPE(pos) for pos in axis]
        axis =  np.array(axis, dtype=FLOAT_TYPE)
        self.__axis = axis/FLOAT_TYPE( np.linalg.norm(axis) )

    def transform_coordinates(self, coordinates, argument=None):
        """
        Rotate coordinates.

        :Parameters:
            #. coordinates (np.ndarray): The coordinates on which to apply
               the rotation.
            #. argument (object): Not used here.

        :Returns:
            #. coordinates (np.ndarray): The new coordinates after
               applying the rotation.
        """
        # get rotation angle
        rotationAngle  = (1-2*generate_random_float())*self.amplitude
        # get rotation matrix
        rotationMatrix = get_rotation_matrix(self.__axis, rotationAngle)
        # get atoms group center and rotation axis
        center,_,_,_,_,_,_ = get_principal_axis(coordinates)
        # translate to origin
        rotatedCoordinates = coordinates-center
        # rotate
        for idx in range(rotatedCoordinates.shape[0]):
            rotatedCoordinates[idx,:] = np.dot( rotationMatrix, rotatedCoordinates[idx,:])
        # translate back to center and return rotated coordinates
        return np.array(rotatedCoordinates+center, dtype=FLOAT_TYPE)


class RotationAboutSymmetryAxisGenerator(RotationGenerator):
    """
    Generates random rotational moves upon groups of atoms about one of
    their symmetry axis. Only groups of more than 1 atom are accepted.

    :Parameters:
        #. group (None, fullrmc.Engine): The constraint fullrmc engine.
        #. amplitude (number): Maximum rotation angle in degrees.
           It must be strictly bigger than 0 and strictly smaller than 360.
        #. axis (integer): Must be 0,1 or 2 for respectively the main,
           secondary or tertiary symmetry axis

    .. code-block:: python

        # import fullrmc modules
        from fullrmc.Engine import Engine
        from fullrmc.Generators.Rotations import RotationAboutSymmetryAxisGenerator

        # create engine
        ENGINE = Engine(path='my_engine.rmc')

        # set pdb file
        ENGINE.set_pdb('system.pdb')

        # Add constraints ...
        # Re-define groups if needed ...
        # Re-define groups selector if needed ...

        # set moves generators to random rotations about the second symmetry axis of each group.
        # Maximum rotation amplitude is set to 5 degrees to all defined groups
        for g in ENGINE.groups:
            if len(g) >1:
                g.set_move_generator( RotationAboutSymmetryAxisGenerator(amplitude=5, axis=1) )

    """
    def __init__(self, group=None, amplitude=2, axis=0):
        super(RotationAboutSymmetryAxisGenerator, self).__init__(group=group, amplitude=amplitude)
        # set amplitude
        self.set_axis(axis)

    def _codify__(self, name='generator', group=None, addDependencies=True):
        assert isinstance(name, basestring), LOGGER.error("name must be a string")
        assert re.match('[a-zA-Z_][a-zA-Z0-9_]*$', name) is not None, LOGGER.error("given name '%s' can't be used as a variable name"%name)
        dependencies = 'from fullrmc.Generators import Rotations'
        code         = []
        if addDependencies:
            code.append(dependencies)
        code.append("{name} = Rotations.RotationAboutSymmetryAxisGenerator\
(group={group}, amplitude={amplitude}, axis={axis})"\
.format(name=name, group=group, amplitude=self.amplitude*FLOAT_TYPE(180)/PI, axis=self.axis))
        # return
        return [dependencies], '\n'.join(code)

    @property
    def axis(self):
        """ Rotation axis index."""
        return self.__axis

    def set_axis(self, axis):
        """
        Set the symmetry axis index to rotate about.

        :Parameters:
            #. axis (integer): Must be 0,1 or 2 for respectively the main,
               secondary or tertiary symmetry axis.
        """
        assert is_integer(axis), LOGGER.error("rotation symmetry axis must be an integer")
        axis = INT_TYPE(axis)
        assert axis>=0, LOGGER.error("rotation symmetry axis must be positive.")
        assert axis<=2, LOGGER.error("rotation symmetry axis must be smaller or equal to 2")
        # convert to radian and store amplitude
        self.__axis = axis

    def transform_coordinates(self, coordinates, argument=None):
        """
        Rotate coordinates.

        :Parameters:
            #. coordinates (np.ndarray): The coordinates on which to apply
               the rotation.
            #. argument (object): Not used here.

        :Returns:
            #. coordinates (np.ndarray): The new coordinates after
               applying the rotation.
        """
        if coordinates.shape[0]<=1:
            # atoms where removed, fall back to random translation
            return coordinates+generate_random_vector(minAmp=self.__amplitude[0],
                                                      maxAmp=self.__amplitude[1])
        else:
            # get rotation angle
            rotationAngle = (1-2*generate_random_float())*self.amplitude
            # get atoms group center and rotation axis
            center,_,_,_,X,Y,Z =get_principal_axis(coordinates)
            rotationAxis = [X,Y,Z][self.__axis]
            # get rotation matrix
            rotationMatrix = get_rotation_matrix(rotationAxis, rotationAngle)
            # translate to origin
            rotatedCoordinates = coordinates-center
            # rotate
            for idx in range(rotatedCoordinates.shape[0]):
                rotatedCoordinates[idx,:] = np.dot( rotationMatrix, rotatedCoordinates[idx,:])
            # translate back to center and return rotated coordinates
            return np.array(rotatedCoordinates+center, dtype=FLOAT_TYPE)


class RotationAboutSymmetryAxisPath(PathGenerator):
    """
    Generate rotational moves upon groups of atoms about one of their
    symmetry axis. Only groups of more than one atom are accepted.

    :Parameters:
        #. group (None, fullrmc.Engine): The constraint fullrmc engine.
        #. axis (integer): Must be 0,1 or 2 for respectively the main,
           secondary or tertiary symmetry axis.
        #. path (List): list of angles.
        #. randomize (boolean): Whether to pull moves randomly from path or
           pull moves in order at every step.

    .. code-block:: python

        # import fullrmc modules
        from fullrmc.Engine import Engine
        from fullrmc.Generators.Rotations import RotationAboutSymmetryAxisPath

        # create engine
        ENGINE = Engine(path='my_engine.rmc')

        # set pdb file
        ENGINE.set_pdb('system.pdb')

        # Add constraints ...
        # Re-define groups if needed ...
        # Re-define groups selector if needed ...

        # set moves generators to pre-defined rotations about the second symmetry axis of each group.
        angles = [-0.1, -0.5, -0.05, 0.5, 0.01, 2, 3, 1, -3]
        for g in ENGINE.groups:
            if len(g) >1:
                g.set_move_generator( RotationAboutSymmetryAxisPath(axis=1, path=angles) )

    """
    def __init__(self, group=None,  axis=0, path=None, randomize=False):
        # initialize PathGenerator
        PathGenerator.__init__(self, group=group, path=path, randomize=randomize)
        # set axis
        self.set_axis(axis)

    def _codify__(self, name='generator', group=None, addDependencies=True):
        assert isinstance(name, basestring), LOGGER.error("name must be a string")
        assert re.match('[a-zA-Z_][a-zA-Z0-9_]*$', name) is not None, LOGGER.error("given name '%s' can't be used as a variable name"%name)
        dependencies = 'from fullrmc.Generators import Rotations'
        code         = []
        if addDependencies:
            code.append(dependencies)
        code.append("{name} = Rotations.RotationAboutSymmetryAxisPath\
(group={group}, axis={axis}, path={path}, randomize={randomize})"\
.format(name=name, group=group, axis=self.axis,
        path=[p*FLOAT_TYPE(180)/PI for p in self.path], randomize=self.randomize))
        # return
        return [dependencies], '\n'.join(code)

    @property
    def axis(self):
        """ Rotation axis index."""
        return self.__axis

    def set_axis(self, axis):
        """
        Set the symmetry axis index to rotate about.

        :Parameters:
            #. axis (integer): Must be 0,1 or 2 for respectively the main,
               secondary or tertiary symmetry axis
        """
        assert is_integer(axis), LOGGER.error("rotation symmetry axis must be an integer")
        axis = INT_TYPE(axis)
        assert axis>=0, LOGGER.error("rotation symmetry axis must be positive.")
        assert axis<=2,LOGGER.error("rotation symmetry axis must be smaller or equal to 2")
        # convert to radian and store amplitude
        self.__axis = axis

    def check_path(self, path):
        """
        Check the generator's path.

        :Parameters:
            #. path (None, list): The list of moves.
        """
        if not isinstance(path, (list, set, tuple, np.ndarray)):
            return False, "path must be a list"
        path = list(path)
        if not len(path):
            return False, "path can't be empty"
        for angle in path:
            if not is_number(angle):
                return False, "path items must be numbers"
        return True, ""

    def normalize_path(self, path):
        """
        Transform all path angles to radian.

        :Parameters:
            #. path (list): The list of moves in degrees.

        :Returns:
            #. path (list): The list of moves in rad.
        """
        path = [FLOAT_TYPE(angle)*PI/FLOAT_TYPE(180.) for angle in path]
        return list(path)

    def check_group(self, group):
        """
        Check the generator's group.

        :Parameters:
            #. group (Group): The Group instance.
        """
        if len(group.indexes)<=1:
            return False, "At least two atoms needed in a group to perform rotation."
        else:
            return True, ""

    def transform_coordinates(self, coordinates, argument):
        """
        Rotate coordinates.

        :Parameters:
            #. coordinates (np.ndarray): The coordinates on which to apply
               the rotation.
            #. argument (object): The rotation angle.

        :Returns:
            #. coordinates (np.ndarray): The new coordinates after applying
               the rotation.
        """
        if coordinates.shape[0]<=1:
            # atoms where removed, fall back to random translation
            return coordinates+generate_random_vector(minAmp=self.__amplitude[0],
                                                      maxAmp=self.__amplitude[1])
        else:
            # get atoms group center and rotation axis
            center,_,_,_,X,Y,Z =get_principal_axis(coordinates)
            rotationAxis = [X,Y,Z][self.__axis]
            # get rotation matrix
            rotationMatrix = get_rotation_matrix(rotationAxis, argument)
            # translate to origin
            rotatedCoordinates = coordinates-center
            # rotate
            for idx in range(rotatedCoordinates.shape[0]):
                rotatedCoordinates[idx,:] = np.dot( rotationMatrix, rotatedCoordinates[idx,:])
            # translate back to center and return rotated coordinates
            return np.array(rotatedCoordinates+center, dtype=FLOAT_TYPE)



class OrientationGenerator(MoveGenerator):
    """
    Generate rotational moves upon groups of atoms to align and orient
    along an axis. Orientation rotations are computed randomly allowing
    offset angle between grouAxis and orientationAxis Only groups of more
    than 1 atom are accepted.

    :Parameters:
        #. group (None, Group): The group instance.
        #. maximumOffsetAngle (number): The maximum offset angle in degrees
           between groupAxis and orientationAxis.
        #. groupAxis (dict): The group axis. Only one key is allowed.
           If key is 'fixed', value must be a list, tuple or a numpy.array of
           a vector such as [X,Y,Z]. If key is 'symmetry', in this case the
           group axis is computed as one of the three symmetry axis of the
           group atoms. the value must be even 0, 1 or 2 for respectively
           the first, second and tertiary symmetry axis.
        #. orientationAxis (dict): The axis to align the group with.
           If key is 'fixed', value must be a list, tuple or a numpy.array
           of a vector such as [X,Y,Z]. If Key is 'symmetry', in this case
           the value must be a list of two items, the first one is a list
           of atoms indexes to compute symmetry axis and the second item
           must be even 0, 1 or 2 for respectively the first, second and
           tertiary symmetry axis.
        #. flip (None, bool): Whether to allow flipping axis orientation
           or not. If True, orientationAxis will be flipped forcing
           anti-parallel orientation. If False, orientationAxis will not be
           flipped forcing parallel orientation. If None is given, no
           flipping is forced, flipping can be set randomly to True or False
           during run time execution.

    .. code-block:: python

        # import fullrmc modules
        from fullrmc.Engine import Engine
        from fullrmc.Generators.Rotations import OrientationGenerator

        # create engine
        ENGINE = Engine(path='my_engine.rmc')

        # set pdb file
        ENGINE.set_pdb('system.pdb')

        # Add constraints ...
        # Re-define groups if needed ...
        # Re-define groups selector if needed ...

        # set moves generators to orientations of each group third symmetry axis
        # towards the (-1,0,2) the predefined axis within maximum 5 degrees.
        for g in ENGINE.groups:
            if len(g) >1:
                g.set_move_generator( OrientationGenerator(maximumOffsetAngle=5,
                                                           groupAxis={"symmetry":2},
                                                           orientationAxis={"fixed":(-1,0,2)}) )
    """
    def __init__(self, group=None, maximumOffsetAngle=10, groupAxis={"symmetry":0}, orientationAxis={"fixed":(1,0,0)}, flip=None):
        super(OrientationGenerator, self).__init__(group=group)
        # set maximumOffsetAngle
        self.set_maximum_offset_angle(maximumOffsetAngle)
        # set group axis
        self.set_group_axis(groupAxis)
        # set orientation axis
        self.set_orientation_axis(orientationAxis)
        # set flip
        self.set_flip(flip)

    def _codify__(self, name='generator', group=None, addDependencies=True):
        assert isinstance(name, basestring), LOGGER.error("name must be a string")
        assert re.match('[a-zA-Z_][a-zA-Z0-9_]*$', name) is not None, LOGGER.error("given name '%s' can't be used as a variable name"%name)
        dependencies = 'from fullrmc.Generators import Rotations'
        code         = []
        if addDependencies:
            code.append(dependencies)
        groupAxis = list(self.groupAxis)[0]
        if isinstance(self.groupAxis[groupAxis], (list,set,tuple,np.ndarray)):
            groupAxis = {groupAxis:list(self.groupAxis[groupAxis])}
        else:
            groupAxis = {groupAxis:self.groupAxis[groupAxis]}
        orientationAxis = list(self.orientationAxis)[0]
        orientationAxis = {orientationAxis:list(self.orientationAxis[orientationAxis])}
        code.append("{name} = Rotations.OrientationGenerator\
(group={group}, maximumOffsetAngle={maximumOffsetAngle},\
orientationAxis={orientationAxis}, flip={flip})"\
.format(name=name, group=group, maximumOffsetAngle=self.maximumOffsetAngle*FLOAT_TYPE(180)/PI,
        groupAxis=groupAxis, orientationAxis=orientationAxis, flip=self.flip))
        # return
        return [dependencies], '\n'.join(code)

    @property
    def maximumOffsetAngle(self):
        """ Maximum offset angle in degrees between groupAxis and
        orientationAxis in rad."""
        return self.__maximumOffsetAngle

    @property
    def orientationAxis(self):
        """ Orientation axis value or definition."""
        return self.__orientationAxis

    @property
    def groupAxis(self):
        """ Group axis value or definition."""
        return self.__groupAxis

    @property
    def flip(self):
        """ Flip value."""
        return self.__flip

    def set_maximum_offset_angle(self, maximumOffsetAngle):
        """
        Set the maximum offset angle allowed.

        :Parameters:
            #. maximumOffsetAngle (number): The maximum offset angle in
               degrees between groupAxis and orientationAxis.
        """
        assert is_number(maximumOffsetAngle), LOGGER.error("maximumOffsetAngle must be a number")
        maximumOffsetAngle = float(maximumOffsetAngle)
        assert maximumOffsetAngle>0, LOGGER.error("maximumOffsetAngle must be bigger than 0 deg.")
        assert maximumOffsetAngle<180, LOGGER.error("maximumOffsetAngle must be smaller than 180 deg.")
        # convert to radian and store amplitude
        self.__maximumOffsetAngle = FLOAT_TYPE(PI*maximumOffsetAngle/180.)

    def check_group(self, group):
        """
        Check the generator's group.

        :Parameters:
            #. group (Group): the Group instance.
        """
        if len(group.indexes)<=1:
            return False, "At least two atoms needed in a group to perform rotation."
        else:
            return True, ""

    def set_flip(self, flip):
        """
        Set flip flag value.

        :Parameters:
            #. flip (None, bool): Whether to allow flipping axis orientation
               or not. If True, orientationAxis will be flipped forcing
               anti-parallel orientation. If False, orientationAxis will not
               be flipped forcing parallel orientation. If None is given,
               no flipping is forced, flipping can be set randomly to True
               or False during run time execution.
        """
        assert flip in (None, True, False), LOGGER.error("flip can only be None, True or False")
        self.__flip = flip

    def set_group_axis(self, groupAxis):
        """
        Sets group axis value.

        :Parameters:
           #. groupAxis (dict): The group axis. Only one key is allowed.
              If key is fixed, value must be a list, tuple or a numpy.array
              of a vector such as [X,Y,Z]. If key is symmetry, in this case
              the group axis is computed as one of the three symmetry axis of
              the group atoms. the value must be even 0, 1 or 2 for
              respectively the first, second and tertiary symmetry axis.
        """
        assert isinstance(groupAxis, dict), LOGGER.error("groupAxis must be a dictionary")
        assert len(groupAxis) == 1, LOGGER.error("groupAxis must have a single key")
        key = list(groupAxis)[0]
        val = groupAxis[key]
        if key == "fixed":
            self.__mustComputeGroupAxis = False
            assert isinstance(val, (list,set,tuple,np.ndarray)), LOGGER.error("groupAxis value must be a list")
            if isinstance(val, np.ndarray):
                assert len(val.shape) == 1, LOGGER.error("groupAxis value must have a single dimension")
            val = list(val)
            assert len(val)==3, LOGGER.error("groupAxis fixed value must be a vector")
            for v in val:
                assert is_number(v), LOGGER.error("groupAxis value item must be numbers")
            val  = np.array([FLOAT_TYPE(v) for v in val], dtype=FLOAT_TYPE)
            norm = FLOAT_TYPE(np.sqrt(np.sum(val**2)))
            val /= norm
        elif key == "symmetry":
            self.__mustComputeGroupAxis = True
            assert is_integer(val), LOGGER.error("groupAxis symmetry value must be an integer")
            val = INT_TYPE(val)
            assert val>=0 and val<3, LOGGER.error("groupAxis symmetry value must be positive smaller than 3")
        else:
            self.__mustComputeGroupAxis = None
            raise Exception(LOGGER.error("groupAxis key must be either 'fixed' or 'symmetry'"))
        # set groupAxis
        self.__groupAxis = {key:val}

    def set_orientation_axis(self, orientationAxis):
        """
        Set orientation axis value.

        :Parameters:
            #. orientationAxis (dict): The axis to align the group axis with.
               If key is fixed, value must be a list, tuple or a numpy.array
               of a vector such as [X,Y,Z]. If Key is symmetry, in this case
               the value must be a list of two items, the first one is a list
               of atoms indexes to compute symmetry axis and the second item
               must be even 0, 1 or 2 for respectively the first, second and
               tertiary symmetry axis.
        """
        assert isinstance(orientationAxis, dict), LOGGER.error("orientationAxis must be a dictionary")
        assert len(orientationAxis) == 1, LOGGER.error("orientationAxis must have a single key")
        key = list(orientationAxis)[0]
        val = orientationAxis[key]
        if key == "fixed":
            self.__mustComputeOrientationAxis = False
            assert isinstance(val, (list,set,tuple,np.ndarray)), LOGGER.error("orientationAxis value must be a list")
            if isinstance(val, np.ndarray):
                assert len(val.shape) == 1, LOGGER.error("orientationAxis value must have a single dimension")
            val = list(val)
            assert len(val)==3, LOGGER.error("orientationAxis fixed value must be a vector")
            for v in val:
                assert is_number(v), LOGGER.error("orientationAxis value item must be numbers")
            val  = np.array([FLOAT_TYPE(v) for v in val], dtype=FLOAT_TYPE)
            norm = FLOAT_TYPE(np.sqrt(np.sum(val**2)))
            val /= norm
        elif key == "symmetry":
            self.__mustComputeOrientationAxis = True
            assert isinstance(val, (list, tuple)), LOGGER.error("orientationAxis symmetry value must be a list")
            assert len(val) == 2, LOGGER.error("orientationAxis symmetry value must be a list of two items")
            val0 = []
            for v in val[0]:
                assert is_integer(v), LOGGER.error("orientationAxis symmetry value list items must be integers")
                v0 = INT_TYPE(v)
                assert v0>=0, LOGGER.error("orientationAxis symmetry value list items must be positive")
                val0.append(v0)
            assert len(set(val0))==len(val[0]), LOGGER.error("orientationAxis symmetry value list redundant items indexes found")
            val0 = np.array(val0, dtype=INT_TYPE)
            val1 = val[1]
            assert is_integer(val1), LOGGER.error("orientationAxis symmetry value second item must be an integer")
            val1 = INT_TYPE(val1)
            assert val1>=0 and val1<3, LOGGER.error("orientationAxis symmetry value second item must be positive smaller than 3")
            val = (val0,val1)
        else:
            self.__mustComputeOrientationAxis = None
            raise Exception(LOGGER.error("orientationAxis key must be either 'fixed' or 'symmetry'"))
        # set orientationAxis
        self.__orientationAxis = {key:val}

    def __get_orientation_axis__(self):
        if self.__mustComputeOrientationAxis:
            coordinates   = self.group._get_engine().realCoordinates[self.__orientationAxis["symmetry"][0]]
            _,_,_,_,X,Y,Z = get_principal_axis(coordinates)
            axis = [X,Y,Z][self.__orientationAxis["symmetry"][1]]
        else:
            axis  = self.__orientationAxis["fixed"]
        return axis

    def __get_group_axis__(self, coordinates):
        if self.__mustComputeGroupAxis:
            _,_,_,_,X,Y,Z = get_principal_axis(coordinates)
            axis = [X,Y,Z][self.__groupAxis["symmetry"]]
        else:
            axis = self.__groupAxis["fixed"]
        return axis

    def transform_coordinates(self, coordinates, argument=None):
        """
        Rotate coordinates.

        :Parameters:
            #. coordinates (np.ndarray): The coordinates on which to apply
               the rotation.
            #. argument (object): Not used here.

        :Returns:
            #. coordinates (np.ndarray): The new coordinates after applying
               the rotation.
        """
        if coordinates.shape[0]<=1:
            # atoms where removed, fall back to random translation
            return coordinates+generate_random_vector(minAmp=self.__amplitude[0],
                                                      maxAmp=self.__amplitude[1])
        else:
           # create flip flag
            if self.__flip is None:
                flip = FLOAT_TYPE( np.sign(1-2*generate_random_float()) )
            elif self.__flip:
                flip = FLOAT_TYPE(-1)
            else:
                flip = FLOAT_TYPE(1)
            # get group axis
            groupAxis = self.__get_group_axis__(coordinates)
            # get align axis within offset angle
            orientationAxis = flip*self.__get_orientation_axis__()
            orientationAxis = generate_vectors_in_solid_angle(direction=orientationAxis,
                                                              maxAngle=self.__maximumOffsetAngle,
                                                              numberOfVectors=1)[0]
            # get coordinates center
            center = np.array(np.sum(coordinates, 0)/coordinates.shape[0] , dtype=FLOAT_TYPE)
            # translate to origin
            rotatedCoordinates = coordinates-center
            # align coordinates
            rotatedCoordinates = orient(rotatedCoordinates, groupAxis, orientationAxis)
            # translate back to center and return rotated coordinates
            return np.array(rotatedCoordinates+center, dtype=FLOAT_TYPE)