# Metric_Analysis.py
# this program computes the distance between machines M1, M2 accepting regular languages L1, L2 using a variety of metrics.
# first we generate a list of all possible words of a designated length N accepted by either machine. We call these sets of words W1, W2.
# W1 = [[length 1 words in L1], [length 2 words in L1], ....  [length N words in L1]]
# each of the sublists may have one of two formats: (1) an explicit list of words 101001, 011010, ... or a sequence of 2^k 1's or 0's.
# 1 if corresponding word (ordered as binary decimals is accepted) 1 if not.
from numpy import *
from pylab import *
from CA_Functions import *
from Automata_Functions import *
from Epsilon_Machine_Reconstruction import *


# below is sample data to be used in testing functions

# list format 
#L1 = [ [[0],[1]], [[0,1],[1,0],[1,1]], [[0,1,1],[1,0,1],[1,1,0],[1,1,1]], [[0,1,1,1],[1,0,1,1],[1,1,0,1],[1,1,1,0]], [[0,1,1,1,0],[1,0,1,1,1],[1,1,0,1,1],[1,1,1,0,1]]  ]   
#L2 = [ [[0],[1]], [[0,1],[1,0]], [[0,1,0],[1,0,1]], [[0,1,0,1],[1,0,1,0]], [[0,1,0,1,0],[1,0,1,0,1]] ]
#L3 = [ [[0]], [[0,1],[0,0]], [[0,1,0],[0,0,0]], [[0,0,0,1],[0,0,0,0]] ]
#L4 = [ [[1]], [[0,1],[0,0],[1,1]], [[1,1,1]], [[1,0,0,1],[0,0,0,0]] ]

# yes/no format
#L1 = [ [0,1], [0,1,1,0], [0,1,1,1,0,0,0,1], [0,1,1,0,1,0,1,1,0,1,0,1,0,0,0,1], [1,1,1,0,1,0,0,1,0,0,0,1,0,1,1,1,0,1,1,0,1,0,1,1,0,0,1,1,0,0,0,1] ]
#L2 = [ [0,0], [0,0,1,0], [0,1,0,0,1,0,0,1], [0,1,0,0,1,0,1,1,0,1,0,1,0,0,0,1], [0,0,0,0,1,0,0,1,0,1,0,1,0,1,1,1,0,1,1,0,1,0,1,1,0,0,1,1,0,0,0,1] ]
#L3 = [ [0,1], [0,1,1,0], [0,1,1,1,0,0,0,1], [0,1,1,0,1,0,1,1,0,1,0,1,0,0,0,1], [1,1,1,0,1,0,0,1,0,0,0,1,0,1,1,1,0,1,1,0,1,0,1,1,0,0,1,1,0,0,0,1] ]
#L4 = [ [0,1], [0,1,1,0], [0,1,1,1,0,0,0,1], [0,1,1,0,1,0,1,1,0,1,0,1,0,0,0,1], [1,1,1,0,1,0,0,1,0,0,0,1,0,1,1,1,0,1,1,0,1,0,1,1,0,0,1,1,0,0,0,1] ]

# various words
#w1 = [1, 1, 1, 0, 0]
#w2 = [0, 1, 0, 1, 0]

# various strings
#S1 = [1,1,1,0,0,0]
#S2 = [1,0,0,0,1,1]

#S1 = [1,0,1,1,1,1,1,1,0,1,1]
#S2 = [0,1,0,0,0,1,0,1,0]

    

#define compute_probabilities(S,N,WordListsN) computes the probabilities of all words of length 1,2, ... N for a language samples (strings) S.
def compute_probabilities(S,N,WordlistsN):
    probabilities = []

    for n in range(1,N+1):
        
        length_S = len(S) - n # we can only look at positions in string with n symbols to the right
        n_probabilities = []

        for w in WordListsN[n - 1]:
            count = 0
        
            for x in xrange(length_S):
                S_word = S[x:x+n]
                if S_word == w: count = count + 1

            p_w = (count*1.0)/length_S
            n_probabilities.append(p_w)

        probabilities.append(n_probabilities)

    #print 'probabilites =', probabilities
    return probabilities

# define norm1_metric(P1,P2) on two probability distributions for words of length n
def norm1_metric(P1,P2):
    d = 0
    
    for x in xrange(len(P1)):
        d = d + abs(P1[x]-P2[x])

    return d




# define the (normalized) distance between two words of the same length n using 1norm
def dist(w1,w2):
    n = len(w1)
    d = 0
    
    for x in xrange(n):
        delta = mod(w1[x] + w2[x], 2)
        d = d + delta

    d = (d*1.0)/n
    return d

# define construct_word_lists(N)
# this function constucts a list of length n word lists for n = 1,2, ... N.
# Where each length_n_word_list is the list of all possible words of length n over alphabet {0,1}


def construct_word_lists(N):

    # start with length 1 words
    WordListsN = [ [[0],[1]] ]

    # then add new ones based on the last level
    for n in range(1,N):
        n_length_word_list = []
    
        for w in WordListsN[n-1]:
            w0 = w + [0]
            w1 = w + [1]
            n_length_word_list.append(w0)
            n_length_word_list.append(w1)

        WordListsN.append(n_length_word_list)

    return WordListsN
        
            
# define construct_accepted_word_lists(M). Takes a DFA M and constructs the corresponding language L up to words or length N
# these are given in both the yes/no format and list format defined above
def construct_accepted_word_lists(M,N,WordListsN):

    L_YesNo = []
    L_ListForm = []

    for n in xrange(N):
        length_n_words_YesNo = []
        length_n_words_ListForm = []
    
        for w in WordListsN[n]:
            accept = test_word(M,w)
            
            if accept == 1:
                length_n_words_YesNo.append(1)
                length_n_words_ListForm.append(w)

            if accept == 0:
                length_n_words_YesNo.append(0)

        L_YesNo.append(length_n_words_YesNo)
        L_ListForm.append(length_n_words_ListForm)

    return L_YesNo, L_ListForm



# define Hausdorff_metric on languages of the list format for word of length n
def Hausdorff_metric(L1,L2,n):
    #print 'n = ', n

    p_L1_L2 = 0
    for w1 in L1[n-1]:
        d_w1_L2 = 999

        for w2 in L2[n-1]:
            d_w1_w2 = dist(w1,w2)
            if d_w1_w2 < d_w1_L2: d_w1_L2 = d_w1_w2

        if d_w1_L2 > p_L1_L2: p_L1_L2 = d_w1_L2
    #print 'p_L1_l2 =', p_L1_L2

    p_L2_L1 = 0
    for w2 in L2[n-1]:
        d_w2_L1 = 999

        for w1 in L1[n-1]:
            d_w2_w1 = dist(w2,w1)
            if d_w2_w1 < d_w2_L1: d_w2_L1 = d_w2_w1

        if d_w2_L1 > p_L2_L1: p_L2_L1 = d_w2_L1
    #print 'p_L2_L1 =', p_L2_L1

    D_L1_L2 = max(p_L1_L2, p_L2_L1)
    #print 'D_L1_L2 =', D_L1_L2
    return D_L1_L2

# define Average_Min_Distance_metric on languages of the list format for words of length n
def Averaged_Min_Distance_metric(L1,L2,n):
    #print 'n = ', n

    A_L1_L2 = 0
    for w1 in L1[n-1]:
        d_w1_L2 = 999

        for w2 in L2[n-1]:
            d_w1_w2 = dist(w1,w2)
            if d_w1_w2 < d_w1_L2: d_w1_L2 = d_w1_w2

        A_L1_L2 = A_L1_L2 + d_w1_L2

    A_L1_L2 = A_L1_L2/len(L1[n-1]) 
    #print 'A_L1_l2 =', A_L1_L2

    A_L2_L1 = 0
    for w2 in L2[n-1]:
        d_w2_L1 = 999

        for w1 in L1[n-1]:
            d_w2_w1 = dist(w2,w1)
            if d_w2_w1 < d_w2_L1: d_w2_L1 = d_w2_w1

        A_L2_L1 = A_L2_L1 + d_w2_L1

    A_L2_L1 = A_L2_L1/len(L2[n-1])     
    #print 'A_L2_L1 =', A_L2_L1

    D_L1_L2 = max(A_L1_L2, A_L2_L1)
    #print 'D_L1_L2 =', D_L1_L2
    return D_L1_L2
    


# average a given metric for fixed word lengths n over all n to define general metric on the languages
# In the case of Hausdorff or Averaged_Min_Distance mertric L1,L2 are lists of accepted strings (in ListForm as defined above.)
# In case of norm1_metric L1, L2 are not lists of accepted words but rather lists of probability distribution for length n words
def compute_distance(L1,L2,N,metric,alpha): 
    D_flat = 0
    D_alpha = 0
    d_values  = []

    if metric == 1:
        for n in xrange(N):
            d = norm1_metric(L1[n],L2[n])
            d = 0.5*d # to normalize and make distance be between (0,1) like other metrics.
            d_values.append(d)
            D_flat = D_flat + d
            D_alpha = D_alpha + d*(alpha**n)

    if metric == 2:
        for n in xrange(N):
            d = Hausdorff_metric(L1,L2,n+1)
            d_values.append(d)
            D_flat = D_flat + d
            D_alpha = D_alpha + d*(alpha**n)
            

    if metric == 3:
        for n in xrange(N):
            d = Averaged_Min_Distance_metric(L1,L2,n+1)
            d_values.append(d)
            D_flat = D_flat + d
            D_alpha = D_alpha + d*(alpha**n)
           

    D_flat = (D_flat*1.0)/N
    #print 'D_alpha unscaled =', D_alpha
    alpha_scale_factor = 1.0/(1 - alpha)
    D_alpha = D_alpha/alpha_scale_factor
    
    return D_flat, D_alpha, d_values


                

# Main Program to test computation of metrics

###### Code to test metric functions ########

#d = dist(w1,w2)
#print 'd =', d

#N = 3
#WordListsN = construct_word_lists(N)
#print 'WordListsN'
#for ll in WordListsN:
#    print ll
#    print ''

#P1 = compute_probabilities(S1,N,WordListsN)
#P2 = compute_probabilities(S2,N,WordListsN)

#D_flat, D_alpha, d_values = compute_distance(L1,L2,N,2,0.9)
#D_flat,D_alpha, d_values = compute_distance(P1,P2,N,1,0.9)
#print 'D_flat =', D_flat
#print 'D_alpha =', D_alpha
#print 'd_values = ', d_values


#M = [   [1,[0,1]], [1,[2,3]], [1,[1,1]], [0,[3,3]]  ]                                                  # DFA for the rule 18 domain
#M = [   [1,[1,2]], [1,[7,4]], [1,[1,3]], [1,[1,6]], [1,[7,5]], [1,[7,6]], [1,[1,7]], [0,[7,7]]     ]   # ECA 54 Domain {1110...}
#M = [   [1,[2,1]], [1,[4,7]], [1,[3,1]], [1,[6,1]], [1,[5,7]], [1,[6,7]], [1,[7,1]], [0,[7,7]]     ]   # ECA 54 Domain {0001...}

#print ''
#L_YesNo, L_ListForm = construct_accepted_word_lists(M,N,WordListsN)

#print 'L_YesNo'
#for ll in L_YesNo:
#    print ll
#    print ''

#print 'L_ListForm'
#for ll in L_ListForm:
#    print ll
#    print ''

#Process_Graph = [[1,'X'],[2,'X'],[3,'X'],['X',0]]   # ECA 54 0001 domain
#Process_Graph = [['X',1],['X',2],['X',3],[0,'X']]   # ECA 54 1110 domain
#Process_Graph = [[1,'X'],[0,0]]                      # ECA 18 domain
#DFA = convert_process_graph_to_DFA(Process_Graph)
#print 'DFA =', DFA
#equivalent = check_equivalence_of_DFAs(M,DFA) 
#print 'equivalent =', equivalent


###### Code to use Metric Functions #######

machines = []

# ECA 18 domain  0,W,0,W,0,W..  W = wildcard)
machines.append( [   [1,[0,1]], [1,[2,3]], [1,[1,1]], [0,[3,3]]  ]  )

# ECA 54 Domain {1110...}
machines.append( [   [1,[1,2]], [1,[7,4]], [1,[1,3]], [1,[1,6]], [1,[7,5]], [1,[7,6]], [1,[1,7]], [0,[7,7]]     ] )

# ECA 54 Domain {0001...}
machines.append( [   [1,[2,1]], [1,[4,7]], [1,[3,1]], [1,[6,1]], [1,[5,7]], [1,[6,7]], [1,[7,1]], [0,[7,7]]     ] )

# ECA 80 at least two 0's followed by at most one 1's
machines.append( [[1, [0, 1]], [1, [2, 3]], [1, [0, 4]], [1, [2, 4]], [0, [4, 4]]] )

#ECA 160
machines.append( [[1, [0, 1]], [0, [1, 1]]] )

# ECA 25
#machines.append ( [[1, [1, 2]], [1, [3, 4]], [1, [5, 6]], [1, [7, 8]], [1, [9, 10]], [1, [11, 12]], [1, [14, 15]], [1, [16, 8]], [1, [17, 10]], [1, [11, 17]], [1, [17, 15]],
#                   [1, [13, 17]], [1, [9, 17]], [1, [16, 17]], [1, [17, 12]], [1, [14, 17]], [1, [17, 8]], [0, [17, 17]]] )

# ECA 9
#machines.append ( [[1, [1, 2]], [1, [3, 4]], [1, [5, 6]], [1, [7, 8]], [1, [9, 10]], [1, [11, 12]], [1, [13, 18]], [1, [17, 8]], [1, [19, 10]], [1, [11, 19]], [1, [19, 18]],
#                  [1, [15, 19]], [1, [9, 19]], [1, [19, 12]], [1, [19, 8]], [1, [16, 19]], [1, [14, 19]], [1, [14, 8]], [1, [13, 19]], [0, [19, 19]]] )

# domain 1,0,W,1,0,W,1,0,W
#machines.append( [   [1,[1,2]], [1,[3,6]], [1,[4,5]], [1,[7,5]], [1,[3,3]], [1,[4,7]], [1,[4,5]], [0,[7,7]]  ] )

# domain 0,W,1,W,0,W,1,W,0,W,1,W...
#machines.append( [[1, [1, 2]], [1, [3, 4]], [1, [5, 6]], [1, [7, 4]], [1, [8, 6]], [1, [3, 7]], [1, [5, 8]], [1, [11, 10]], [1, [9, 11]], [1, [7, 7]], [1, [8, 8]],  [0, [11, 11]]] )      

# Random-Random-zero process domain 
#machines.append ( [[1, [0, 1]], [1, [2, 3]], [1, [4, 5]], [1, [6, 7]], [1, [1, 1]], [1, [3, 3]], [1, [5, 5]], [0, [7, 7]]] )

# RR-XOR process
machines.append ( [[1, [1, 2]], [1, [3, 4]], [1, [5, 6]], [1, [3, 7]], [1, [8, 6]], [1, [9, 4]], [1, [5, 10]], [1, [11, 12]], [1, [13, 14]], [1, [12, 11]], [1, [14, 13]],               
                   [1, [15, 16]], [1, [17, 18]], [1, [16, 15]], [1, [19, 20]], [0, [15, 15]], [1, [19, 18]], [1, [9, 7]], [1, [11, 13]], [1, [13, 11]], [1, [8, 10]]] )


for x in xrange(len(machines)):
    machines[x] = sort_DFA(machines[x])


N = 10
WordListsN = construct_word_lists(N)
num_data_points = 10000
alpha = 0.9

#L_YesNo, L_ListForm = construct_accepted_word_lists(machines[0],N,WordListsN)
#for n in xrange(N):
#    print ''
#    print 'n =', n+1
#    fraction_accepted = 0
#    for y in xrange(len(L_YesNo[n])):
#        fraction_accepted = fraction_accepted + L_YesNo[n][y]

#    print 'total # words accepted =', fraction_accepted
#    fraction_accepted = (fraction_accepted*1.0)/len(L_YesNo[n])
#    print 'fraction_accepted =', fraction_accepted


probability_data = []
for m in xrange(len(machines)):
    HMC = convert_DFA_to_hidden_markov_chain(machines[m])
    data = generate_data(HMC,num_data_points)
    #print 'data =', data
    probabilities = compute_probabilities(data,N,WordListsN)
    probability_data.append(probabilities)

X = []
Y = []
Z = []

print 'done data generation'

D1 = []
DH = []
DA = []

#for i in xrange(1):
for i in xrange(len(machines)):
    for j in xrange(len(machines)):
    

        L_YesNo1, L_ListForm1 = construct_accepted_word_lists(machines[i],N,WordListsN)
        L_YesNo2, L_ListForm2 = construct_accepted_word_lists(machines[j],N,WordListsN)          
        print i,j
        
        d_1norm_flat, d_1norm_alpha, d_1norm_values = compute_distance(probability_data[i],probability_data[j],N,1,alpha)
        print 'd_1norm_flat =', d_1norm_flat
        print 'd_1norm_alpha=', d_1norm_alpha
        print 'd_1norm_values =', d_1norm_values

        d_Hausdorff_flat, d_Hausdorff_alpha, d_Hausdorff_values = compute_distance(L_ListForm1,L_ListForm2,N,2,alpha)
        print 'd_Hausdorff_flat =', d_Hausdorff_flat
        print 'd_Hausdorff_alpha =', d_Hausdorff_alpha
        print 'd_Hausdorff_values =', d_Hausdorff_values

        d_Averaged_Min_Distance_flat, d_Averaged_Min_Distance_alpha, d_Averaged_Min_Distance_values = compute_distance(L_ListForm1,L_ListForm2,N,3,alpha)
        print 'd_Averaged_Min_Distance_flat =', d_Averaged_Min_Distance_flat
        print 'd_Averaged_Min_Distance_alpha =', d_Averaged_Min_Distance_alpha
        print 'd_Averaged_Min_Distance_values', d_Averaged_Min_Distance_values

        X.append(d_1norm_values[N-1])
        Y.append(d_Hausdorff_values[N-1])
        Z.append(d_Averaged_Min_Distance_values[N-1])

        if (i == 0) and (j > 0):
            D1.append(d_1norm_values)
            DH.append(d_Hausdorff_values)
            DA.append(d_Averaged_Min_Distance_values)
        

#        print ''

#figure(1)
#plot(X,Y,'.')
#xlabel('X') # set x-axis label
#ylabel('Y') # set y-axis label
#title('X vs. Y') # set plot title

#figure(2)
#plot(X,Z,'.')
#xlabel('X') # set x-axis label
#ylabel('Z') # set y-axis label
#title('X vs. Z') # set plot title


#figure(3)
#plot(Y,Z,'.')
#xlabel('Hausdorff') # set x-axis label
#ylabel('Averaged Min Distance') # set y-axis label
#title('Hausdorff Metric vs. Averaged Min Distance Metric') # set plot title
#savefig('Metric_Comparisons_lim', dpi=600)

#figure(4)
#plot_sequence_data_machines(machines, 100)
#show()

#figure(5)
#t_points = range(1,N+1)
#plot(t_points, D1[0], t_points, D1[1], t_points, D1[2], t_points, D1[3], t_points, D1[4] )
#xlabel('word length n')
#ylabel('1norm distance')
#title('Convergence in 1norm')

#figure(6)
#t_points = range(1,N+1)
#plot(t_points, DH[0], t_points, DH[1], t_points, DH[2], t_points, DH[3], t_points, DH[4] )
#xlabel('word length n')
#ylabel('Hausdorff distance')
#title('Convergence in Hausdorff Metric')

#figure(7)
#t_points = range(1,N+1)
#plot(t_points, DA[0], t_points, DA[1], t_points, DA[2], t_points, DA[3], t_points, DA[4] )
#xlabel('word length n')
#ylabel('Avg_Min_dist distance')
#title('Convergence in Averaged Min Distance Metric')

        
        






            

    
