Rev 140 | Rev 143 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
#!/usr/bin/env python# Copyright: Ira W. Snyder# Start Date: 2005-10-08# End Date:# License: Public Domain## Changelog Follows:# - 2005-10-16# - Implemented the nfa class. It's mostly complete at this point.# - E() function works for circular loops now.# - Made the nfa.__next_states() function always return a valid reference# to a list in the dictionary. This means you should NEVER use# self.trans_func[(state, letter)] in code anywhere now :)# - Final states are now checked for validity.## - 2005-10-19# - Everything now works, with a menu even.## - 2005-10-20# - Added the check for <Python-2.3 compatibility.# - Commented the source more.## - 2005-10-22# - Removed an unnecessary call in input_autmaton()################################################################################## IMPORTANT NOTES################################################################################# The DFA table format:## [ [Final, 0, NFA_Equivalent, letter1, letter2, ..., lettern],# [ [Final, 1, NFA_Equivalent, letter1, letter2, ..., lettern],# [ [Final, 2, NFA_Equivalent, letter1, letter2, ..., lettern] ]################################################################################## The nfa.trans_func format:## dict[(state, letter)] = [list, of, states]################################################################################## Check for <Python-2.3 compatibility (boolean values)try:True, Falseexcept NameError:(True, False) = (1, 0)import sys, stringclass nfa:"""Implements a Non-Deterministic Finite Automaton"""def __init__(self):"""Constructor for the nfa object"""self.__clear()def __clear(self):"""Clear all instance variables of the nfa class"""self.trans_func = {}self.num_states = 0self.final_states = []self.initial_state = 0self.possible_letters = []self.dfa_table = []def __state_in_range(self, state):"""Check if a given state is in the acceptable range"""# Check to make sure this can be converted to an int.# Return False if the conversion fails.try:temp = int(state)except (TypeError, ValueError):return False# Make sure this is a state in the rangeif temp >= 0 and temp < self.num_states:return True# We weren't in the correct range, so return Falsereturn Falsedef __input_num_states(self):"""Ask the user (nicely) for the number of states this automaton will have"""done = Falsewhile not done:num_states = raw_input('How many states in this automaton: ')# Make sure we can convert the value successfully, then set# the num_states variable.try:self.num_states = int(num_states)done = Trueexcept (TypeError, ValueError):print 'Bad input, not an integer, try again'def __input_final_states(self):"""Ask the user for a list of final states for this automaton"""print 'Enter final states on one line, seperated by spaces'done = Falsewhile not done:final_states = raw_input('Final states: ')# Check each value entered, one by one, and set bad_data if there# was a state out of the valid rangebad_data = Falsefor i in final_states.split():if not self.__state_in_range(i):print 'State %s out of range. All input discarded.' % (i, )print 'Try again please'bad_data = Truebreak# If we left the for loop with bad data, start over.if bad_data:continue# All data is good, read the values into the final_states list.for i in final_states.split():if self.__state_in_range(i):self.final_states.append(int(i))done = Truedef __input_all_edges(self):"""Ask the user to input all of the edges for this automaton"""print 'Edges take the form "from HERE, by LETTER, to THERE"'print 'Enter something like: "1 a 3" to represent the following'print 'from q1, by a, go to q3'print 'RULES:'print '------------------------------------------------------'print '1. Enter a blank line to stop reading edges'print '2. ^ is the empty string'print '3. Enter one letter at a time (no multi-letter states)'# Read edges, one by one. Check them as they are entered.done = Falsewhile not done:edge = raw_input('Edge: ')# Check to see if we got a blank lineif len(edge) == 0:done = Truecontinue# If we didn't get exactly 3 arguments, try againif len(edge.split()) != 3:print 'Bad edge entered.'print 'Exactly 3 arguments are required for a valid edge.'continue# All checks appear fine, set a variable for each piece(cur_st, letter, next_st) = edge.split()# Make sure the states entered are in rangeif self.__state_in_range(cur_st) and self.__state_in_range(next_st):new_state = Falsecur_st = int(cur_st) # convert to intnext_st = int(next_st) # convert to int# Add the state to the list if it's not there alreadyif next_st not in self.__next_states(cur_st, letter):self.__next_states(cur_st, letter).append(next_st)else: # At least one state was not in rangeprint 'Invalid current or next state entered'continuedef __input_automaton(self):"""Read this entire automaton's input"""self.__input_num_states()self.__input_final_states()self.__input_all_edges()def main_menu(self):"""Display the main menu for this automaton"""done = Falseautomaton_entered = Falsewhile not done:print 'Menu:'print '========================================'print '1. Enter an NFA'print '2. Output corresponding DFA'print '3. Quit's = raw_input('Choice >>> ')if s == '1':self.__clear()self.__input_automaton()automaton_entered = Trueelif s == '2':if automaton_entered:self.__output_dfa()else:print 'Enter a NFA first'elif s == '3':done = Trueelse:print 'Bad Selection'def __output_dfa(self):"""Generate and print the DFA that corresponds to the NFA entered"""print 'Initial State: %s' % (0, )self.__generate_dfa_table()self.__print_dfa_table()def __print_dfa_table(self):"""Print out a nicely spaced representation of the DFA's table"""#header1 = '%-8s%-8s%-20s' % ('Final', 'Number', 'NFA Equiv')header1 = '%-8s%-8s' % ('Final', 'State #')header2 = ''for l in self.possible_letters:header2 = '%s%-4s' % (header2, l)heading = '%s %s' % (header1, header2)hdr_line = ''for i in range(len(heading)):hdr_line = '%s-' % (hdr_line, )print headingprint hdr_linefor rec in self.dfa_table:#line1 = '%-8s%-8s%-20s' % (rec[0], rec[1], rec[2])line1 = '%-8s%-8s' % (rec[0], rec[1])line2 = ''for l in range(len(self.possible_letters)):line2 = '%s%-4s' % (line2, rec[l+3])print '%s %s' % (line1, line2)def __next_states(self, state, letter):"""Return the next states for the key (state, letter)"""try:next = self.trans_func[(state, letter)]except KeyError:# Create a new empty list for this state if it doesn't exist yetnext = self.trans_func[(state, letter)] = []# Make sure that we can go to ourselves by the empty letterif letter == '^' and state not in next:next.append(state)return nextdef __E(self, state, letter='^', storage=None, visited=None):"""Calculate E(state) and return it. This handles circular E()calculations."""# Work around weird mutable default argument stuffif storage is None:storage = []# Work around weird mutable default argument stuffif visited is None:visited = []# Find all of the direct next states that we can get to by# the empty string, and append anything that is not already therefor i in self.__next_states(state, letter):if i not in storage:storage.append(i)# Visit everything in storage that has not been visited alreadyfor i in storage:if i not in visited:visited.append(i)temp = self.__E(i, letter, storage, visited)# Avoid duplicating things in storagefor j in temp:if j not in storage:storage.append(j)return storagedef __by_letter(self, states, letter):"""Returns a list of states to where you can go from a group (list)of states, with a certain letter"""from_states = []# Make sure E() has been called on every state here.# It should have been already, but just to make sure,# we'll do it here, too :)for s in states:from_states.extend(self.__E(s))from_states = self.__unique_sort(from_states)new_states = []# For each state, find the next states, and add them to the listfor s in from_states:new_states.extend(self.__next_states(s, letter))new_states = self.__unique_sort(new_states)return new_statesdef __unique_sort(self, li):"""Make sure everything in a list is unique, and then sort it"""newlist = []for i in li:if i not in newlist:newlist.append(i)newlist.sort()return newlistdef __is_final_state(self, state):"""Check if a state is a final state."""if state in self.final_states:return Truereturn Falsedef __is_list_final(self, states):"""Check if at least one state in the list "states" is final"""for s in states:if self.__is_final_state(s):return Truereturn Falsedef __get_temp_record(self, num_letters):"""Create a record (for the DFA table) that has the proper numberof slots"""blank = Nonereturn [blank for i in range(num_letters + 3)]def __get_possible_letters(self):"""Create a list of all the possible letters for the NFA,and store it in possible_letters"""# Get the list of all letterspossible_letters = []for (state, letter) in self.trans_func.keys():if letter not in possible_letters and letter != '^':possible_letters.append(letter)possible_letters = self.__unique_sort(possible_letters)self.possible_letters = possible_lettersdef __generate_dfa_table(self):"""Generate a table for the DFA representation of the NFA entered earlier"""# Get all the possible lettersself.__get_possible_letters()# Prime the dfa table with the first stateself.make_basic_record(self.__E(0))# Loop until we resolve every letter in the tablewhile self.find_unresolved_letter() != (-1, -1):(record, letter_pos) = self.find_unresolved_letter()self.resolve_letter(record, letter_pos)def resolve_letter(self, record, letter_pos):"""Resolve a letter in the table, either adding a new entry if therequired state doesn't already exist, or putting a link toan existing state"""# Get the NFA equivalent multi-statefromstates = self.dfa_table[record][2]tostates = []# Find all the states we can go to from our multi-statefor s in fromstates:tostates.extend(self.__next_states(s, self.possible_letters[letter_pos-3]))tostates = self.__unique_sort(tostates)# Make the letter we are trying to resolve point to a record in the table.# make_basic_record() will return the correct record, even if it needs to# create a new one.self.dfa_table[record][letter_pos] = self.make_basic_record(tostates)def find_unresolved_letter(self):"""Returns an index pair of an unresolved letter that exists in the dfa_table.If there are no more letters, return (-1, -1)"""for i in range(len(self.dfa_table)):for j in range(len(self.possible_letters)):if self.dfa_table[i][j+3] == None:return (i, j+3)return (-1, -1)def make_basic_record(self, from_states):"""Create a record, if necessary, for the states "from_states."If a corresponding state already exists, return it's index. A new statewill have everything but the letters filled in."""if self.dfa_state_exists(from_states) == -1: #doesn't existtemp_record = self.__get_temp_record(len(self.possible_letters))recordnum = len(self.dfa_table)temp_record[1] = recordnumtemp_record[2] = self.__unique_sort(from_states)temp_record[0] = self.__is_list_final(from_states)self.dfa_table.append(temp_record)# always return a re-call of the function. This will not fail, since a new# record is added above if a record does not already exist.return self.dfa_state_exists(from_states)def dfa_state_exists(self, from_states):"""Find out if this state already exists in the dfa table.If it does exist, return it's dfa state name.If it does not exist, return -1"""sorted_states = self.__unique_sort(from_states)for i in range(len(self.dfa_table)):if self.dfa_table[i][2] == sorted_states:return self.dfa_table[i][1]return -1if __name__ == '__main__':automaton = nfa()automaton.main_menu()