Repository: caleb531/automata

Issue:
Conversion from DFA to NFA does not properly handle state names of mixed data types
I found the following bug:
nfa._add_new_state introduces integers into the set of states.

Current code is:
```
    def _add_new_state(state_set, start=0):
        """Adds new state to the state set and returns it"""
        new_state = start
        while new_state in state_set:
            new_state += 1

        state_set.add(new_state)

        return new_state
```
which could be fixed, for example, in this way:
```
    def _add_new_state(state_set, start=0):
        """Adds new state to the state set and returns it"""
        new_state = start
        while str(new_state) in state_set:
            new_state += 1

        state_set.add(str(new_state))

        return str(new_state)
```
EDIT: this bugfix actually runs into other problems later - this is just an idea
_____________________
This can lead to logic problems later. For example the following code fails:
```
from automata.fa.dfa import DFA
from automata.fa.nfa import NFA

A = NFA(
    states={'0', '1'},
    input_symbols={'0'},
    transitions={'0': {'0': {'1'}}, '1': {'0': {'1'}}},
    initial_state='0',
    final_states={'1'}
)

print(A.reverse().states)

B = DFA.from_nfa(A.reverse())

assert(A.accepts_input('00')==B.accepts_input('00'))
```

Comments:

Patch:
diff --git a/README.md b/README.md
index 2941653..6431318 100644
--- a/README.md
+++ b/README.md
@@ -317,9 +317,10 @@ Returns `True` if the DFA accepts a finite language, False otherwise.
 dfa.isfinite()
 ```
 
-#### DFA.from_nfa(cls, nfa)
+#### DFA.from_nfa(cls, nfa, retain_names=False)
 
-Creates a DFA that is equivalent to the given NFA.
+Creates a DFA that is equivalent to the given NFA. States are renamed by
+default unless `retain_names` is set to `True`.
 
 ```python
 from automata.fa.dfa import DFA
diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py
index 766bf8b..a6d5b3c 100644
--- a/automata/fa/dfa.py
+++ b/automata/fa/dfa.py
@@ -4,7 +4,7 @@
 import copy
 from collections import deque
 from enum import IntEnum
-from itertools import product
+from itertools import product, count
 
 import networkx as nx
 from pydot import Dot, Edge, Node
@@ -540,44 +540,61 @@ def _stringify_states(states):
         return '{{{}}}'.format(','.join(sorted(str(state) for state in states)))
 
     @classmethod
-    def from_nfa(cls, target_nfa):
+    def from_nfa(cls, target_nfa, retain_names=False):
         """Initialize this DFA as one equivalent to the given NFA."""
         dfa_states = set()
         dfa_symbols = target_nfa.input_symbols
-        dfa_transitions = dict()
+        dfa_transitions = {}
+
+        # Data structures for state renaming
+        new_state_name_dict = dict()
+        state_name_counter = count(0)
+
+        def get_name_renamed(states):
+            nonlocal state_name_counter, new_state_name_dict
+            return new_state_name_dict.setdefault(frozenset(states), next(state_name_counter))
+
+        def get_name_original(states):
+            return frozenset(states)
+
+        get_name = get_name_original if retain_names else get_name_renamed
 
         # equivalent DFA states states
         nfa_initial_states = target_nfa._get_lambda_closure(target_nfa.initial_state)
-        dfa_initial_state = cls._stringify_states(nfa_initial_states)
+        dfa_initial_state = get_name(nfa_initial_states)
         dfa_final_states = set()
 
         state_queue = deque()
         state_queue.append(nfa_initial_states)
         while state_queue:
             current_states = state_queue.popleft()
-            current_state_name = cls._stringify_states(current_states)
+            current_state_name = get_name(current_states)
             if current_state_name in dfa_states:
                 # We've been here before and nothing should have changed.
                 continue
 
+
             # Add NFA states to DFA as it is constructed from NFA.
             dfa_states.add(current_state_name)
             dfa_transitions[current_state_name] = {}
             if (current_states & target_nfa.final_states):
                 dfa_final_states.add(current_state_name)
 
+
             # Enqueue the next set of current states for the generated DFA.
             for input_symbol in target_nfa.input_symbols:
                 next_current_states = target_nfa._get_next_current_states(
                     current_states, input_symbol)
-                dfa_transitions[current_state_name][input_symbol] = cls._stringify_states(next_current_states)
+                dfa_transitions[current_state_name][input_symbol] = get_name(next_current_states)
                 state_queue.append(next_current_states)
 
+
         return cls(
             states=dfa_states, input_symbols=dfa_symbols,
             transitions=dfa_transitions, initial_state=dfa_initial_state,
             final_states=dfa_final_states)
 
+
     def show_diagram(self, path=None):
         """
             Creates the graph associated with this DFA


Test patch:
diff --git a/tests/test_dfa.py b/tests/test_dfa.py
index 52e3b9d..e51e2e8 100644
--- a/tests/test_dfa.py
+++ b/tests/test_dfa.py
@@ -1144,17 +1144,17 @@ def test_init_nfa_simple(self):
             initial_state='q0',
             final_states={'q2'}
         )
-        dfa = DFA.from_nfa(nfa)
-        self.assertEqual(dfa.states, {'{}', '{q0}', '{q0,q1}', '{q2}'})
+        dfa = DFA.from_nfa(nfa, retain_names=True)
+        self.assertEqual(dfa.states, {frozenset(), frozenset(('q0',)), frozenset(('q0','q1')), frozenset(('q2',))})
         self.assertEqual(dfa.input_symbols, {'0', '1'})
         self.assertEqual(dfa.transitions, {
-            '{}': {'0': '{}', '1': '{}'},
-            '{q0}': {'0': '{q0,q1}', '1': '{}'},
-            '{q0,q1}': {'0': '{q0,q1}', '1': '{q2}'},
-            '{q2}': {'0': '{}', '1': '{}'}
+            frozenset(): {'0': frozenset(), '1': frozenset()},
+            frozenset(('q0',)): {'0': frozenset(('q0','q1')), '1': frozenset()},
+            frozenset(('q0','q1')): {'0': frozenset(('q0','q1')), '1': frozenset(('q2',))},
+            frozenset(('q2',)): {'0': frozenset(), '1': frozenset()}
         })
-        self.assertEqual(dfa.initial_state, '{q0}')
-        self.assertEqual(dfa.final_states, {'{q2}'})
+        self.assertEqual(dfa.initial_state, frozenset(('q0',)))
+        self.assertEqual(dfa.final_states, {frozenset(('q2',))})
 
     def test_init_nfa_more_complex(self):
         """Should convert to a DFA a more complex NFA."""
@@ -1169,32 +1169,32 @@ def test_init_nfa_more_complex(self):
             initial_state='q0',
             final_states={'q2'}
         )
-        dfa = DFA.from_nfa(nfa)
+        dfa = DFA.from_nfa(nfa, retain_names=True)
         self.assertEqual(dfa.states, {
-            '{q0}', '{q0,q1}', '{q0,q2}', '{q0,q1,q2}'
+            frozenset(('q0',)), frozenset(('q0','q1')), frozenset(('q0','q2')), frozenset(('q0','q1','q2'))
         })
         self.assertEqual(dfa.input_symbols, {'0', '1'})
         self.assertEqual(dfa.transitions, {
-            '{q0}': {'1': '{q0}', '0': '{q0,q1}'},
-            '{q0,q1}': {'1': '{q0,q2}', '0': '{q0,q1}'},
-            '{q0,q2}': {'1': '{q0,q1}', '0': '{q0,q1,q2}'},
-            '{q0,q1,q2}': {'1': '{q0,q1,q2}', '0': '{q0,q1,q2}'}
+            frozenset(('q0',)): {'1': frozenset(('q0',)), '0': frozenset(('q0','q1'))},
+            frozenset(('q0','q1')): {'1': frozenset(('q0','q2')), '0': frozenset(('q0','q1'))},
+            frozenset(('q0','q2')): {'1': frozenset(('q0','q1')), '0': frozenset(('q0','q1','q2'))},
+            frozenset(('q0','q1','q2')): {'1': frozenset(('q0','q1','q2')), '0': frozenset(('q0','q1','q2'))}
         })
-        self.assertEqual(dfa.initial_state, '{q0}')
-        self.assertEqual(dfa.final_states, {'{q0,q1,q2}', '{q0,q2}'})
+        self.assertEqual(dfa.initial_state, frozenset(('q0',)))
+        self.assertEqual(dfa.final_states, {frozenset(('q0','q1','q2')), frozenset(('q0','q2'))})
 
     def test_init_nfa_lambda_transition(self):
         """Should convert to a DFA an NFA with a lambda transition."""
-        dfa = DFA.from_nfa(self.nfa)
-        self.assertEqual(dfa.states, {'{}', '{q0}', '{q1,q2}'})
+        dfa = DFA.from_nfa(self.nfa, retain_names=True)
+        self.assertEqual(dfa.states, {frozenset(), frozenset(('q0',)), frozenset(('q1', 'q2'))})
         self.assertEqual(dfa.input_symbols, {'a', 'b'})
         self.assertEqual(dfa.transitions, {
-            '{}': {'a': '{}', 'b': '{}'},
-            '{q0}': {'a': '{q1,q2}', 'b': '{}'},
-            '{q1,q2}': {'a': '{q1,q2}', 'b': '{q0}'},
+            frozenset(): {'a': frozenset(), 'b': frozenset()},
+            frozenset(('q0',)): {'a': frozenset(('q1', 'q2')), 'b': frozenset()},
+            frozenset(('q1', 'q2')): {'a': frozenset(('q1', 'q2')), 'b': frozenset(('q0',))},
         })
-        self.assertEqual(dfa.initial_state, '{q0}')
-        self.assertEqual(dfa.final_states, {'{q1,q2}'})
+        self.assertEqual(dfa.initial_state, frozenset(('q0',)))
+        self.assertEqual(dfa.final_states, {frozenset(('q1', 'q2'))})
 
     def test_nfa_to_dfa_with_lambda_transitions(self):
         """ Test NFA->DFA when initial state has lambda transitions """
@@ -1209,8 +1209,8 @@ def test_nfa_to_dfa_with_lambda_transitions(self):
             initial_state='q0',
             final_states={'q1'}
         )
-        dfa = DFA.from_nfa(nfa)  # returns an equivalent DFA
-        self.assertEqual(dfa.read_input('a'), '{q1}')
+        dfa = DFA.from_nfa(nfa, retain_names=True)  # returns an equivalent DFA
+        self.assertEqual(dfa.read_input('a'), frozenset(('q1',)))
 
     def test_partial_dfa(self):
         """Should allow for partial DFA when flag is set"""