def score_transition(song_chunk, new_frame, smooth=True, cache=None):
    num_frames = utils.song_length(song_chunk)
    prev_frames = ["|".join([str(song_chunk[0][i]), str(song_chunk[1][i]), str(song_chunk[2][i])]) for i in range(0, num_frames)]

    denominator_obs = ".".join(prev_frames)
    numerator_obs = denominator_obs + "." + "|".join([str(n) for n in new_frame])

    if cache is not None:
        if numerator_obs in cache:
            numerator_count = cache[numerator_obs]
        else:
            numerator_count = count_for_obs(numerator_obs) or 0
            numerator_count += 1 if smooth else 0
            cache[numerator_obs] = numerator_count

        if denominator_obs in cache:
            denominator_count = cache[denominator_obs]
        else:
            denominator_count = count_for_obs(denominator_obs) or 0
            denominator_count += num_possible_prev_states if smooth else 0
            cache[denominator_obs] = denominator_count
    else:
        numerator_count = count_for_obs(numerator_obs) or 0
        numerator_count += 1 if smooth else 0
        denominator_count = count_for_obs(denominator_obs) or 0
        denominator_count += num_possible_prev_states if smooth else 0

    return None if numerator_count == 0 and not smooth else math.log(float(numerator_count) / denominator_count, 10)
def predict_song(song_start, scoring_function, frame_filter=None, length=100, order=1, variability=0):
    from copy import copy
    from random import randint

    song = copy(song_start)
    init_song_len = utils.song_length(song)
    possible_frames = [[int(note) for note in frame.split("|")] for frame in hmm.all_observations()]

    cache = dict()

    for i in range(0, length - init_song_len):

        print "SONG: %s" % (song)

        if i == 0:
            current_frame_obs = [[channel[i]] for channel in song]
        else:
            obs_to_pull = min(utils.song_length(song), order - 1)
            current_frame_obs = [channel[-1 * obs_to_pull:] for channel in song]

        scores = {}

        prev_frame = [channel[-1] for channel in song]

        if frame_filter is not None:
            filtered_frames = [frame for frame in possible_frames if frame_filter(prev_frame, frame)]
            # if we filtered everything out, easy back and let the full set back in
            if len(filtered_frames) > 0:
                local_possible_frames = filtered_frames
            else:
                local_possible_frames = possible_frames
        else:
            local_possible_frames = possible_frames

        for a_frame in local_possible_frames:
            score = scoring_function(current_frame_obs, a_frame, smooth=False, cache=cache)
            scores["|".join([str(note) for note in a_frame])] = score

        sorted_scores = sorted([(value, key) for (key, value) in scores.items() if value is not None], reverse=True)

        print current_frame_obs
        print sorted_scores[0:10]

        # If we don't have any valid transitions from this state, we just have
        # to guess a new one at random (not great!)  We just use semi popular
        # observations, to limit the damage
        if len(sorted_scores) == 0:
            print "WARNING, Random Transition!"
            random_frames = all_observations(cutoff=200)
            selected_observation = (0, random_frames[randint(0, len(random_frames) - 1)])
        elif variability == 0 or len(sorted_scores) == 1:
            selected_observation = sorted_scores[0]
        else:
            selected_observation = sorted_scores[randint(0, min(variability - 1, len(sorted_scores) - 1))]

        print selected_observation

        frame_parts = [int(part) for part in selected_observation[1].split("|")]

        for i in range(0, 3):
            song[i].append(frame_parts[i])
        print "Generated note {0}/{1}".format(utils.song_length(song), length)
    print song
    return song