Makes the command list for a move sequence.

    Constructs the list of commands to execute the given sequence of
    motion. Program/command line commands or profile commands can be
    generated depending on the value of `program_or_profile` so that the
    commands can be used to construct a program or profile later. Types
    of motion supported (see Notes for how to specify) are moves from
    one position to another (the motion will always come to a stop
    before doing the next motion), waiting a given interval of time till
    starting the next move, and looping over a sequence of moves.

    Parameters
    ----------
    cycles : iterable of dicts
        The iterable of cycles of motion to do one after another. See
        Notes for format.
    program_or_profile : {'program', 'profile'}, optional
        Whether program or profile motion commands should be used.
        Anything other than these two values implies the default.
    unit_converter : UnitConverter, optional
        ``GeminiMotorDrive.utilities.UnitConverter`` to use to convert
        the units in `cycles` to motor units. ``None`` indicates that
        they are already in motor units.

    Returns
    -------
    commands : list of str
        ``list`` of ``str`` commands making up the move sequence.

    Notes
    -----
    `cycles` is an iterable of individual cycles of motion. Each cycle
    is a ``dict`` that represents a sequence of moves that could
    possibly be looped over. The field ``'iterations'`` gives how many
    times the sequence of moves should be done (a value > 1 implies a
    loop). Then the field ``'moves'`` is an iterable of the individual
    moves. Each individual move is a ``dict`` with the acceleration
    (``'A'``), deceleration (``'AD'`` with 0 meaning the value of the
    acceleration is used), velocity (``'V'``), and the distance/position
    (``'D'``). Back in the cycle, the field ``'wait_times'`` is an
    iterable of numbers giving the time in seconds to wait after each
    move before going onto the next.

    See Also
    --------
    get_sequence_time
    convert_sequence_to_motor_units
    GeminiMotorDrive.utilities.UnitConverter

    Examples
    --------

    Simple program style two motions with a pause in between.

    >>> from GeminiMotorDrive.compilers.move_sequence import *
    >>> cycles = [{'iterations':1, 'wait_times':[1, 0],
    ...           'moves':[{'A':100, 'AD':0, 'D':-1000, 'V':100},
    ...           {'A':90, 'AD':0, 'D':-1000, 'V':100}]}]
    >>> compile_sequence(cycles)
    ['A100',
     'AD0',
     'V100',
     'D-1000',
     'GO1',
     'WAIT(AS.1=b0)',
     'T1',
     'A90',
     'GO1',
     'WAIT(AS.1=b0)']

    The same motion but in profile style commands

    >>> from GeminiMotorDrive.compilers.move_sequence import *
    >>> cycles = [{'iterations':1, 'wait_times':[1, 0],
    ...           'moves':[{'A':100, 'AD':0, 'D':-1000, 'V':100},
    ...           {'A':90, 'AD':0, 'D':-1000, 'V':100}]}]
    >>> compile_sequence(cycles, program_or_profile='profile')
    ['A100',
     'AD100',
     'V100',
     'D-1000',
     'VF0',
     'GOBUF1',
     'GOWHEN(T=1000)',
     'A90',
     'AD90',
     'VF0',
     'GOBUF1']

    Another motion with a back and forth loop (100 iterations) in the
    middle, done in program style commands.

    >>> from GeminiMotorDrive.compilers.move_sequence import *
    >>> cycles = [{'iterations':1, 'wait_times':[1],
    ...           'moves':[{'A':100, 'AD':0, 'D':-1000, 'V':100}]},
    ...           {'iterations':100, 'wait_times':[0, 0],
    ...           'moves':[{'A':50, 'AD':40, 'D':-1000, 'V':30},
    ...           {'A':50, 'AD':40, 'D':1000, 'V':30}]},
    ...           {'iterations':1, 'wait_times':[0],
    ...           'moves':[{'A':100, 'AD':0, 'D':1000, 'V':100}]}]
    >>> compile_sequence(cycles)
    ['A100',
     'AD0',
     'V100',
     'D-1000',
     'GO1',
     'WAIT(AS.1=b0)',
     'T1',
     'L100',
     'A50',
     'AD40',
     'V30',
     'D-1000',
     'GO1',
     'WAIT(AS.1=b0)',
     'D~',
     'GO1',
     'WAIT(AS.1=b0)',
     'LN',
     'A100',
     'AD0',
     'V100',
     'GO1',
     'WAIT(AS.1=b0)']

def compile_sequence(cycles, program_or_profile='program',
                     unit_converter=None):
    """ Makes the command list for a move sequence.

    Constructs the list of commands to execute the given sequence of
    motion. Program/command line commands or profile commands can be
    generated depending on the value of `program_or_profile` so that the
    commands can be used to construct a program or profile later. Types
    of motion supported (see Notes for how to specify) are moves from
    one position to another (the motion will always come to a stop
    before doing the next motion), waiting a given interval of time till
    starting the next move, and looping over a sequence of moves.

    Parameters
    ----------
    cycles : iterable of dicts
        The iterable of cycles of motion to do one after another. See
        Notes for format.
    program_or_profile : {'program', 'profile'}, optional
        Whether program or profile motion commands should be used.
        Anything other than these two values implies the default.
    unit_converter : UnitConverter, optional
        ``GeminiMotorDrive.utilities.UnitConverter`` to use to convert
        the units in `cycles` to motor units. ``None`` indicates that
        they are already in motor units.

    Returns
    -------
    commands : list of str
        ``list`` of ``str`` commands making up the move sequence.

    Notes
    -----
    `cycles` is an iterable of individual cycles of motion. Each cycle
    is a ``dict`` that represents a sequence of moves that could
    possibly be looped over. The field ``'iterations'`` gives how many
    times the sequence of moves should be done (a value > 1 implies a
    loop). Then the field ``'moves'`` is an iterable of the individual
    moves. Each individual move is a ``dict`` with the acceleration
    (``'A'``), deceleration (``'AD'`` with 0 meaning the value of the
    acceleration is used), velocity (``'V'``), and the distance/position
    (``'D'``). Back in the cycle, the field ``'wait_times'`` is an
    iterable of numbers giving the time in seconds to wait after each
    move before going onto the next.

    See Also
    --------
    get_sequence_time
    convert_sequence_to_motor_units
    GeminiMotorDrive.utilities.UnitConverter

    Examples
    --------

    Simple program style two motions with a pause in between.

    >>> from GeminiMotorDrive.compilers.move_sequence import *
    >>> cycles = [{'iterations':1, 'wait_times':[1, 0],
    ...           'moves':[{'A':100, 'AD':0, 'D':-1000, 'V':100},
    ...           {'A':90, 'AD':0, 'D':-1000, 'V':100}]}]
    >>> compile_sequence(cycles)
    ['A100',
     'AD0',
     'V100',
     'D-1000',
     'GO1',
     'WAIT(AS.1=b0)',
     'T1',
     'A90',
     'GO1',
     'WAIT(AS.1=b0)']

    The same motion but in profile style commands

    >>> from GeminiMotorDrive.compilers.move_sequence import *
    >>> cycles = [{'iterations':1, 'wait_times':[1, 0],
    ...           'moves':[{'A':100, 'AD':0, 'D':-1000, 'V':100},
    ...           {'A':90, 'AD':0, 'D':-1000, 'V':100}]}]
    >>> compile_sequence(cycles, program_or_profile='profile')
    ['A100',
     'AD100',
     'V100',
     'D-1000',
     'VF0',
     'GOBUF1',
     'GOWHEN(T=1000)',
     'A90',
     'AD90',
     'VF0',
     'GOBUF1']

    Another motion with a back and forth loop (100 iterations) in the
    middle, done in program style commands.

    >>> from GeminiMotorDrive.compilers.move_sequence import *
    >>> cycles = [{'iterations':1, 'wait_times':[1],
    ...           'moves':[{'A':100, 'AD':0, 'D':-1000, 'V':100}]},
    ...           {'iterations':100, 'wait_times':[0, 0],
    ...           'moves':[{'A':50, 'AD':40, 'D':-1000, 'V':30},
    ...           {'A':50, 'AD':40, 'D':1000, 'V':30}]},
    ...           {'iterations':1, 'wait_times':[0],
    ...           'moves':[{'A':100, 'AD':0, 'D':1000, 'V':100}]}]
    >>> compile_sequence(cycles)
    ['A100',
     'AD0',
     'V100',
     'D-1000',
     'GO1',
     'WAIT(AS.1=b0)',
     'T1',
     'L100',
     'A50',
     'AD40',
     'V30',
     'D-1000',
     'GO1',
     'WAIT(AS.1=b0)',
     'D~',
     'GO1',
     'WAIT(AS.1=b0)',
     'LN',
     'A100',
     'AD0',
     'V100',
     'GO1',
     'WAIT(AS.1=b0)']

    """
    # If needed, cycles needs to be converted to motor units.
    if unit_converter is None:
        cv_cycles = cycles
    else:
        cv_cycles = convert_sequence_to_motor_units(cycles, \
            unit_converter=unit_converter)

    # Initially, we have no commands in our command list.
    commands = []

    # The A, AD, D, and V parameters of the previous motion should be
    # kept track of because if they don't change from one motion to the
    # next, the commands to set them don't need to be included. They
    # will be started blank since there are no previous motions yet.
    previous_motion = {'A': [], 'AD': [], 'D': [], 'V': []}

    # Construct each cycle one by one.
    for cycle in cv_cycles:
        # If more than one iteration is being done, a loop needs to be
        # setup. It will be either 'L' or 'PLOOP' with the number of
        # iterations attached if it is a program or a profile
        # respectively. Since it will be tough to keep track of what
        # motion changed from the end of a loop to the beginning of it,
        # it is easier to just forget all previous motion values and set
        # them all at the beginning of the loop (clear previous_motion).
        iterations = int(cycle['iterations'])
        if iterations > 1:
            previous_motion = {'A': [], 'AD': [], 'D': [], 'V': []}
            if program_or_profile != 'profile':
                commands.append('L' + str(iterations))
            else:
                commands.append('PLOOP' + str(iterations))

        # Construct each individual move in the cycle.
        for i in range(0, len(cycle['moves'])):
            # Grab the motion indicated by the current move.
            new_motion = cycle['moves'][i]

            # If we are doing a profile, AD must be set explicitly
            # to A if it is 0.
            if program_or_profile == 'profile' \
                    and new_motion['AD'] == 0.0:
                new_motion['AD'] = new_motion['A']

            # Set A, AD, and V if they have changed.
            for k in ('A', 'AD', 'V'):
                if previous_motion[k] != new_motion[k]:
                    # Grab it and round it to 4 places after the decimal
                    # point because that is the most that is
                    # supported. Then, if it is an integer value,
                    # convert it to an integer because that is what the
                    # drive will send back if requested (makes
                    # comparisons easier). Then add the command.
                    val = round(float(new_motion[k]), 4)
                    if val == int(val):
                        val = int(val)
                    commands.append(k + str(val))

            # If the sign of D has flipped, we just need to issue a 'D~'
            # command. If the value has changed in another way, it needs
            # to be reset.
            if previous_motion['D'] != new_motion['D']:
                if previous_motion['D'] == -new_motion['D']:
                    commands.append('D~')
                else:
                    commands.append('D'
                                    + str(int(new_motion['D'])))

            # Grab the amount of time that should be waited after the
            # move is done.
            wait_time = cycle['wait_times'][i]

            # Give the motion command (GO or GOBUF), tell the drive to
            # wait till the motor has stopped (a WAIT command if it is a
            # program and a VF0 command if it is a profile), and make it
            # wait the period of time wait_time (T and GOWHEN commands).
            if program_or_profile != 'profile':
                commands.append('GO1')
                commands.append('WAIT(AS.1=b0)')
                if wait_time != 0:
                    # The wait time needs to be rounded to 3 places
                    # after the decimal. If it is an integer, it should
                    # be converted to an int so that the drive will send
                    # back what we send (makes compairisons easier).
                    wait_time = round(float(wait_time), 3)
                    if wait_time == int(wait_time):
                        wait_time = int(wait_time)
                    commands.append('T' + str(wait_time))
            else:
                commands.append('VF0')
                commands.append('GOBUF1')
                if wait_time != 0:
                    commands.append('GOWHEN(T='
                                    + str(int(1000*wait_time))
                                    + ')')

            # Before going onto the next move, previous_motion needs to
            # be set to the one just done.
            previous_motion = new_motion

        # Done with all the moves of the cycle. If we are looping, the
        # loop end needs to be put in.
        if iterations > 1:
            if program_or_profile != 'profile':
                commands.append('LN')
            else:
                commands.append('PLN')

    # Done constructing the command list.
    return commands