# Automata_Functions.py
# File of Basic functions for DFA's, NFA's, and Transducers. There are 13 main functions:

# (0) sort_DFA(M)
# (1) convert_process_graph_to_DFA
# (2) convert_NFA_to_DFA
# (3) find_equivalence_classes
# (4) check_equivalence_of_DFAs     (uses 2)
# (5) minimize_DFA                  (uses 2)
# (6) construct_T_out               (uses 1,4)
# (7) compose_transducer_and_DFA
# (8) find_recurrent_components
# (9) construct_process_graphs
# (10) test_word
# (11) create_unit_perturbation_DFA
# (12) check_containment

# NOTE: In this program, DFA's and Transducers are required to have a single start state (the state 0). NFA's are allowed to have multiple start states.
# NOTE: Throughout this program a T/F variable has value 0 if false, 1 if true. T/F variables are used many places. (i.e. accept, start, equivalent ...)

# import modules
from numpy import *
from Graph_Functions import *

# (0) Define sort_DFA(M)
# This function takes a DFA, M, with a single non-accept state and sorts the order of the DFA states so this becomes the final state for the DFA and
# relabels transitions accordingly. It is assumed that the first state in the list is the start state and that this state is an accept state so it
# remains the start for the sorted DFA. The DFA is given as a list of states of the form [accept?, [IS = 0 transition, Is = 1 transitition]]
def sort_DFA(M):
    
    num_states = len(M)
    for x in xrange(num_states):
        if M[x][0] == 0: junk_state = x

    sorted_DFA = []
    
    for x in xrange(junk_state):
        state = M[x]

        if state[1][0] < junk_state: IS0 = state[1][0]
        elif state[1][0] == junk_state: IS0 = num_states - 1
        elif state[1][0] > junk_state: IS0 = state[1][0] - 1

        if state[1][1] < junk_state: IS1 = state[1][1]
        elif state[1][1] == junk_state: IS1 = num_states - 1
        elif state[1][1] > junk_state: IS1 = state[1][1] - 1

        new_state = [1,[IS0,IS1]]
        sorted_DFA.append(new_state)

    for x in range(junk_state + 1,  num_states):
        state = M[x]

        if state[1][0] < junk_state: IS0 = state[1][0]
        elif state[1][0] == junk_state: IS0 = num_states - 1
        elif state[1][0] > junk_state: IS0 = state[1][0] - 1

        if state[1][1] < junk_state: IS1 = state[1][1]
        elif state[1][1] == junk_state: IS1 = num_states - 1
        elif state[1][1] > junk_state: IS1 = state[1][1] - 1

        new_state = [1,[IS0, IS1]]
        sorted_DFA.append(new_state)

    sorted_DFA.append([0,[num_states - 1, num_states - 1]])
    return sorted_DFA


# (1) define convert_process_graph_to_DFA(Process_Graph)
# Input - a process graph given as a list of states of the form [IS0 transition, IS1 transition], (all states are assumed to be both start and accept).
# Output - First the process graph is converted to an NFA, and then the NFA is converted to a DFA which is given as output.
# This DFA accepts all and only word 'generatable' by the process graph.

def convert_process_graph_to_DFA(Process_Graph):
    NFA = []
    num_states = len(Process_Graph)
    
    for x in xrange(len(Process_Graph)):
        node = Process_Graph[x]
        IS0 = node[0]
        IS1 = node[1]
        if IS0 == 'X':IS0 = num_states
        if IS1 == 'X':IS1 = num_states
        NFA.append([ [1,1], [[IS0],[IS1]] ])

    NFA.append([ [0,0], [[num_states],[num_states]] ])
    print 'NFA =',  NFA
    
    DFA, DFA_Fixed = convert_NFA_to_DFA(NFA)
    DFA_Fixed = minimize_DFA(DFA_Fixed) # new line here may mess things up to minimize but shouldn't
    return DFA_Fixed

        
# (2) Define convert_NFA_to_DFA(M)
# Function to convert an NFA to a DFA using the standard subset constructios.
# The input MM is an NFA given as a list of states each of the form [ [accept?, start?], [[IS = 0 transitions], [Is = 1 transititions]]  ]
# The ouptut is two DFA's M and M_fixed. M gives the states and transitions listed explicitly as subsets of the power set of the NFA states,
# and M_fixed renames them with single numbers for simplicitly/further use.
# Each entry of M and M_fixed has the form [accept?, [IS = 0 transition, Is = 1 transitition]]      

def convert_NFA_to_DFA(MM):
    num_input_symbols = len(MM[0][1])
    num_NFA_states = len(MM)
    M = []
    num_DFA_states = 0
    
    start_state = []                                    #
    for n in xrange(num_NFA_states):                    #
        if MM[n][0][1] == 1: start_state.append(n)      #
        
    #next_DFA_state = [0]
    next_DFA_state = start_state

    go = 1
    while go == 1:
        num_DFA_states = num_DFA_states + 1
        L = [next_DFA_state]
        
        for symbol in xrange(num_input_symbols):  # loop through each input symbol and create list of transitions
            transitions = []
            
            for state in next_DFA_state:
                for t in MM[state][1][symbol]:
                    transitions.append(t)

            transitions = set(transitions)          #remove duplications and create sorted list
            transitions = list(transitions)         #then add this set of transitions to the DFA
            transitions = sort(transitions)
            transitions = list(transitions)
            L.append(transitions)

        M.append(L)


        go = 0
        Break = 0
        for state in xrange(num_DFA_states):                 #check if we have any new DFA states
            if Break == 1:break
            
            for symbol in xrange(1, num_input_symbols + 1):
                possible_new_state = M[state][symbol]
                old = 0

                for n in xrange(num_DFA_states):
                    if possible_new_state == M[n][0]: old = 1

                if old == 0:
                    next_DFA_state = possible_new_state
                    Break = 1
                    go = 1
                    break
                

    #create a 'cleaned up' version of M, M_fixed, where each state has a single symbol, also tells if states are accept states
    num_states = len(M)
    M_fixed = []
    
    for n in xrange(num_states):

        #check if accept state
        accept = 0
        for i in M[n][0]:
            #if MM[i][0] == 1: accept = 1
            if MM[i][0][0] == 1: accept = 1
            
        L = [accept]
        
        LL = []
        for s in xrange(num_input_symbols):
            state = M[n][s+1]
            for t in xrange(num_states):
                if M[t][0] == state:
                    LL.append(t)
                    break
                
        L.append(LL)
        M_fixed.append(L)



    return M,M_fixed


# (3) Define find_equivalence_classes(M)
# Find the equivalence classes for a DFA M with states 0,1,2,... N-1 using the table filing algorithm.
# In the table we have a 1 in position (i,j) if states i and j are equivalent and a 0 if NOT.
# M = list of states of form [accept?, [output on symbol 0, output on sybmol 1]].

def find_equivalence_classes(M):
    N = len(M)
    table = 7*ones((N,N),int)

    # set each state equivalent to itself
    for i in xrange(N):
        table[i][i] = 1

    # determine which states are distinguishable by length 0 words
    for i in xrange(N):
        for j in xrange(i):
            test = mod(M[i][0] + M[j][0],2)
            if test == 1:
                table[i][j] = 0
                table[j][i] = 0
            

    # loop until no more states are found to be distinguishable
    num_iterations = 0
    go = 1
    
    while go == 1:
        num_iterations = num_iterations + 1
        go = 0

        for i in xrange(N):
            for j in xrange(i):
                if table[i][j] != 0:

                    next_i0 = M[i][1][0]
                    next_i1 = M[i][1][1]
                    next_j0 = M[j][1][0]
                    next_j1 = M[j][1][1]
                    
                    if table[next_i0][next_j0] == 0 or table[next_i1][next_j1] == 0:
                        table[i][j] = 0
                        table[j][i] = 0
                        go = 1                    
     
        if num_iterations > 100: break

        
    for i in xrange(N):
        for j in xrange(N):
            if table[i][j] != 0: table[i][j] = 1

    for i in xrange(N):
        table[i][i] = 9

    # use table to determine equivalence classes
    remaining_states = range(N)
    remaining_states = set(remaining_states)
    equivalence_classes = []
    num_states_left = N

    while num_states_left > 0:
        
        remaining_states_list = list(remaining_states)
        next_class_representative = remaining_states_list[0]
        next_equivalence_class = [next_class_representative]
        
        for n in xrange(N):
            if table[n][next_class_representative] != 0: 
                next_equivalence_class.append(n)
                
        next_equivalence_class = set(next_equivalence_class)
        next_equivalence_class = list(next_equivalence_class)
        next_equivalence_class = sort(next_equivalence_class)
        next_equivalence_class = list(next_equivalence_class)

        equivalence_classes.append(next_equivalence_class)
        remaining_states = remaining_states - set(next_equivalence_class)
        num_states_left = num_states_left - len(next_equivalence_class)


    return table, equivalence_classes

                
                
# (4) Define check_equivalence_of_DFAs(M1,M2)
# Determine whether two DFA's M1,M2 over the same alphabet 0,1,2...N are equivalent.
def check_equivalence_of_DFAs(M1,M2):
    
    num_M1_states = len(M1)
    num_M2_states = len(M2)
    num_symbols = len(M2[0][1])

    # rename the M2 states
    for n in xrange(num_M2_states):
        for s in xrange(num_symbols):
            M2[n][1][s] = M2[n][1][s] + num_M1_states
    

    # create new machine M of M1 combined with the new M2
    M = M1 + M2

    #determine the equivalence of M and see if the M1 start state = 0, M2 start state = num_M1_states are in same equivalence class
    table, equivalence_classes = find_equivalence_classes(M)
    #print 'table =', table
    #print 'equivalence classes =', equivalence_classes
    
    test_set = set(equivalence_classes[0])
    #if 0 in test_set: print '0_M1 in test set'
    #if num_M1_states in test_set: print '0_M2 in test set'
    
    equivalent = 0
    if (0 in test_set) and (num_M1_states in test_set): equivalent = 1

    return equivalent


# (5) Define minimize_DFA(M)
# Determine the minimum equivalent DFA to a given DFA M.

def minimize_DFA(M):
    num_symbols = len(M[0][1])
    num_states = len(M)

    # create a list of sets of possible transitions from each state  = [ {1,2}, {1,3,4}, ...{6,7} ]
    possible_transitions = []
    for n in xrange(num_states):
        L = []
        
        for s in xrange(num_symbols):
                L.append(M[n][1][s])

        possible_transitions.append(set(L))

    # deterime the set of states accesible from the start state 0
    accesible_states = [0]
    go = 1

    while go == 1:
        transition_set = set([])
        
        for state in accesible_states:
            transition_set = transition_set | possible_transitions[state]

        N = len(accesible_states)
        accesible_states = list(set(accesible_states) | transition_set)
        accesible_states = sort(accesible_states)
        if len(accesible_states) == N: go = 0

    #print 'accesible_states =', accesible_states

    # remove non-accesible states from the machine, and create a new machine with the remaining states renumbered 0,1,2,...Max_State_Number
    num_removed_states = num_states - len(accesible_states)
    num_accesible_states = len(accesible_states)


    M_new = []
    for n in xrange(num_accesible_states):
        old_state_number = accesible_states[n]
        if M[old_state_number][0] == 0: L = [0]
        if M[old_state_number][0] == 1: L = [1]

        transitions = []
        for s in xrange(num_symbols):
            for nn in xrange(num_accesible_states):
                if M[old_state_number][1][s] == accesible_states[nn]:
                    transitions.append(nn)
                    break

        L.append(transitions)
        M_new.append(L)
    
        
    M = M_new
    #print 'M_new =', M_new

    # reduce DFA by grouping remaining states into equivalence classes
    table, equivalence_classes = find_equivalence_classes(M)
    #print 'table =', table
    #print 'equivalence_classes =', equivalence_classes
    N = len(equivalence_classes)
    MM = []

    for n in xrange(N):
        element = equivalence_classes[n][0]
        if M[element][0] == 0: L = [0]
        if M[element][0] == 1: L = [1]
        transitions = []
        
        for s in xrange(num_symbols):
            next_element = M[element][1][s]
            
            for x in xrange(N):
                for y in equivalence_classes[x]:
                    if next_element == y:transitions.append(x)

        L.append(transitions)
        MM.append(L)
        
    return MM


# (6) Define construct_T_out(T,num_input_symbols,num_output_symbols)
# This function takes a transducer T with input language L1 = {0,1,2,...num_input_symbols - 1} and output language
# L2 = {0, 1,2,...num_output_symbols - 1}, and converts it to a DFA, T_out, with input language L2.
# T_out accepts only words in L2 which are possible outputs of T. i.e. if T_out accepts a word w from L2 there exists some w' in L1 s.t. T(w') = w.
# The transducer T has the following form (TS = transducer state, TSN = transducer_state_next, OS = ouptut symbol, IS = input symbol).           
                
# The Transducer Format: 
#
#                     IS 0        IS 1        IS 2 
#
# [    [accept?, [ [[OS],TSN], [[OS],TSN], [[OS],TSN] ]     ]      TS 0
#      [accept?, [ [[OS],TSN], [[OS],TSN], [[OS],TSN] ]     ]      TS 1
#      [accept?, [ [[OS],TSN], [[OS],TSN], [[OS],TSN] ]     ]      TS 2
#      [accept?, [ [[OS],TSN], [[OS],TSN], [[OS],TSN] ]     ]  ]   TS 3
    
        
def construct_T_out(T, num_IN_symbols, num_OUT_symbols):
    num_transducer_states = len(T)

    # (i) construct an NFA accepting outputs of T. This NFA has the same number of states as T, same start state, and an extra input symbol lambda = null symbol (=2).
    NFA = []
    for n in xrange(num_transducer_states):
        if T[n][0] == 0: next_NFA_state = [0]
        if T[n][0] == 1: next_NFA_state = [1]

        transitions = []
        for OS in xrange(num_OUT_symbols):  
            L = []
            
            for IS in xrange(num_IN_symbols):
                if T[n][1][IS][0][0] == OS: L.append(T[n][1][IS][1])

            transitions.append(L)
                                                 
        next_NFA_state.append(transitions)
        NFA.append(next_NFA_state)
        #print 'next_NFA_state =', next_NFA_state



    # (ii) modify this NFA to an NFA with multiple start states and NOT accepting the lamda symbol, only accepting {0,1}.
    num_input_symbols = num_OUT_symbols - 1 # these are the input symbols for the New NFA
    

    # find all states reachable in NFA from start state 0
    accesible_states = set([0])
    
    go = 1
    while go == 1:

        accesible_states_next = list(accesible_states)
        for state in accesible_states:
            for s in xrange(num_input_symbols + 1):
                for t in NFA[state][1][s]:
                    accesible_states_next.append(t)

        accesible_states_next = set(accesible_states_next)
        if accesible_states_next == accesible_states: go = 0
        accesible_states = accesible_states_next
    

    # find all states reachable in NFA transitioning along lambda's
    lambda_accesible_states = set([0])
    
    go = 1
    while go == 1:

        lambda_accesible_states_next = list(lambda_accesible_states)
        for state in lambda_accesible_states:
            for t in NFA[state][1][num_input_symbols]:
                lambda_accesible_states_next.append(t)

        lambda_accesible_states_next = set(lambda_accesible_states_next)
        if lambda_accesible_states_next == lambda_accesible_states: go = 0
        lambda_accesible_states = lambda_accesible_states_next

    # find all start states
    start_states = []
    
    lambda_accesible_states = list(lambda_accesible_states)
    for state in lambda_accesible_states:
        transitions = []
        
        for s in xrange(num_input_symbols):
            for x in NFA[state][1][s]:transitions.append(t)

        if transitions != []: start_states.append(state)

    start_states = set(start_states)
    lambda_accesible_states = set(lambda_accesible_states)
    
    # classify the states into types based on this information
    all_states = set(range(num_transducer_states))
    removeable_accesible_states = lambda_accesible_states - start_states    
    new_NFA_states = accesible_states - removeable_accesible_states
    non_start_states = new_NFA_states - start_states

    #print 'all_states =', all_states
    #print 'accesible_states =', accesible_states
    #print 'removeable_accesible_states =', removeable_accesible_states
    #print 'new_NFA_states =', new_NFA_states
    #print 'start_states =', start_states
    #print 'non_start_states =', non_start_states
    #print ''

    new_NFA_states = list(new_NFA_states)
    start_states = list(start_states)
    non_start_states = list(non_start_states)

    num_new_NFA_states = len(new_NFA_states)
    num_start_states = len(start_states)
    num_non_start_states = len(non_start_states)

    # create the new NFA without lambda transitions
    state_list_new_to_old = []
    for n in xrange(num_start_states):
        state_list_new_to_old.append(start_states[n])
    for n in xrange(num_non_start_states):
        state_list_new_to_old.append(non_start_states[n])

    state_list_old_to_new = []
    for n in xrange(num_transducer_states):
        found_it = 0
        
        for m in xrange(num_new_NFA_states):
            if state_list_new_to_old[m] == n:
                state_list_old_to_new.append(m)
                found_it = 1
                break

        if found_it == 0: state_list_old_to_new.append('x')
            
                

    NewNFA = []
    for n in xrange(num_new_NFA_states):
        old_state_number = state_list_new_to_old[n]
        accept = NFA[old_state_number][0]
        
        if n < num_start_states:start = 1
        if n >= num_start_states:start = 0

        transitions = []
        for s in xrange(num_input_symbols):
            L = []

            for t in NFA[old_state_number][1][s]:
                L.append(t)

            LL = []
            for s in L:
                LL.append(state_list_old_to_new[s])

            transitions.append(LL)

        NewNFA.append([[accept,start], transitions])


    #print 'NFA ='
    #for line in NFA:
    #    print line
    #print ''

    #print 'NewNFA ='
    #for line in NewNFA:
    #    print line
    #print ''

    # (iii) convert NFA to DFA
    DFA, DFA_fixed = convert_NFA_to_DFA(NewNFA)
    #print 'DFA = ', DFA
    #print 'DFA_fixed =', DFA_fixed

    # (iv) minimize the DFA
    T_out = minimize_DFA(DFA_fixed)
    #print 'T_out =', T_out

    return T_out


# (7) Define compose_Transducer_and_DFA(T,M)
# This function takes a transducer T and DFA M over the same set of input symbols {1,2,...num_input_symbols} and produces the composition transducder TT = T of M.
# TT admits as inputs only strings accepted by M and acts on these strings identically to T other strings end in non-accept states of TT. TTT is a cleaned
# up, but less explicit, form of TT that has a single list of states as oppose to states doubly indexed by i,j. 

def compose_Transducer_and_DFA(T,M):    
    num_input_symbols = len(M[0][1])
    num_DFA_states = len(M)
    num_Transducer_states = len(T)
    
    TT = []
    TTT = []

    for j in xrange(num_Transducer_states):
            for i in xrange(num_DFA_states):

                TT_state = [i,j]
                TTT_state = num_DFA_states*j + i
                if M[i][0] == 0 or T[j][0] == 0:accept = 0
                if M[i][0] == 1 and T[j][0] == 1:accept = 1
                TT_transitions = []
                TTT_transitions = []
            
                for s in xrange(num_input_symbols):
                    TT_next_state = [ M[i][1][s], T[j][1][s][1] ]
                    TTT_next_state = num_DFA_states*T[j][1][s][1] + M[i][1][s]
                
                    TT_next_OS = T[j][1][s][0]
                    TTT_next_OS = T[j][1][s][0]
                
                    TT_transitions.append([TT_next_OS,TT_next_state])
                    TTT_transitions.append([TTT_next_OS,TTT_next_state])

                TT_next = [TT_state, accept, TT_transitions]
                TTT_next = [accept, TTT_transitions]
            
                TT.append(TT_next)
                TTT.append(TTT_next)

    return TT, TTT


#(8) Define find_reccurent_components(M)
# This function takes a DFA M, determines its stronly connected components, and then creates a new graph with each strongly connected component as a node. A component is said to be "recurrent" if it has no
# outgoing links to other components. The function returns a list of such components and also the 'entropy' H_0 calculated from the recurrent component graph.
# The initial DFA M is given as list of states of the form [accept?, [IS = 0 transition, Is = 1 transitition]].
# DFA is assumed to have 1 and only 1 non-accept state since it is minimized DFA for a process language.

def find_reccurent_components(M):
    Num_States = len(M)

    #Find non_accept_state
    for n in xrange(Num_States):
        if M[n][0] == 0: reject_state = n

    #Now put the DFA in a new form to use the the function Construct_Component_Graph from Graph_Functions.py (must transfer all connections from the junk state to self connections). 
    L = []
    for n in xrange(Num_States):
        if M[n][1][0] == reject_state: IS0_transition = n
        if M[n][1][0] != reject_state: IS0_transition = M[n][1][0]
        if M[n][1][1] == reject_state: IS1_transition = n
        if M[n][1][1] != reject_state: IS1_transition = M[n][1][1]
        next_state = [IS0_transition, IS1_transition]
        L.append(next_state)

    #Determine the component graph
    LL, LL_fixed = Construct_Component_Graph(L)

    #Find recurrent components
    recurrent_components = []
    NN = len(LL)
    for n in xrange(NN):
        if LL[n][1] == [LL[n][0]]: recurrent_components.append(LL[n][0])

    #compute h_0 for each recurrent component
    H_0 = []
    
    for component in recurrent_components:
        h0 = 1
        
        for x in component:
            num_branches = 0           
            if M[x][1][0] != reject_state: num_branches = num_branches + 1
            if M[x][1][1] != reject_state: num_branches = num_branches + 1
            h0 = h0*num_branches

        h0 = (log(h0)/log(2)) / len(component)
        H_0.append(h0)
            
    return recurrent_components, H_0, LL_fixed
 

#(9) define construct_process_graphs(M)
def construct_process_graphs(M):
    process_graphs = []
    recurrent_components, H_0, LL_fixed = find_reccurent_components(M)
    num_states = len(M)

    # remove the "junk component" consisting only of the recurret non-accept (i.e. junk) state
    removed = 0
    x = -1
    
    while removed == 0:
        x = x + 1
        component = recurrent_components[x]
        
        for state in component:
            if M[state][0] == 0:
                recurrent_components1 = recurrent_components[0:x]
                recurrent_components2 = recurrent_components[x+1: len(recurrent_components)]
                recurrent_components = recurrent_components1 + recurrent_components2
                removed = 1
                break
                
    # now construct the process graphs                
    for component in recurrent_components:
        process_graph = []
        temp_process_graph = []
        
        for x in component:
            temp_process_graph.append([ x, M[x][1][0], M[x][1][1] ])

        for y in temp_process_graph:
            IS0_num = y[1]
            IS1_num = y[2]
            IS0 = 'X'
            IS1 = 'X'

            for z in xrange(len(temp_process_graph)):
                temp_state = temp_process_graph[z]
                if temp_state[0] == IS0_num: IS0 = z
                if temp_state[0] == IS1_num: IS1 = z

            process_graph.append([IS0,IS1])

        process_graphs.append(process_graph)

    return process_graphs


            
# (10) Define test_word(M,w)
# Takes a word w over an alphabet A = 0,1,2 ... n and a DFA M with input alphabet A = 0,1,2... n and determines whether the DFA accepts the word.
# The word is given in the form w = [symbol 0, symbol 1, symbol 2, .... symbol n] and the DFA is a list of states of the form [accept?, [IS = 0 transition, Is = 1 transitition]].

def test_word(M,w):

    num_symbols = len(M[0][1])
    L = len(w)
    machine_state = 0
    
    for l in xrange(L):
        machine_state = M[machine_state][1][w[l]]

    accept = M[machine_state][0]
    
    #print ''
    #print 'accept =', accept
    return accept

    
# (11) Define create_unit_perturbation_DFA(M)
# Takes a DFA M accepting some process language L, and returns a DFA M_unit s.t. M_unit accepts all words within the 'ball of radius 1' about the set of
# words accepted by M. That is, a word is accepted by M_unit if and only if by changing 0 or 1 of the symbols in the word we get a new word accepted by M.
# Both DFA's are a list of states of the form [accept?, [IS = 0 transition, Is = 1 transitition]] with start state = state 0, input alphabet = {0,1}.
# An NFA is created as an intermediate. It is list of states of the form [ [accept?, start?], [[IS = 0 transitions], [Is = 1 transititions]]  ].

def create_unit_perturbation_DFA(M):
    num_states = len(M)

    # First create an NFA accepting all words within the "ball of radius 1" about the language accepted by our DFA. It has twice the number of states as original DFA.
    NFA = []

    # the first half
    for state in xrange(num_states):
        
        if state == 0: start = 1
        if state > 0: start = 0
        accept = M[state][0]
        IS0_transitions = [M[state][1][0], M[state][1][1] + num_states]
        IS1_transitions = [M[state][1][1], M[state][1][0] + num_states]
        NFA.append([ [accept, start],[IS0_transitions, IS1_transitions] ])

    # the second half
    for state in xrange(num_states):
        
        start = 0
        accept = M[state][0]
        IS0_transitions = [M[state][1][0] + num_states]
        IS1_transitions = [M[state][1][1] + num_states]
        NFA.append([ [accept, start],[IS0_transitions, IS1_transitions] ])

    #print 'NFA ='
    #for i in NFA: print i
    
    
    # next convert the NFA to a DFA
    M_unit_expanded_form, M_unit = convert_NFA_to_DFA(NFA)
    #print ''
    #print 'M_unit =',
    #for i in M_unit: print i

    # minimize the DFA
    M_unit_min = minimize_DFA(M_unit)
    #print ''
    #print 'M_unit_min ='
    #for i in M_unit_min: print i
    
    return M_unit_min


# (12) Define check_containment(M1,M2)
# M1, M2 are 2 DFA's over the binary alphabet {0,1}. Each is given as list of states of the form [accept?, [IS = 0 transition, Is = 1 transitition]].
# This Function Determines whether the regular language accepted by M1 is contained in the regular language accepted by M2.
# That is if all words accepted by M1 are also accepted by M2. It does this by creating a new machine MM whose states are pairs of states (M1 state, M2 state).
# First we find all accesible pairs. Then if all pairs which have the M1 state as an accept state also have M2 state as an accept state we know that
# the regualar language accepted by M1 is contained in the regular language accepted by M2. 
# MM is list of states of form [  [M1 state, M2 state], [M1 state accept?, M2 state accept?], [ [M1 state IS0 transition, M1 state IS1 transition], [M2 state IS0 transition, M2 state IS1 transition] ]  ]

def check_containment(M1,M2):

    # set the inititial state of MM as the joint start state
    MM = []
    MM.append([ [0,0] , [ M1[0][0], M2[0][0] ], [[ M1[0][1][0], M1[0][1][1] ], [ M2[0][1][0], M2[0][1][1] ]]  ])

    # add states to MM until we have reached all accesible joint states
    go = 1
    while go == 1:
        stop = 0
        go = 0

        # loop over current MM_state
        for x in xrange(len(MM)):
            if stop == 1:break
            current_M1_state = MM[x][0][0]
            current_M2_state = MM[x][0][1]

            # loop over possible transitions  
            for symbol in xrange(2):             
                new_M1_state = M1[current_M1_state][1][symbol]
                new_M2_state = M2[current_M2_state][1][symbol]

                # create next potential new MM_state
                new_MM_state = []
                new_MM_state.append([new_M1_state, new_M2_state])                                                                                   # [M1 state, M2 state]
                new_MM_state.append([ M1[new_M1_state][0], M2[new_M2_state][0] ])                                                                   # [M1 state accept?, M2 state accept?]
                new_MM_state.append([ [ M1[new_M1_state][1][0], M1[new_M1_state][1][1] ], [ M2[new_M2_state][1][0], M2[new_M2_state][1][1] ] ])     # [ [M1 state IS0 transition, M1 state IS1 transition], [M2 state IS0 transition, M2 state IS1 transition] ]

                # check if we already have this next potential MM_state
                new = 1
                for y in xrange(len(MM)):    
                    if MM[y] == new_MM_state:
                        new = 0
                        break

                if new == 1:
                    MM.append(new_MM_state)
                    stop = 1
                    go = 1
                    break

    # check if there are any states in MM that are accept states for M1, but not for M2
    contained = 1
    for x in xrange(len(MM)):
        if MM[x][1][0] == 1 and MM[x][1][1] == 0: contained = 0

    return contained




##### MAIN PROGRAM #####

# (0) Test sort_DFA(M)
#M = [ [1,[0,1]], [1,[2,3]], [1,[1,1]], [0,[3,3]] ]  #domain 0,W,0,W ...
#M = [ [1,[0,2]], [0,[1,1]], [1,[3,1]], [1,[2,2]] ]  #domain 0,W,0,W ...
#M = [ [1,[4,1]], [1,[4,2]], [1,[4,3]], [1,[4,7]], [1,[7,5]], [1,[7,6]], [1,[7,3]], [0,[7,7]] ] #domain 1110...
#M = [ [1,[6,7]], [1,[6,3]], [1,[6,1]], [0,[3,3]], [1,[3,5]], [1,[3,1]], [1,[3,4]], [1,[6,2]] ] #domain 1110...

#period 14 domain from ECA rule 193
#M = [[1, [1, 2]], [1, [3, 4]], [1, [5, 6]], [1, [7, 8]], [1, [9, 10]], [1, [11, 12]], [1, [13, 14]], [1, [15, 16]], [1, [9, 17]], [1, [18, 19]], [1, [25, 14]], [1, [23, 26]], [1, [19, 22]], [1, [27, 12]], [1, [21, 19]], [1, [24, 16]], [1, [9, 19]], [1, [25, 19]], [1, [19, 26]], [0, [19, 19]], [1, [24, 19]], [1, [27, 19]], [1, [19, 14]], [1, [20, 19]], [1, [19, 16]], [1, [19, 12]], [1, [19, 17]], [1, [23, 19]]]

#sorted_DFA = sort_DFA(M)
#print 'M =', M
#print 'sorted_DFA =', sorted_DFA
#equivalent = check_equivalence_of_DFAs(M, sorted_DFA)
#print 'equivalent =', equivalent


# (1) Test convert_proces_graph_to_DFA(Process_Graph)

#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...}
#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,[0,1]], [1,[2,3]], [1,[1,1]], [0,[3,3]]  ]                                                  # ECA 18 Domain

#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


# (2) Test convert_NFA_to_DFA(MM)

# 1 Start States given (state 0)
#MM = [    [[0,1],[[0,1],[0]]], [[0,0],[[],[2]]], [[1,0],[[],[]]]     ]
#MM = [    [[0,1],[[0,1],[0]]], [[0,0],[[2],[2]]], [[0,0],[[3],[]]], [[1,0],[[3],[3]]]     ]
#MM = [    [[0,1],[[1,3],[1]]], [[1,0],[[2],[1,2]]], [[0,0],[[3],[0]]], [[1,0],[[],[0]]]     ]

# Multiple Start States given
#MM = [    [[0,1],[[0,1],[0]]], [[0,0],[[2],[2]]], [[0,1],[[3],[]]], [[1,0],[[3],[3]]]     ]
#MM = [    [[0,1],[[1,3],[1]]], [[1,1],[[2],[1,2]]], [[0,0],[[3],[0]]], [[1,0],[[],[0]]]     ]
#MM = [ [[1,1],[[1],[4]]], [[1,1],[[2],[2]]], [[1,1],[[4],[3]]], [[1,1],[[0],[0]]], [[0,0],[[4],[4]]] ]                                                           # 1,W,0,W,1,W,0,W, ...
#MM = [ [[1,1],[[1],[2]]], [[1,1],[[3],[4]]], [[1,1],[[5],[6]]], [[1,1],[[0],[7]]], [[1,1],[[7],[0]]], [[1,1],[[7],[0]]], [[1,1],[[0],[7]]], [[0,0],[[7],[7]]]  ] # RRXOR
#MM = [ [[1,1],[[1],[1]]], [[1,1],[[2],[2]]], [[1,1],[[0],[3]]], [[0,0],[[3],[3]]]  ]

# domain (10W)*

# domain (1W0W)*

# domain RR0

# domain RR-XOR 



#M,M_fixed = convert_NFA_to_DFA(MM)
#N = len(M)
#print ''
#print 'M =' , M
#print 'M_fixed =', M_fixed
#print 'N =' , N


# (3) Test find_equivalence_classes(M)

#M = [   [0,[1,5]], [0,[6,2]], [1,[0,2]], [0,[2,6]], [0,[7,5]], [0,[2,6]], [0,[6,4]], [0,[6,2]]   ]
#M = [   [1,[0,1]], [0,[0,1]], [1,[3,4]], [1,[3,4]], [0,[2,4]]     ]
#table, equivalence_classes = find_equivalence_classes(M)
#print table
#print equivalence_classes


# (4) Test check_equivalence_of_DFAs(M1,M2)
          
#M1 = [  [1,[0,1]], [0,[0,1]]    ]
#M2 = [  [1,[1,2]], [1,[1,2]], [0,[0,2]]     ]
#M3 = [  [1,[1,0]], [0,[1,1]] ]
#equivalent = check_equivalence_of_DFAs(M2,M3)
#print 'equivalent =', equivalent


# (5) Test minimize_DFA(M)

#M = [   [0,[1,5]], [0,[6,2]], [1,[0,2]], [0,[2,6]], [0,[7,5]], [0,[2,6]], [0,[6,4]], [0,[6,2]]   ]
#M = [   [1,[0,1]], [0,[0,1]], [1,[3,4]], [1,[3,4]], [0,[2,4]]     ]
#MM = minimize_DFA(M)
#print MM


# (6) Test construct_T_out(T)
#T = [    [1,[ [[2],1],[[2],6] ]], [1,[ [[2],2],[[2],3] ]], [1,[ [[0],2],[[1],3] ]], [1,[ [[0],4],[[0],5] ]], [1,[ [[1],2],[[0],3] ]], [1,[ [[0],4],[[0],5] ]], [1,[ [[2],4],[[2],5] ]]    ]
#T = [    [1,[ [[2],1],[[2],6] ]], [1,[ [[2],2],[[2],3] ]], [1,[ [[0],2],[[1],3] ]], [1,[ [[1],4],[[0],5] ]], [1,[ [[1],2],[[1],3] ]], [1,[ [[0],4],[[0],5] ]], [1,[ [[2],4],[[2],5] ]]    ]
#T_out = construct_T_out(T, 2, 3)
#print ''
#print 'T_out =', T_out
           

# (7) Test compose_Transducer_and_DFA(T,M)

#T = [   [1,[ [[2],1], [[2],2] ]], [1, [ [[0],1], [[0],2] ]], [1, [ [[1],1], [[1],2] ]]  ]
#M = [   [0,[0,1]], [0,[2,1]], [1,[2,2]]     ]
#TT, TTT = compose_Transducer_and_DFA(T,M)
#print 'TT =' 
#for line in TT:
#    print line
#print ''

#print 'TTT ='
#for line in TTT:
#    print line
#print ''


# (8),(9) Test find_recurrent_components(M) and construct_process_graphs(M):
#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...}
#M = [   [1,[1,2]], [1,[3,4]], [1,[2,7]], [1,[10,4]], [1,[5,5]], [1,[6,5]], [1,[3,3]], [1,[9,8]], [1,[7,9]], [1,[7,10]], [0,[10,10]]    ]
#M = [   [1,[1,4]], [1,[3,10]], [0,[2,2]], [1,[3,3]], [1,[4,9]], [1,[6,2]], [1,[10,6]], [1,[8,2]], [1,[2,9]], [1,[2,7]], [1,[2,5]]    ]

#print 'M =', M
#recurrent_components, H_0, LL_fixed = find_reccurent_components(M)
#print 'reccurent components =', recurrent_components
#print 'H_0 =', H_0
#print 'LL_fixed =', LL_fixed

#process_graphs = construct_process_graphs(M)
#print ''
#print 'process_graphs ...'
#for pg in process_graphs:
#    print pg



# (10) Test test_word(M,w)

#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...}
#M = [   [1,[0,1]], [1,[2,3]], [1,[1,1]], [0,[3,3]]  ]                                                  # ECA 18 Domain {0X...}

#w1 = []
#w2 = [0,1]
#w3 = [1,0]
#w4 = [0,0,0,1,0,0]
#w5 = [1,1,0,1,1,1,0]
#w6 = [1,0,1,1,1,0,1,0,0,1]
#w7 = [0,0,1,0,0,0,1,0,1] 

#accept = test_word(M,w1);
#print 'accept =', accept

#accept = test_word(M,w2);
#print 'accept =', accept

#accept = test_word(M,w3);
#print 'accept =', accept

#accept = test_word(M,w4);
#print 'accept =', accept

#accept = test_word(M,w5);
#print 'accept =', accept

#accept = test_word(M,w6);
#print 'accept =', accept

#accept = test_word(M,w7);
#print 'accept =', accept


# (11) Test create_unit_perturbation_DFA(M)

#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...}
#M = [   [1,[0,1]], [1,[2,3]], [1,[1,1]], [0,[3,3]]  ]                                                  # ECA 18 Domain

#M_unit_min = create_unit_perturbation_DFA(M)
#print ''
#print 'M_unit_min ='
#for i in M_unit_min: print i

#w11 = [1,1,1,0,1,1,1,0]
#w12 = [1,1,0,1,1,1,0,1]
#w13 = [1,1,1,1,1,1,1,0]
#w14 = [0,1,1,0,1,1,1,0]
#w15 = [1,0,1,0,1,0,1,0]

#w21 = [0,0,0,1,0,0,0,1]
#w22 = [0,0,1,0,0,0,1,0]
#w23 = [0,0,0,0,0,0,0,1]
#w24 = [1,0,0,1,0,0,0,1]
#w25 = [0,1,0,1,0,1,0,1]

#L = [w11,w12,w13,w14,w15,w21,w22,w23,w24,w25]

#print ''
#for w in L:
#    print 'word =', w
#    accept = test_word(M_unit_min,w);
#    print 'accept =', accept
#    print ''


# (12) Test check_containment(M1,M2)

#M1 = [   [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...}
#M2 = [   [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...}
#M3 = [   [1,[0,1]], [1,[2,3]], [1,[1,1]], [0,[3,3]]  ]                                                  # ECA 18 Domain
#M4 = [ [1,[0,0]] ]                                                                                      # Accepts all words
#M5 = [ [1,[1,1]], [0,[1,1]] ]                                                                           # Rejects all words accept the null word (no symbols)

#print ''
#print '1110 contained in 0001 ?'
#contained = check_containment(M1,M2)
#print 'contained =', contained

#print ''
#print '0001 contained in 1110 ?'
#contained = check_containment(M2,M1)
#print 'contained =', contained

#print ''
#print '1110 contained in 0X ?'
#contained = check_containment(M1,M3)
#print 'contained =', contained

#print ''
#print '0X contained in 1110 ?'
#contained = check_containment(M3,M1)
#print 'contained =', contained

#print ''
#print '0001 contained in 0X ?'
#contained = check_containment(M2,M3)
#print 'contained =', contained

#print ''
#print '0X contained in 0001 ?'
#contained = check_containment(M3,M2)
#print 'contained =', contained

#print ''
#print '1110 contained in all ?'
#contained = check_containment(M1,M4)
#print 'contained =', contained

#print ''
#print '1110 contained in none ?'
#contained = check_containment(M1,M5)
#print 'contained =', contained

#print ''
#print '0001 contained in all ?'
#contained = check_containment(M2,M4)
#print 'contained =', contained

#print ''
#print '0001 contained in none ?'
#contained = check_containment(M2,M5)
#print 'contained =', contained


# (X) Use the functions compose_Transducer_and_DFA, construct_T_out to test if [T of M]_out = M, where M is the DFA for a given domain, T is the rule 18/rule 54 transducer.
# This show the domain language is invariant under global update operator PHI (i.e. domain is preserved when CA is iterated)

# ECA 18 
#T = [    [1,[ [[2],1],[[2],6] ]], [1,[ [[2],2],[[2],3] ]], [1,[ [[0],2],[[1],3] ]], [1,[ [[0],4],[[0],5] ]], [1,[ [[1],2],[[0],3] ]], [1,[ [[0],4],[[0],5] ]], [1,[ [[2],4],[[2],5] ]]    ] #Note '2' as an OS in T = lambda 
#T = [[1, [[[2], 1], [[2], 2]]], [1, [[[2], 3], [[2], 4]]], [1, [[[2], 5], [[2], 6]]], [1, [[[0], 3], [[1], 4]]], [1, [[[0], 5], [[0], 6]]], [1, [[[1], 3], [[0], 4]]], [1, [[[0], 5], [[0], 6]]]] #alternate version
#M = [   [1,[0,1]], [1,[2,3]], [1,[1,1]], [0,[3,3]]  ] # DFA for the rule 18 domain
#M = [   [1,[0,1]], [1,[1,3]], [1,[1,1]], [0,[2,3]]  ] # Slightly modified DFA

#TT, TTT = compose_Transducer_and_DFA(T,M)
#DFA = construct_T_out(TTT, 2, 3)            #2 input symbols {0,1}, 3 output symbols {0,1,2} where 2 = lambda = null symbol
#DFA_min = minimize_DFA(DFA)


# ECA 54 (must iterate twice to get back to the same DFA, since domain has period 2)
#T = [    [1,[ [[2],1],[[2],6] ]], [1,[ [[2],2],[[2],3] ]], [1,[ [[0],2],[[1],3] ]], [1,[ [[1],4],[[0],5] ]], [1,[ [[1],2],[[1],3] ]], [1,[ [[0],4],[[0],5] ]], [1,[ [[2],4],[[2],5] ]]    ]
#T = [[1, [[[2], 1], [[2], 2]]], [1, [[[2], 3], [[2], 4]]], [1, [[[2], 5], [[2], 6]]], [1, [[[0], 3], [[1], 4]]], [1, [[[1], 5], [[0], 6]]], [1, [[[1], 3], [[1], 4]]], [1, [[[0], 5], [[0], 6]]]] #alternate version
#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...}

#TT, TTT = compose_Transducer_and_DFA(T,M)
#DFA = construct_T_out(TTT, 2, 3)                    #2 input symbols {0,1}, 3 output symbols {0,1,2} where 2 = lambda = null symbol
#DFA_min = minimize_DFA(DFA)

#TT, TTT = compose_Transducer_and_DFA(T,DFA_min)
#DFA = construct_T_out(TTT, 2, 3)                    #2 input symbols {0,1}, 3 output symbols {0,1,2} where 2 = lambda = null symbol
#DFA_min = minimize_DFA(DFA)


# The output for either ECA 18 or ECA 54
#print 'DFA = ', DFA
#print 'DFA_min =', DFA_min
#print 'M =', M

#equivalent1 = check_equivalence_of_DFAs(M,DFA)
#equivalent2 = check_equivalence_of_DFAs(M,DFA_min)
#print 'equivalent1 =', equivalent1
#print 'equivalent2 =', equivalent2





        


        

            
            
                
            
            
        
        
