This function expands and HMM model into a semi HMM model by multiplying the number of states repetition_coutn:
    times and modify the transition matrices accordingly to only allow between min_repetitions: and max_repetitions:
    repetitions.

    :param model: The model to transform in place
    :param repetition_count: The amount of repeated occupations of a state to model
    :param min_repetitions: The minimum amount of repeated occupations of a state to allow
    :param max_repetitions: The maximum amount of repeated occupations of a state to allow

def transform_to_semi_hmm(model, repetition_count=4, min_repetitions=1, max_repetitions=np.inf):
    """This function expands and HMM model into a semi HMM model by multiplying the number of states repetition_coutn:
    times and modify the transition matrices accordingly to only allow between min_repetitions: and max_repetitions:
    repetitions.

    :param model: The model to transform in place
    :param repetition_count: The amount of repeated occupations of a state to model
    :param min_repetitions: The minimum amount of repeated occupations of a state to allow
    :param max_repetitions: The maximum amount of repeated occupations of a state to allow
    """

    def transform_transition(definition):
        """Transform in place the definition of the transition element.
        :param definition: The transition element
        """
        dim = definition['dim']
        matrix = definition['matrix']

        new_dim = (definition['dim'] - 2) * repetition_count + 2

        new_matrix = np.zeros((new_dim, new_dim))

        new_matrix[0, 1] = matrix[0, 1]

        def state_repeat_to_ind(sta, rep):
            """Return the index corresponding to the state sta: and repetition rep:.
            States and repetitions start at 0 (state 0 corresponds to the initial dummy state).

            :param sta: The state for which to get the index.
            :param rep: The repetition for which to get the index
            :return: The index in the transition matrix
            """
            if sta == 0:
                return 0

            return ((sta - 1) * repetition_count + rep) + 1

        def probability(source_state, source_repeat, target_state, target_repeat):
            """Return the computed probability of the transition,
            given the source (state source_state:, repeat source_repeat:)
            and the target (state target_state:, repeat target_repeat:).
            Currently this simply keeps the probabilistic model unchanged.

            :param source_state: The source state
            :param source_repeat: The source repetition
            :param target_state: The target state
            :param target_repeat: The target repetition
            """
            return matrix[source_state, target_state]

        for state in range(1, dim-1):
            for repeat in range(min_repetitions-1, min(max_repetitions, repetition_count)):
                source = state_repeat_to_ind(state, repeat)

                # Case in which we advance to the next state
                target = state_repeat_to_ind(state+1, 0)
                prob = probability(state, repeat, state+1, 0)
                new_matrix[source, target] = prob

                if repeat == repetition_count - 1:
                    if np.isinf(max_repetitions):
                        # Case in which we repeat the last state that we model (self-transition)
                        target = source
                        prob = probability(state, repeat, state, repeat)
                        new_matrix[source, target] = prob

                else:
                    # Case in which repeat the state again
                    target = state_repeat_to_ind(state, repeat+1)
                    prob = probability(state, repeat, state, repeat+1)
                    new_matrix[source, target] = prob

        new_matrix[:-1, :] /= new_matrix.sum(axis=1)[:-1, np.newaxis]

        definition['dim'] = new_dim
        definition['matrix'] = new_matrix
        return definition

    # Go through all hmm models
    transition_references = set()
    for hmm in model['hmms']:
        new_statecount = (hmm['definition']['state_count'] - 2) * repetition_count + 2
        hmm['definition']['state_count'] = new_statecount

        # Expand states mantaining the expanded states tied
        new_states = []
        index = None
        for state in sorted(hmm['definition']['states'], key=lambda x: x['index']):
            index = index or state['index']
            for repetition in range(repetition_count):
                new_state = copy.copy(state)
                new_state['index'] = index
                new_states.append(new_state)
                index += 1

        hmm['definition']['states'] = new_states

        # Transform the transition matrices
        if isinstance(hmm['definition']['transition'], collections.Mapping):
            # Definition: transform now
            transform_transition(hmm['definition']['transition']['definition'])
        else:
            # Reference: collect it and transform them all later
            transition_references.add(hmm['definition']['transition'])

    for reference in transition_references:
        definitions = filter(lambda x: x['transition'] and x['transition']['name'] == reference, model['macros'])

        if not len(definitions) == 1:
            logging.warning('Invalid number of definitions ({}) for transition macro {}.'
                            '  Expecting 1 definition.'.format(len(definitions), reference))

        for definition in definitions:
            transform_transition(definition['transition']['definition'])