From e1c2317b5d8f7a395c9dafe81f2c49b1943cf290 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Fri, 17 Mar 2023 12:59:29 +0330 Subject: [PATCH 01/83] Added `get_state_name` --- automata/fa/fa.py | 30 ++++++++++++++++++++++++++++++ setup.py | 4 +++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 34b0dfe3..5894b048 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -2,6 +2,7 @@ """Classes and methods for working with all finite automata.""" import abc +from typing import Iterable from automata.base.automaton import Automaton, AutomatonStateT @@ -12,3 +13,32 @@ class FA(Automaton, metaclass=abc.ABCMeta): """An abstract base class for finite automata.""" __slots__ = tuple() + + @staticmethod + def get_state_name(state_data): + """ + Get an string representation of a state. This is used for displaying and + uses `str` for any unsupported python data types. + """ + if isinstance(state_data, str): + if state_data == "": + return "λ" + + return state_data + + if isinstance(state_data, Iterable): + try: + state_items = sorted(state_data) + except TypeError: + state_items = state_data + + inner = ", ".join(FA.get_state_name(sub_data) for sub_data in state_items) + if isinstance(state_data, (set, frozenset)): + return "{" + inner + "}" + + if isinstance(state_data, tuple): + return "(" + inner + ")" + + return "[" + inner + "]" + + return str(state_data) diff --git a/setup.py b/setup.py index 60684932..26e08e48 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,5 @@ from setuptools import setup -setup() + +if __name__ == "__main__": + setup() From 7d657fcf5947c6e8e7b061626be8ca1368d85825 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Fri, 17 Mar 2023 13:44:18 +0330 Subject: [PATCH 02/83] Fixed reordering list and tuple --- automata/fa/fa.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 5894b048..a64d1f62 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -2,7 +2,7 @@ """Classes and methods for working with all finite automata.""" import abc -from typing import Iterable +from typing import Collection, Iterable, Mapping, Set from automata.base.automaton import Automaton, AutomatonStateT @@ -26,19 +26,15 @@ def get_state_name(state_data): return state_data - if isinstance(state_data, Iterable): - try: - state_items = sorted(state_data) - except TypeError: - state_items = state_data - - inner = ", ".join(FA.get_state_name(sub_data) for sub_data in state_items) - if isinstance(state_data, (set, frozenset)): + if isinstance(state_data, (Set, list, tuple)): + inner = ", ".join(FA.get_state_name(sub_data) for sub_data in state_data) + if isinstance(state_data, Set): return "{" + inner + "}" if isinstance(state_data, tuple): return "(" + inner + ")" - return "[" + inner + "]" + if isinstance(state_data, list): + return "[" + inner + "]" return str(state_data) From 128d38a52cf57ad2080dce9c72d9e322aa06e0dc Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Fri, 17 Mar 2023 15:40:10 +0330 Subject: [PATCH 03/83] Added abstract methods for visualization --- automata/fa/dfa.py | 17 +++++++++++++++++ automata/fa/fa.py | 25 ++++++++++++++++++++++--- automata/fa/gnfa.py | 16 ++++++++++++++++ automata/fa/nfa.py | 18 ++++++++++++++++++ 4 files changed, 73 insertions(+), 3 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 81eafd27..0b24c7e5 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1588,3 +1588,20 @@ def show_diagram(self, path=None): if path: graph.write_png(path) return graph + + def iter_states(self): + return iter(self.states) + + def iter_transitions(self): + return ( + (from_, to_, symbol) + + for from_, lookup in self.transitions.items() + for symbol, to_ in lookup.items() + ) + + def is_accepted(self, state): + return state in self.final_states + + def is_initial(self, state): + return state == self.initial_state diff --git a/automata/fa/fa.py b/automata/fa/fa.py index a64d1f62..a94c86e4 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -2,7 +2,7 @@ """Classes and methods for working with all finite automata.""" import abc -from typing import Collection, Iterable, Mapping, Set +from typing import Any, Iterable from automata.base.automaton import Automaton, AutomatonStateT @@ -26,9 +26,9 @@ def get_state_name(state_data): return state_data - if isinstance(state_data, (Set, list, tuple)): + if isinstance(state_data, (set, frozenset, list, tuple)): inner = ", ".join(FA.get_state_name(sub_data) for sub_data in state_data) - if isinstance(state_data, Set): + if isinstance(state_data, (set, frozenset)): return "{" + inner + "}" if isinstance(state_data, tuple): @@ -38,3 +38,22 @@ def get_state_name(state_data): return "[" + inner + "]" return str(state_data) + + @abc.abstractmethod + def iter_states(self) -> Iterable[Any]: + """Iterate over all states in the automaton.""" + + @abc.abstractmethod + def iter_transitions(self) -> Iterable[tuple[Any, Any, Any]]: + """ + Iterate over all transitions in the automaton. Each transition is a tuple + of the form (from_state, to_state, symbol) + """ + + @abc.abstractmethod + def is_accepted(self, state) -> bool: + """Check if a state is an accepting state.""" + + @abc.abstractmethod + def is_initial(self, state) -> bool: + """Check if a state is an initial state.""" diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 2bdb5b4a..e1ca5611 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -356,3 +356,19 @@ def show_diagram(self, path=None, show_None=True): if path: graph.write_png(path) return graph + + def iter_transitions(self): + return ( + (from_, to_, symbol) + + for from_, lookup in self.transitions.items() + for to_, symbol in lookup.items() + + if symbol is not None + ) + + def is_accepted(self, state): + return state == self.final_state + + def is_initial(self, state): + return state == self.initial_state diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index ad03bedc..cf62d067 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -1024,3 +1024,21 @@ def add_any_transition(start_state_dict, end_state): initial_state=(0, 0), final_states=final_states, ) + + def iter_states(self): + return iter(self.states) + + def iter_transitions(self): + return ( + (from_, to_, symbol) + + for from_, lookup in self.transitions.items() + for symbol, to_lookup in lookup.items() + for to_ in to_lookup + ) + + def is_accepted(self, state): + return state in self.final_states + + def is_initial(self, state): + return state == self.initial_state From ae0d99634a968d6b3850724486ccbb336fa019a9 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Fri, 17 Mar 2023 22:02:28 +0330 Subject: [PATCH 04/83] Implemented graph visualization --- automata/fa/dfa.py | 37 ---------- automata/fa/fa.py | 163 ++++++++++++++++++++++++++++++++++++++++++-- automata/fa/gnfa.py | 39 ----------- automata/fa/nfa.py | 40 ----------- 4 files changed, 159 insertions(+), 120 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 0b24c7e5..5cf0634b 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1553,49 +1553,12 @@ def get_name_original(states: FrozenSet[DFAStateT]) -> DFAStateT: final_states=dfa_final_states, ) - def show_diagram(self, path=None): - """ - Creates the graph associated with this DFA - """ - # Nodes are set of states - - graph = Dot(graph_type="digraph", rankdir="LR") - nodes = {} - for state in self.states: - if state == self.initial_state: - # color start state with green - if state in self.final_states: - initial_state_node = Node( - state, style="filled", peripheries=2, fillcolor="#66cc33" - ) - else: - initial_state_node = Node( - state, style="filled", fillcolor="#66cc33" - ) - nodes[state] = initial_state_node - graph.add_node(initial_state_node) - else: - if state in self.final_states: - state_node = Node(state, peripheries=2) - else: - state_node = Node(state) - nodes[state] = state_node - graph.add_node(state_node) - # adding edges - for from_state, lookup in self.transitions.items(): - for to_label, to_state in lookup.items(): - graph.add_edge(Edge(nodes[from_state], nodes[to_state], label=to_label)) - if path: - graph.write_png(path) - return graph - def iter_states(self): return iter(self.states) def iter_transitions(self): return ( (from_, to_, symbol) - for from_, lookup in self.transitions.items() for symbol, to_ in lookup.items() ) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index a94c86e4..d5359eb2 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -2,13 +2,89 @@ """Classes and methods for working with all finite automata.""" import abc +import pathlib +import typing +import uuid +from collections import defaultdict from typing import Any, Iterable +import pydot + from automata.base.automaton import Automaton, AutomatonStateT FAStateT = AutomatonStateT +class IpythonGraph: + def __init__(self, graph): + self.graph = graph + + def _repr_mimebundle_( + self, + include: typing.Optional[typing.Iterable[str]] = None, + exclude: typing.Optional[typing.Iterable[str]] = None, + **_, + ) -> typing.Dict[str, typing.Union[bytes, str]]: + mime_method = { + # 'image/jpeg': self._repr_image_jpeg, + # 'image/png': self._repr_image_png, + "image/svg+xml": self._repr_image_svg_xml, + } + + default_mime_types = {"image/svg+xml"} + + include = set(include) if include is not None else default_mime_types + include -= set(exclude or []) + return {mimetype: mime_method[mimetype]() for mimetype in include} + + def _repr_image_jpeg(self): + """Return the rendered graph as JPEG bytes.""" + return self.graph.create(format="jpeg") + + def _repr_image_png(self): + """Return the rendered graph as PNG bytes.""" + return self.graph.create(format="png") + + def _repr_image_svg_xml(self): + """Return the rendered graph as SVG string.""" + return self.graph.create(format="svg", encoding="utf-8") + + +class IpythonGraph: + def __init__(self, graph): + self.graph = graph + + def _repr_mimebundle_( + self, + include: typing.Optional[typing.Iterable[str]] = None, + exclude: typing.Optional[typing.Iterable[str]] = None, + **_, + ) -> typing.Dict[str, typing.Union[bytes, str]]: + mime_method = { + # 'image/jpeg': self._repr_image_jpeg, + # 'image/png': self._repr_image_png, + "image/svg+xml": self._repr_image_svg_xml, + } + + default_mime_types = {"image/svg+xml"} + + include = set(include) if include is not None else default_mime_types + include -= set(exclude or []) + return {mimetype: mime_method[mimetype]() for mimetype in include} + + def _repr_image_jpeg(self): + """Return the rendered graph as JPEG bytes.""" + return self.graph.create(format="jpeg") + + def _repr_image_png(self): + """Return the rendered graph as PNG bytes.""" + return self.graph.create(format="png") + + def _repr_image_svg_xml(self): + """Return the rendered graph as SVG string.""" + return self.graph.create(format="svg", encoding="utf-8") + + class FA(Automaton, metaclass=abc.ABCMeta): """An abstract base class for finite automata.""" @@ -40,20 +116,99 @@ def get_state_name(state_data): return str(state_data) @abc.abstractmethod - def iter_states(self) -> Iterable[Any]: + def iter_states(self): """Iterate over all states in the automaton.""" @abc.abstractmethod - def iter_transitions(self) -> Iterable[tuple[Any, Any, Any]]: + def iter_transitions(self): """ Iterate over all transitions in the automaton. Each transition is a tuple of the form (from_state, to_state, symbol) """ @abc.abstractmethod - def is_accepted(self, state) -> bool: + def is_accepted(self, state): """Check if a state is an accepting state.""" @abc.abstractmethod - def is_initial(self, state) -> bool: + def is_initial(self, state): """Check if a state is an initial state.""" + + def show_diagram(self, path=None, format=None, prog=None, rankdir="LR"): + """ + Creates the graph associated with this FA. + """ + graph = pydot.Dot(prog=prog, rankdir=rankdir) + + state_nodes = [] + for state in self.iter_states(): + shape = "doublecircle" if self.is_accepted(state) else "circle" + node = pydot.Node(self.get_state_name(state), shape=shape) + # we append the node to a list so that we can add all null nodes to + # the graph before adding any edges. + state_nodes.append(node) + + # every edge needs an origin node, so we add a null node for every + # initial state. This is a hack to make the graph look nicer with + # arrows that appear to come from nowhere. + if self.is_initial(state): + # we use a random uuid to make sure that the null node has a + # unique id to avoid colliding with other states and null_nodes. + null_node = pydot.Node( + str(uuid.uuid4()), + label="", + shape="none", + width="0", + height="0", + ) + graph.add_node(null_node) + graph.add_edge(pydot.Edge(null_node, node)) + + # add all the nodes to the graph + # we do this after adding all the null nodes so that the null nodes + # appear preferably at left of the diagram, or at any direction that is + # specified by the rankdir + for node in state_nodes: + graph.add_node(node) + + # there can be multiple transitions between two states, so we need to + # group them together and create a single edge with a label that + # contains all the symbols. + edge_labels = defaultdict(list) + for from_state, to_state, symbol in self.iter_transitions(): + label = "ε" if symbol == "" else str(symbol) + from_node = self.get_state_name(from_state) + to_node = self.get_state_name(to_state) + edge_labels[from_node, to_node].append(label) + + for (from_node, to_node), labels in edge_labels.items(): + label = ",".join(labels) + edge = pydot.Edge( + from_node, + to_node, + label='"' + label + '"', + ) + graph.add_edge(edge) + + if path is not None: + path = pathlib.Path(path) + # if the format is not specified, try to infer it from the file extension + if format is None: + # get the file extension without the leading "." + # e.g. ".png" -> "png" + file_format = path.suffix.split(".")[-1] + if file_format in graph.formats: + format = file_format + + graph.write(path, format=format) + + return graph + + def _ipython_display_(self): + """ + Display the graph associated with this FA in Jupyter notebooks + """ + from IPython.display import display + + graph = self.show_diagram() + display(IpythonGraph(graph)) diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index e1ca5611..20dfd4ab 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -320,50 +320,11 @@ def option(self): def reverse(self): raise NotImplementedError - def show_diagram(self, path=None, show_None=True): - """ - Creates the graph associated with this DFA - """ - # Nodes are set of states - - graph = Dot(graph_type="digraph", rankdir="LR") - nodes = {} - for state in self.states: - if state == self.initial_state: - # color start state with green - initial_state_node = Node(state, style="filled", fillcolor="#66cc33") - nodes[state] = initial_state_node - graph.add_node(initial_state_node) - else: - if state == self.final_state: - state_node = Node(state, peripheries=2) - else: - state_node = Node(state) - nodes[state] = state_node - graph.add_node(state_node) - # adding edges - for from_state, lookup in self.transitions.items(): - for to_state, to_label in lookup.items(): # pragma: no branch - if to_label is None and show_None: - to_label = "ø" - graph.add_edge( - Edge(nodes[from_state], nodes[to_state], label=to_label) - ) - elif to_label is not None: - graph.add_edge( - Edge(nodes[from_state], nodes[to_state], label=to_label) - ) - if path: - graph.write_png(path) - return graph - def iter_transitions(self): return ( (from_, to_, symbol) - for from_, lookup in self.transitions.items() for to_, symbol in lookup.items() - if symbol is not None ) diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index cf62d067..852017f7 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -812,45 +812,6 @@ def left_quotient(self, other: NFA) -> Self: final_states=new_final_states, ) - def show_diagram(self, path=None): - """ - Creates the graph associated with this DFA - """ - # Nodes are set of states - - graph = Dot(graph_type="digraph", rankdir="LR") - nodes = {} - for state in self.states: - if state == self.initial_state: - # color start state with green - if state in self.final_states: - initial_state_node = Node( - state, style="filled", peripheries=2, fillcolor="#66cc33" - ) - else: - initial_state_node = Node( - state, style="filled", fillcolor="#66cc33" - ) - nodes[state] = initial_state_node - graph.add_node(initial_state_node) - else: - if state in self.final_states: - state_node = Node(state, peripheries=2) - else: - state_node = Node(state) - nodes[state] = state_node - graph.add_node(state_node) - # adding edges - for from_state, lookup in self.transitions.items(): - for to_label, to_states in lookup.items(): - for to_state in to_states: - graph.add_edge( - Edge(nodes[from_state], nodes[to_state], label=to_label) - ) - if path: - graph.write_png(path) - return graph - @staticmethod def _load_new_transition_dict( state_map_dict: Mapping[NFAStateT, NFAStateT], @@ -1031,7 +992,6 @@ def iter_states(self): def iter_transitions(self): return ( (from_, to_, symbol) - for from_, lookup in self.transitions.items() for symbol, to_lookup in lookup.items() for to_ in to_lookup From 4ca70670c2637f7b8955b7d7a7d174a7af9ea923 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Fri, 17 Mar 2023 22:49:05 +0330 Subject: [PATCH 05/83] Fixed svg display bug --- automata/fa/fa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index d5359eb2..076b56f5 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -82,7 +82,7 @@ def _repr_image_png(self): def _repr_image_svg_xml(self): """Return the rendered graph as SVG string.""" - return self.graph.create(format="svg", encoding="utf-8") + return self.graph.create(format="svg").decode() class FA(Automaton, metaclass=abc.ABCMeta): From ae19ccbd646733a16a79e7645f16dff2a3dd9794 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Sat, 18 Mar 2023 00:20:29 +0330 Subject: [PATCH 06/83] Removed IpythonGraph class --- automata/fa/fa.py | 106 ++++++++++++---------------------------------- 1 file changed, 28 insertions(+), 78 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 076b56f5..0dca69e1 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -6,7 +6,6 @@ import typing import uuid from collections import defaultdict -from typing import Any, Iterable import pydot @@ -15,76 +14,6 @@ FAStateT = AutomatonStateT -class IpythonGraph: - def __init__(self, graph): - self.graph = graph - - def _repr_mimebundle_( - self, - include: typing.Optional[typing.Iterable[str]] = None, - exclude: typing.Optional[typing.Iterable[str]] = None, - **_, - ) -> typing.Dict[str, typing.Union[bytes, str]]: - mime_method = { - # 'image/jpeg': self._repr_image_jpeg, - # 'image/png': self._repr_image_png, - "image/svg+xml": self._repr_image_svg_xml, - } - - default_mime_types = {"image/svg+xml"} - - include = set(include) if include is not None else default_mime_types - include -= set(exclude or []) - return {mimetype: mime_method[mimetype]() for mimetype in include} - - def _repr_image_jpeg(self): - """Return the rendered graph as JPEG bytes.""" - return self.graph.create(format="jpeg") - - def _repr_image_png(self): - """Return the rendered graph as PNG bytes.""" - return self.graph.create(format="png") - - def _repr_image_svg_xml(self): - """Return the rendered graph as SVG string.""" - return self.graph.create(format="svg", encoding="utf-8") - - -class IpythonGraph: - def __init__(self, graph): - self.graph = graph - - def _repr_mimebundle_( - self, - include: typing.Optional[typing.Iterable[str]] = None, - exclude: typing.Optional[typing.Iterable[str]] = None, - **_, - ) -> typing.Dict[str, typing.Union[bytes, str]]: - mime_method = { - # 'image/jpeg': self._repr_image_jpeg, - # 'image/png': self._repr_image_png, - "image/svg+xml": self._repr_image_svg_xml, - } - - default_mime_types = {"image/svg+xml"} - - include = set(include) if include is not None else default_mime_types - include -= set(exclude or []) - return {mimetype: mime_method[mimetype]() for mimetype in include} - - def _repr_image_jpeg(self): - """Return the rendered graph as JPEG bytes.""" - return self.graph.create(format="jpeg") - - def _repr_image_png(self): - """Return the rendered graph as PNG bytes.""" - return self.graph.create(format="png") - - def _repr_image_svg_xml(self): - """Return the rendered graph as SVG string.""" - return self.graph.create(format="svg").decode() - - class FA(Automaton, metaclass=abc.ABCMeta): """An abstract base class for finite automata.""" @@ -204,11 +133,32 @@ def show_diagram(self, path=None, format=None, prog=None, rankdir="LR"): return graph - def _ipython_display_(self): - """ - Display the graph associated with this FA in Jupyter notebooks - """ - from IPython.display import display + def _repr_mimebundle_( + self, + include: typing.Optional[typing.Iterable[str]] = None, + exclude: typing.Optional[typing.Iterable[str]] = None, + **_, + ) -> typing.Dict[str, typing.Union[bytes, str]]: + mime_method = { + "image/jpeg": self._repr_image_jpeg, + "image/png": self._repr_image_png, + "image/svg+xml": self._repr_image_svg_xml, + } - graph = self.show_diagram() - display(IpythonGraph(graph)) + default_mime_types = {"image/svg+xml"} + + include = set(include) if include is not None else default_mime_types + include -= set(exclude or []) + return {mimetype: mime_method[mimetype]() for mimetype in include} + + def _repr_image_jpeg(self): + """Return the rendered graph as JPEG bytes.""" + return self.show_diagram().create(format="jpeg") + + def _repr_image_png(self): + """Return the rendered graph as PNG bytes.""" + return self.show_diagram().create(format="png") + + def _repr_image_svg_xml(self): + """Return the rendered graph as SVG string.""" + return self.show_diagram().create(format="svg").decode() From 86a8d8cc20c246c126d67f962f82e42166d93902 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Sat, 18 Mar 2023 00:21:50 +0330 Subject: [PATCH 07/83] Fixed repr error for frozenset states --- automata/base/automaton.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 7f7332ab..9d680e50 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -124,29 +124,10 @@ def copy(self) -> Self: """Create a deep copy of the automaton.""" return self.__class__(**self.input_parameters) - # Format the given value for string output via repr() or str(); this exists - # for the purpose of displaying - - def _get_repr_friendly_value(self, value: Any) -> Any: - """ - A helper function to convert the given value / structure into a fully - mutable one by recursively processing said structure and any of its - members, unfreezing them along the way - """ - if isinstance(value, frozenset): - return {self._get_repr_friendly_value(element) for element in value} - elif isinstance(value, frozendict): - return { - dict_key: self._get_repr_friendly_value(dict_value) - for dict_key, dict_value in value.items() - } - else: - return value - def __repr__(self) -> str: """Return a string representation of the automaton.""" values = ", ".join( - f"{attr_name}={self._get_repr_friendly_value(attr_value)!r}" + f"{attr_name}={attr_value!r}" for attr_name, attr_value in self.input_parameters.items() ) return f"{self.__class__.__qualname__}({values})" From f9e2295973bf0da9f385442651351bfa8533dd21 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Sat, 18 Mar 2023 00:40:37 +0330 Subject: [PATCH 08/83] Fixed `null_node` edge tooltip --- automata/fa/fa.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 0dca69e1..6ddc090a 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -72,7 +72,8 @@ def show_diagram(self, path=None, format=None, prog=None, rankdir="LR"): state_nodes = [] for state in self.iter_states(): shape = "doublecircle" if self.is_accepted(state) else "circle" - node = pydot.Node(self.get_state_name(state), shape=shape) + node_label = self.get_state_name(state) + node = pydot.Node(node_label, shape=shape) # we append the node to a list so that we can add all null nodes to # the graph before adding any edges. state_nodes.append(node) @@ -91,7 +92,8 @@ def show_diagram(self, path=None, format=None, prog=None, rankdir="LR"): height="0", ) graph.add_node(null_node) - graph.add_edge(pydot.Edge(null_node, node)) + edge_label = "->" + node_label + graph.add_edge(pydot.Edge(null_node, node, tooltip=edge_label)) # add all the nodes to the graph # we do this after adding all the null nodes so that the null nodes From 6deff5d056e25cdcca46cf6bc392f1f68e9d2902 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Tue, 21 Mar 2023 20:12:44 +0330 Subject: [PATCH 09/83] Removed unused imports --- automata/fa/dfa.py | 5 +---- automata/fa/fa.py | 5 +++-- automata/fa/gnfa.py | 1 - automata/fa/nfa.py | 1 - 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 5cf0634b..d10b8be4 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -17,18 +17,15 @@ Iterable, Iterator, List, - Literal, Mapping, Optional, Set, Tuple, Type, - TypeVar, cast, ) import networkx as nx -from pydot import Dot, Edge, Node from typing_extensions import Self import automata.base.exceptions as exceptions @@ -332,7 +329,7 @@ def minify(self, retain_names: bool = False) -> Self: Create a minimal DFA which accepts the same inputs as this DFA. First, non-reachable states are removed. - Then, similiar states are merged using Hopcroft's Algorithm. + Then, similar states are merged using Hopcroft's Algorithm. retain_names: If True, merged states retain names. If False, new states will be named 0, ..., n-1. """ diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 6ddc090a..67f81b64 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -67,7 +67,7 @@ def show_diagram(self, path=None, format=None, prog=None, rankdir="LR"): """ Creates the graph associated with this FA. """ - graph = pydot.Dot(prog=prog, rankdir=rankdir) + graph = pydot.Dot(rankdir=rankdir) state_nodes = [] for state in self.iter_states(): @@ -131,7 +131,8 @@ def show_diagram(self, path=None, format=None, prog=None, rankdir="LR"): if file_format in graph.formats: format = file_format - graph.write(path, format=format) + path.parent.mkdir(parents=True, exist_ok=True) + graph.write(path, format=format, prog=prog) return graph diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 20dfd4ab..051df97a 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -5,7 +5,6 @@ from itertools import product from frozendict import frozendict -from pydot import Dot, Edge, Node import automata.base.exceptions as exceptions import automata.fa.fa as fa diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 852017f7..8ecc40b6 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -23,7 +23,6 @@ import networkx as nx from frozendict import frozendict -from pydot import Dot, Edge, Node from typing_extensions import Self import automata.base.exceptions as exceptions From 3eed0e4897733167125395504fbbe2b3ed36654f Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Sun, 26 Mar 2023 20:07:11 +0330 Subject: [PATCH 10/83] Added basic visualizations from Visual Automata --- automata/fa/dfa.py | 2 +- automata/fa/fa.py | 255 +++++++++++++++++++++++++++++--------------- automata/fa/gnfa.py | 2 +- automata/fa/nfa.py | 2 +- pyproject.toml | 4 +- requirements.txt | 6 +- setup.py | 1 - 7 files changed, 177 insertions(+), 95 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index d10b8be4..14b5d09c 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1560,7 +1560,7 @@ def iter_transitions(self): for symbol, to_ in lookup.items() ) - def is_accepted(self, state): + def is_accepting(self, state): return state in self.final_states def is_initial(self, state): diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 67f81b64..05ab9910 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -2,12 +2,15 @@ """Classes and methods for working with all finite automata.""" import abc +import itertools +import os import pathlib import typing import uuid from collections import defaultdict -import pydot +import graphviz +from coloraide import Color from automata.base.automaton import Automaton, AutomatonStateT @@ -56,112 +59,188 @@ def iter_transitions(self): """ @abc.abstractmethod - def is_accepted(self, state): + def is_accepting(self, state): """Check if a state is an accepting state.""" @abc.abstractmethod def is_initial(self, state): """Check if a state is an initial state.""" - def show_diagram(self, path=None, format=None, prog=None, rankdir="LR"): + def show( + self, + input_str: str | None = None, + save_path: str | os.PathLike | None = None, + *, + engine: typing.Optional[str] = None, + view=False, + cleanup: bool = True, + horizontal: bool = True, + reverse_orientation: bool = False, + fig_size: tuple = (8, 8), + font_size: float = 14.0, + arrow_size: float = 0.85, + state_separation: float = 0.5, + ): """ - Creates the graph associated with this FA. + Generates the graph associated with the given DFA. + Args: + input_str (str, optional): String list of input symbols. Defaults to None. + save_path (str or os.PathLike, optional): Path to output file. If None, the output will not be saved. + path (str, optional): Folder path for output file. Defaults to None. + view (bool, optional): Storing and displaying the graph as a pdf. Defaults to False. + cleanup (bool, optional): Garbage collection. Defaults to True. + horizontal (bool, optional): Direction of node layout. Defaults to True. + reverse_orientation (bool, optional): Reverse direction of node layout. Defaults to False. + fig_size (tuple, optional): Figure size. Defaults to (8, 8). + font_size (float, optional): Font size. Defaults to 14.0. + arrow_size (float, optional): Arrow head size. Defaults to 0.85. + state_separation (float, optional): Node distance. Defaults to 0.5. + Returns: + Digraph: The graph in dot format. """ - graph = pydot.Dot(rankdir=rankdir) + # Converting to graphviz preferred input type, + # keeping the conventional input styles; i.e fig_size(8,8) + # TODO why is (8, 8) the "conventional" size? + fig_size = ", ".join(map(str, fig_size)) + font_size = str(font_size) + arrow_size = str(arrow_size) + state_separation = str(state_separation) + + # Defining the graph. + graph = graphviz.Digraph(strict=False, engine=engine) + graph.attr( + # size=fig_size, # TODO maybe needed? + ranksep=state_separation, + ) + if horizontal: + graph.attr(rankdir="LR") + if reverse_orientation: + if horizontal: + graph.attr(rankdir="RL") + else: + graph.attr(rankdir="BT") - state_nodes = [] for state in self.iter_states(): - shape = "doublecircle" if self.is_accepted(state) else "circle" - node_label = self.get_state_name(state) - node = pydot.Node(node_label, shape=shape) - # we append the node to a list so that we can add all null nodes to - # the graph before adding any edges. - state_nodes.append(node) - # every edge needs an origin node, so we add a null node for every - # initial state. This is a hack to make the graph look nicer with - # arrows that appear to come from nowhere. + # initial state. if self.is_initial(state): # we use a random uuid to make sure that the null node has a # unique id to avoid colliding with other states and null_nodes. - null_node = pydot.Node( - str(uuid.uuid4()), + null_node = str(uuid.uuid4()) + graph.node( + null_node, label="", - shape="none", - width="0", - height="0", + shape="point", + fontsize=font_size, + ) + node = self.get_state_name(state) + edge_label = "->" + node + graph.edge( + null_node, + node, + tooltip=edge_label, + arrowsize=arrow_size, + ) + + for state in self.iter_states(): + shape = "doublecircle" if self.is_accepting(state) else "circle" + node = self.get_state_name(state) + graph.node(node, shape=shape, fontsize=font_size) + + if input_str is None: + edge_labels = defaultdict(list) + for from_state, to_state, symbol in self.iter_transitions(): + # TODO only do this for NFA + label = "ε" if symbol == "" else str(symbol) + from_node = self.get_state_name(from_state) + to_node = self.get_state_name(to_state) + edge_labels[from_node, to_node].append(label) + + for (from_node, to_node), labels in edge_labels.items(): + label = ",".join(sorted(labels)) + graph.edge( + from_node, + to_node, + label=label, + arrowsize=arrow_size, + fontsize=font_size, ) - graph.add_node(null_node) - edge_label = "->" + node_label - graph.add_edge(pydot.Edge(null_node, node, tooltip=edge_label)) - - # add all the nodes to the graph - # we do this after adding all the null nodes so that the null nodes - # appear preferably at left of the diagram, or at any direction that is - # specified by the rankdir - for node in state_nodes: - graph.add_node(node) - - # there can be multiple transitions between two states, so we need to - # group them together and create a single edge with a label that - # contains all the symbols. - edge_labels = defaultdict(list) - for from_state, to_state, symbol in self.iter_transitions(): - label = "ε" if symbol == "" else str(symbol) - from_node = self.get_state_name(from_state) - to_node = self.get_state_name(to_state) - edge_labels[from_node, to_node].append(label) - - for (from_node, to_node), labels in edge_labels.items(): - label = ",".join(labels) - edge = pydot.Edge( - from_node, - to_node, - label='"' + label + '"', + else: + # TODO + raise NotImplementedError("input_str is not yet supported yet") + + status, taken_transitions_pairs, taken_steps = self.input_check( + input_str=input_str, return_result=True + ) + remaining_transitions_pairs = [ + x for x in all_transitions_pairs if x not in taken_transitions_pairs + ] + + start_color = Color("#FFFF00") + end_color = Color("#00FF00") if status else Color("#FF0000") + + number_of_colors = len(input_str) + interpolation = Color.interpolate([start_color, end_color], space="lch") + # TODO why cycle? + color_gen = itertools.cycle( + interpolation(x / number_of_colors) for x in range(number_of_colors + 1) ) - graph.add_edge(edge) - if path is not None: - path = pathlib.Path(path) - # if the format is not specified, try to infer it from the file extension - if format is None: - # get the file extension without the leading "." - # e.g. ".png" -> "png" - file_format = path.suffix.split(".")[-1] - if file_format in graph.formats: - format = file_format + # Define all transitions in the finite state machine with traversal. + counter = 0 + for pair in taken_transitions_pairs: + counter += 1 + edge_color = next(color_gen) + graph.edge( + pair[0], + pair[1], + label=" [{}]\n{} ".format(counter, pair[2]), + arrowsize=arrow_size, + fontsize=font_size, + color=edge_color.to_string(hex=True), + penwidth="2.5", + ) - path.parent.mkdir(parents=True, exist_ok=True) - graph.write(path, format=format, prog=prog) + for pair in remaining_transitions_pairs: + graph.edge( + pair[0], + pair[1], + label=" {} ".format(pair[2]), + arrowsize=arrow_size, + fontsize=font_size, + ) + + # TODO find a better way to handle the display of the steps + from IPython.display import display + + display(taken_steps) + + # Write diagram to file. PNG, SVG, etc. + if save_path is not None: + save_path: pathlib.Path = pathlib.Path(save_path) + + directory = save_path.parent + directory.mkdir(parents=True, exist_ok=True) + filename = save_path.stem + format = save_path.suffix.split(".")[1] if save_path.suffix else None + + graph.render( + directory=directory, + filename=filename, + format=format, + cleanup=cleanup, + view=view, + ) + + elif view: + graph.render(view=True) return graph - def _repr_mimebundle_( - self, - include: typing.Optional[typing.Iterable[str]] = None, - exclude: typing.Optional[typing.Iterable[str]] = None, - **_, - ) -> typing.Dict[str, typing.Union[bytes, str]]: - mime_method = { - "image/jpeg": self._repr_image_jpeg, - "image/png": self._repr_image_png, - "image/svg+xml": self._repr_image_svg_xml, - } - - default_mime_types = {"image/svg+xml"} - - include = set(include) if include is not None else default_mime_types - include -= set(exclude or []) - return {mimetype: mime_method[mimetype]() for mimetype in include} - - def _repr_image_jpeg(self): - """Return the rendered graph as JPEG bytes.""" - return self.show_diagram().create(format="jpeg") - - def _repr_image_png(self): - """Return the rendered graph as PNG bytes.""" - return self.show_diagram().create(format="png") - - def _repr_image_svg_xml(self): - """Return the rendered graph as SVG string.""" - return self.show_diagram().create(format="svg").decode() + def _ipython_display_(self): + # IPython is imported here because this function is only called by + # IPython. So if IPython is not installed, this function will not be + # called, therefore no need to add ipython as dependency. + from IPython.display import display + + display(self.show()) diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 051df97a..5480a4c7 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -327,7 +327,7 @@ def iter_transitions(self): if symbol is not None ) - def is_accepted(self, state): + def is_accepting(self, state): return state == self.final_state def is_initial(self, state): diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 8ecc40b6..3bf45156 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -996,7 +996,7 @@ def iter_transitions(self): for to_ in to_lookup ) - def is_accepted(self, state): + def is_accepting(self, state): return state in self.final_states def is_initial(self, state): diff --git a/pyproject.toml b/pyproject.toml index cb52f407..766e88db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,9 @@ maintainers = [ {name = 'Caleb Evans', email = 'caleb@calebevans.me'} ] dependencies = [ - "pydot>=1.4.2", + "coloraide>=1.8.2", + "frozendict>=2.3.4", + "graphviz>=0.20.1", "networkx>=2.6.2", "frozendict>=2.3.4", #TODO I think that typing extensions needs to be in here ] diff --git a/requirements.txt b/requirements.txt index 411b6c03..eee6224e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,17 @@ black==23.3.0 click==8.1.3 +coloraide==1.8.2 coverage==6.4.4 -flake8==6.0.0 flake8-black==0.3.6 flake8-isort==6.0.0 Flake8-pyproject==1.2.3 +flake8==6.0.0 frozendict==2.3.4 +graphviz==0.20.1 isort==5.10.1 mccabe==0.7.0 -mypy==1.1.1 mypy-extensions==1.0.0 +mypy==1.1.1 networkx==2.6.2 nose2==0.12.0 packaging==23.0 diff --git a/setup.py b/setup.py index 26e08e48..7f1a1763 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,4 @@ from setuptools import setup - if __name__ == "__main__": setup() From b73ef8dafef1acb74b8c32fc30724e0dee5993ff Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Fri, 31 Mar 2023 12:24:40 +0330 Subject: [PATCH 11/83] Replaced " with ' --- automata/fa/fa.py | 8 ++++---- setup.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 05ab9910..c18b0b1b 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -66,7 +66,7 @@ def is_accepting(self, state): def is_initial(self, state): """Check if a state is an initial state.""" - def show( + def show_diagram( self, input_str: str | None = None, save_path: str | os.PathLike | None = None, @@ -99,7 +99,7 @@ def show( Digraph: The graph in dot format. """ # Converting to graphviz preferred input type, - # keeping the conventional input styles; i.e fig_size(8,8) + # keeping the conventional input styles; i.e fig_size(8, 8) # TODO why is (8, 8) the "conventional" size? fig_size = ", ".join(map(str, fig_size)) font_size = str(font_size) @@ -167,7 +167,7 @@ def show( ) else: # TODO - raise NotImplementedError("input_str is not yet supported yet") + raise NotImplementedError("input_str is not supported yet") status, taken_transitions_pairs, taken_steps = self.input_check( input_str=input_str, return_result=True @@ -243,4 +243,4 @@ def _ipython_display_(self): # called, therefore no need to add ipython as dependency. from IPython.display import display - display(self.show()) + display(self.show_diagram()) diff --git a/setup.py b/setup.py index 7f1a1763..6b40b52b 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ from setuptools import setup -if __name__ == "__main__": +if __name__ == '__main__': setup() From 6b91b7ccea00e263f38352165f6eabb4f9eaf398 Mon Sep 17 00:00:00 2001 From: Caleb Evans Date: Sat, 1 Apr 2023 12:16:59 -0700 Subject: [PATCH 12/83] Sort dependencies in requirements.txt Using `pip freeze > requirements.txt`. --- requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index eee6224e..9cc8f1e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,5 +21,3 @@ pycodestyle==2.10.0 pydot==1.4.2 pyflakes==3.0.1 pyparsing==3.0.9 -types-frozendict==2.0.9 -typing_extensions==4.5.0 From 720b71b527af306046899f770b9f26673da76296 Mon Sep 17 00:00:00 2001 From: Caleb Evans Date: Sat, 1 Apr 2023 12:20:57 -0700 Subject: [PATCH 13/83] Update remaining files to comply with flake8/black --- automata/base/automaton.py | 1 - automata/fa/fa.py | 30 +++++++++++++++++++----------- setup.py | 2 +- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 9d680e50..0581915f 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -4,7 +4,6 @@ import abc from typing import AbstractSet, Any, Dict, Generator, Mapping, NoReturn, Tuple -from frozendict import frozendict from typing_extensions import Self import automata.base.config as global_config diff --git a/automata/fa/fa.py b/automata/fa/fa.py index c18b0b1b..2752766d 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -85,16 +85,22 @@ def show_diagram( Generates the graph associated with the given DFA. Args: input_str (str, optional): String list of input symbols. Defaults to None. - save_path (str or os.PathLike, optional): Path to output file. If None, the output will not be saved. - path (str, optional): Folder path for output file. Defaults to None. - view (bool, optional): Storing and displaying the graph as a pdf. Defaults to False. - cleanup (bool, optional): Garbage collection. Defaults to True. - horizontal (bool, optional): Direction of node layout. Defaults to True. - reverse_orientation (bool, optional): Reverse direction of node layout. Defaults to False. - fig_size (tuple, optional): Figure size. Defaults to (8, 8). - font_size (float, optional): Font size. Defaults to 14.0. - arrow_size (float, optional): Arrow head size. Defaults to 0.85. - state_separation (float, optional): Node distance. Defaults to 0.5. + - save_path (str or os.PathLike, optional): Path to output file. If + None, the output will not be saved. + - path (str, optional): Folder path for output file. Defaults to + None. + - view (bool, optional): Storing and displaying the graph as a pdf. + Defaults to False. + - cleanup (bool, optional): Garbage collection. Defaults to True. + horizontal (bool, optional): Direction of node layout. Defaults + to True. + - reverse_orientation (bool, optional): Reverse direction of node + layout. Defaults to False. + - fig_size (tuple, optional): Figure size. Defaults to (8, 8). + - font_size (float, optional): Font size. Defaults to 14.0. + - arrow_size (float, optional): Arrow head size. Defaults to 0.85. + - state_separation (float, optional): Node distance. Defaults to 0 + 5. Returns: Digraph: The graph in dot format. """ @@ -173,7 +179,9 @@ def show_diagram( input_str=input_str, return_result=True ) remaining_transitions_pairs = [ - x for x in all_transitions_pairs if x not in taken_transitions_pairs + x + for x in all_transitions_pairs # noqa + if x not in taken_transitions_pairs ] start_color = Color("#FFFF00") diff --git a/setup.py b/setup.py index 6b40b52b..7f1a1763 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ from setuptools import setup -if __name__ == '__main__': +if __name__ == "__main__": setup() From 5dd6474b7f0b9e2c5522fca7e2f66bf87f9c5e62 Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Fri, 7 Apr 2023 18:18:32 +0330 Subject: [PATCH 14/83] Using null set symbol for empty sets --- automata/fa/fa.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 2752766d..0dd0038a 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -37,7 +37,10 @@ def get_state_name(state_data): if isinstance(state_data, (set, frozenset, list, tuple)): inner = ", ".join(FA.get_state_name(sub_data) for sub_data in state_data) if isinstance(state_data, (set, frozenset)): - return "{" + inner + "}" + if state_data: + return "{" + inner + "}" + else: + return "∅" if isinstance(state_data, tuple): return "(" + inner + ")" From 1f5bcd9ce1814d9d17c15cd63a956d17dd88f430 Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Fri, 7 Apr 2023 18:19:32 +0330 Subject: [PATCH 15/83] Added step visualization for DFA --- automata/fa/dfa.py | 28 +++++++++ automata/fa/fa.py | 147 +++++++++++++++++++++------------------------ automata/fa/nfa.py | 3 + 3 files changed, 99 insertions(+), 79 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 14b5d09c..3a0953d4 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1565,3 +1565,31 @@ def is_accepting(self, state): def is_initial(self, state): return state == self.initial_state + + def _get_input_path(self, input_str): + """ + Calculate the path taken by input. + + Args: + input_str (str): The input string to run on the DFA. + + Returns: + tuple[list[list[tuple[str, str, str]], bool]: A 2d list of all + transitions taken in each step and a boolean indicating whether the + DFA accepted the input. + + """ + state_history = [ + state + for state in self.read_input_stepwise(input_str, ignore_rejection=True) + ] + + path = [ + [transition] + for transition in zip(state_history, state_history[1:], input_str) + ] + + last_state = state_history[-1] if state_history else self.initial_state + is_final = last_state in self.final_states + + return path, is_final diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 0dd0038a..56249554 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -23,7 +23,7 @@ class FA(Automaton, metaclass=abc.ABCMeta): __slots__ = tuple() @staticmethod - def get_state_name(state_data): + def get_state_label(state_data): """ Get an string representation of a state. This is used for displaying and uses `str` for any unsupported python data types. @@ -35,7 +35,7 @@ def get_state_name(state_data): return state_data if isinstance(state_data, (set, frozenset, list, tuple)): - inner = ", ".join(FA.get_state_name(sub_data) for sub_data in state_data) + inner = ", ".join(FA.get_state_label(sub_data) for sub_data in state_data) if isinstance(state_data, (set, frozenset)): if state_data: return "{" + inner + "}" @@ -79,7 +79,7 @@ def show_diagram( cleanup: bool = True, horizontal: bool = True, reverse_orientation: bool = False, - fig_size: tuple = (8, 8), + fig_size: tuple = None, font_size: float = 14.0, arrow_size: float = 0.85, state_separation: float = 0.5, @@ -99,7 +99,7 @@ def show_diagram( to True. - reverse_orientation (bool, optional): Reverse direction of node layout. Defaults to False. - - fig_size (tuple, optional): Figure size. Defaults to (8, 8). + - fig_size (tuple, optional): Figure size. Defaults to None. - font_size (float, optional): Font size. Defaults to 14.0. - arrow_size (float, optional): Arrow head size. Defaults to 0.85. - state_separation (float, optional): Node distance. Defaults to 0 @@ -107,20 +107,18 @@ def show_diagram( Returns: Digraph: The graph in dot format. """ - # Converting to graphviz preferred input type, - # keeping the conventional input styles; i.e fig_size(8, 8) - # TODO why is (8, 8) the "conventional" size? - fig_size = ", ".join(map(str, fig_size)) - font_size = str(font_size) - arrow_size = str(arrow_size) - state_separation = str(state_separation) # Defining the graph. graph = graphviz.Digraph(strict=False, engine=engine) - graph.attr( - # size=fig_size, # TODO maybe needed? - ranksep=state_separation, - ) + + # TODO test fig_size + if fig_size is not None: + graph.attr(size=", ".join(map(str, fig_size))) + + graph.attr(ranksep=str(state_separation)) + font_size = str(font_size) + arrow_size = str(arrow_size) + if horizontal: graph.attr(rankdir="LR") if reverse_orientation: @@ -139,92 +137,75 @@ def show_diagram( graph.node( null_node, label="", + tooltip=".", shape="point", fontsize=font_size, ) - node = self.get_state_name(state) - edge_label = "->" + node + node = self.get_state_label(state) graph.edge( null_node, node, - tooltip=edge_label, + tooltip="->" + node, arrowsize=arrow_size, ) for state in self.iter_states(): shape = "doublecircle" if self.is_accepting(state) else "circle" - node = self.get_state_name(state) + node = self.get_state_label(state) graph.node(node, shape=shape, fontsize=font_size) - if input_str is None: - edge_labels = defaultdict(list) - for from_state, to_state, symbol in self.iter_transitions(): - # TODO only do this for NFA - label = "ε" if symbol == "" else str(symbol) - from_node = self.get_state_name(from_state) - to_node = self.get_state_name(to_state) - edge_labels[from_node, to_node].append(label) - - for (from_node, to_node), labels in edge_labels.items(): - label = ",".join(sorted(labels)) - graph.edge( - from_node, - to_node, - label=label, - arrowsize=arrow_size, - fontsize=font_size, - ) - else: - # TODO - raise NotImplementedError("input_str is not supported yet") - - status, taken_transitions_pairs, taken_steps = self.input_check( - input_str=input_str, return_result=True - ) - remaining_transitions_pairs = [ - x - for x in all_transitions_pairs # noqa - if x not in taken_transitions_pairs - ] + is_edge_drawn = defaultdict(lambda: False) + if input_str is not None: + path, is_accepted = self._get_input_path(input_str=input_str) start_color = Color("#FFFF00") - end_color = Color("#00FF00") if status else Color("#FF0000") + end_color = Color("#00FF00") if is_accepted else Color("#FF0000") number_of_colors = len(input_str) interpolation = Color.interpolate([start_color, end_color], space="lch") - # TODO why cycle? - color_gen = itertools.cycle( + colors = ( interpolation(x / number_of_colors) for x in range(number_of_colors + 1) ) # Define all transitions in the finite state machine with traversal. - counter = 0 - for pair in taken_transitions_pairs: - counter += 1 - edge_color = next(color_gen) - graph.edge( - pair[0], - pair[1], - label=" [{}]\n{} ".format(counter, pair[2]), - arrowsize=arrow_size, - fontsize=font_size, - color=edge_color.to_string(hex=True), - penwidth="2.5", - ) - - for pair in remaining_transitions_pairs: - graph.edge( - pair[0], - pair[1], - label=" {} ".format(pair[2]), - arrowsize=arrow_size, - fontsize=font_size, - ) - - # TODO find a better way to handle the display of the steps - from IPython.display import display - - display(taken_steps) + for transition_index, (color, transitions) in enumerate( + zip(colors, path), start=1 + ): + for from_state, to_state, symbol in transitions: + is_edge_drawn[from_state, to_state, symbol] = True + + from_node = self.get_state_label(from_state) + to_node = self.get_state_label(to_state) + label = self.get_edge_label(symbol) + graph.edge( + from_state, + to_state, + label=" [#{}]\n{} ".format(transition_index, symbol), + arrowsize=arrow_size, + fontsize=font_size, + color=color.to_string(hex=True), + penwidth="2.5", + ) + + edge_labels = defaultdict(list) + for from_state, to_state, symbol in self.iter_transitions(): + if is_edge_drawn[from_state, to_state, symbol]: + continue + + label = self.get_edge_label(symbol) + from_node = self.get_state_label(from_state) + to_node = self.get_state_label(to_state) + edge_labels[from_node, to_node].append(label) + + for (from_node, to_node), labels in edge_labels.items(): + label = ",".join(sorted(labels)) + graph.edge( + from_node, + to_node, + label=label, + arrowsize=arrow_size, + fontsize=font_size, + ) # Write diagram to file. PNG, SVG, etc. if save_path is not None: @@ -248,6 +229,14 @@ def show_diagram( return graph + def get_edge_label(self, symbol): + return str(symbol) + + def _get_input_path(self, input_str): + raise NotImplementedError( + f"_get_input_path is not implemented for {self.__class__}" + ) + def _ipython_display_(self): # IPython is imported here because this function is only called by # IPython. So if IPython is not installed, this function will not be diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 3bf45156..56c86897 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -1001,3 +1001,6 @@ def is_accepting(self, state): def is_initial(self, state): return state == self.initial_state + + def get_edge_label(self, symbol): + return "ε" if symbol == "" else str(symbol) From 0a2a9f575397c06b75b6f7bce4b6b0ad03334022 Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Sun, 9 Apr 2023 18:00:54 +0330 Subject: [PATCH 16/83] Fixed dfa step visualization label bug --- automata/fa/fa.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 56249554..8942f702 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -158,8 +158,8 @@ def show_diagram( if input_str is not None: path, is_accepted = self._get_input_path(input_str=input_str) - start_color = Color("#FFFF00") - end_color = Color("#00FF00") if is_accepted else Color("#FF0000") + start_color = Color("#ff0") + end_color = Color("#0f0") if is_accepted else Color("#f00") number_of_colors = len(input_str) interpolation = Color.interpolate([start_color, end_color], space="lch") @@ -178,9 +178,9 @@ def show_diagram( to_node = self.get_state_label(to_state) label = self.get_edge_label(symbol) graph.edge( - from_state, - to_state, - label=" [#{}]\n{} ".format(transition_index, symbol), + from_node, + to_node, + label=f"{label} [#{transition_index}]", arrowsize=arrow_size, fontsize=font_size, color=color.to_string(hex=True), @@ -192,9 +192,9 @@ def show_diagram( if is_edge_drawn[from_state, to_state, symbol]: continue - label = self.get_edge_label(symbol) from_node = self.get_state_label(from_state) to_node = self.get_state_label(to_state) + label = self.get_edge_label(symbol) edge_labels[from_node, to_node].append(label) for (from_node, to_node), labels in edge_labels.items(): From c7f283ebef2838a817aa6ac592e72e3c667be9bd Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Mon, 10 Apr 2023 21:34:47 +0330 Subject: [PATCH 17/83] Improved coloring for step visualization --- automata/fa/fa.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 8942f702..03a2c091 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -161,10 +161,10 @@ def show_diagram( start_color = Color("#ff0") end_color = Color("#0f0") if is_accepted else Color("#f00") - number_of_colors = len(input_str) interpolation = Color.interpolate([start_color, end_color], space="lch") + step_count = len(input_str) colors = ( - interpolation(x / number_of_colors) for x in range(number_of_colors + 1) + interpolation((x + 1) / step_count) for x in range(step_count + 1) ) # Define all transitions in the finite state machine with traversal. @@ -180,7 +180,7 @@ def show_diagram( graph.edge( from_node, to_node, - label=f"{label} [#{transition_index}]", + label=f"<{label} [#{transition_index}]>", arrowsize=arrow_size, fontsize=font_size, color=color.to_string(hex=True), From 9526482f87ecc4e488577dac654c28e80502f5c7 Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Wed, 12 Apr 2023 13:01:24 +0330 Subject: [PATCH 18/83] Implemented step visualization for `NFA` --- automata/fa/dfa.py | 19 ++++--------------- automata/fa/fa.py | 46 ++++++++++++++++++++-------------------------- automata/fa/nfa.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 41 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 3a0953d4..9d1aacf5 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1567,29 +1567,18 @@ def is_initial(self, state): return state == self.initial_state def _get_input_path(self, input_str): - """ - Calculate the path taken by input. - - Args: - input_str (str): The input string to run on the DFA. - - Returns: - tuple[list[list[tuple[str, str, str]], bool]: A 2d list of all - transitions taken in each step and a boolean indicating whether the - DFA accepted the input. - - """ + """Calculate the path taken by input.""" state_history = [ state for state in self.read_input_stepwise(input_str, ignore_rejection=True) ] path = [ - [transition] + transition for transition in zip(state_history, state_history[1:], input_str) ] last_state = state_history[-1] if state_history else self.initial_state - is_final = last_state in self.final_states + accepted = last_state in self.final_states - return path, is_final + return path, accepted diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 03a2c091..98296c54 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -160,32 +160,25 @@ def show_diagram( start_color = Color("#ff0") end_color = Color("#0f0") if is_accepted else Color("#f00") + interpolation = Color.interpolate([start_color, end_color], space="srgb") - interpolation = Color.interpolate([start_color, end_color], space="lch") - step_count = len(input_str) - colors = ( - interpolation((x + 1) / step_count) for x in range(step_count + 1) - ) - - # Define all transitions in the finite state machine with traversal. - for transition_index, (color, transitions) in enumerate( - zip(colors, path), start=1 + # find all transitions in the finite state machine with traversal. + for transition_index, (from_state, to_state, symbol) in enumerate( + path, start=1 ): - for from_state, to_state, symbol in transitions: - is_edge_drawn[from_state, to_state, symbol] = True - - from_node = self.get_state_label(from_state) - to_node = self.get_state_label(to_state) - label = self.get_edge_label(symbol) - graph.edge( - from_node, - to_node, - label=f"<{label} [#{transition_index}]>", - arrowsize=arrow_size, - fontsize=font_size, - color=color.to_string(hex=True), - penwidth="2.5", - ) + color = interpolation(transition_index / len(path)) + label = self.get_edge_label(symbol) + + is_edge_drawn[from_state, to_state, symbol] = True + graph.edge( + self.get_state_label(from_state), + self.get_state_label(to_state), + label=f"<{label} [#{transition_index}]>", + arrowsize=arrow_size, + fontsize=font_size, + color=color.to_string(hex=True), + penwidth="2.5", + ) edge_labels = defaultdict(list) for from_state, to_state, symbol in self.iter_transitions(): @@ -198,11 +191,10 @@ def show_diagram( edge_labels[from_node, to_node].append(label) for (from_node, to_node), labels in edge_labels.items(): - label = ",".join(sorted(labels)) graph.edge( from_node, to_node, - label=label, + label=",".join(sorted(labels)), arrowsize=arrow_size, fontsize=font_size, ) @@ -233,6 +225,8 @@ def get_edge_label(self, symbol): return str(symbol) def _get_input_path(self, input_str): + """Calculate the path taken by input.""" + raise NotImplementedError( f"_get_input_path is not implemented for {self.__class__}" ) diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 56c86897..8b81d399 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -2,6 +2,7 @@ """Classes and methods for working with nondeterministic finite automata.""" from __future__ import annotations +import functools import string from collections import deque from itertools import chain, count, product, repeat @@ -1004,3 +1005,45 @@ def is_initial(self, state): def get_edge_label(self, symbol): return "ε" if symbol == "" else str(symbol) + + def _get_input_path(self, input_str): + """Calculate the path taken by input.""" + + visiting = set() + + def gen_paths_for(state, step): + symbol = input_str[step] + transitions = self.transitions.get(state, {}) + for next_state in transitions.get(symbol, set()): + path, accepted, steps = get_path_from(next_state, step + 1) + yield [(state, next_state, symbol)] + path, accepted, steps + 1 + + for next_state in transitions.get("", set()): + path, accepted, steps = get_path_from(next_state, step) + yield [(state, next_state, "")] + path, accepted, steps + + @functools.cache + def get_path_from(state, step): + if step >= len(input_str): + return [], state in self.final_states, 0 + + if state in visiting: + return [], False, 0 + + visiting.add(state) + shortest_path = [] + # accepting, max_steps, -min_jumps + best_path = (False, 0, 0) + + for path, accepted, steps in gen_paths_for(state, step): + new_path = (accepted, steps, -len(path)) + if new_path > best_path: + shortest_path = path + best_path = new_path + + visiting.remove(state) + accepting, max_steps, _ = best_path + return shortest_path, accepting, max_steps + + path, accepting, _ = get_path_from(self.initial_state, 0) + return path, accepting From df9f3e476f2ee0083b01cb533c7d3fad2918ad22 Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Wed, 12 Apr 2023 13:56:55 +0330 Subject: [PATCH 19/83] Final touches for rebasing --- automata/fa/fa.py | 1 - pyproject.toml | 1 - requirements.txt | 6 ++++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 98296c54..751eb16b 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -2,7 +2,6 @@ """Classes and methods for working with all finite automata.""" import abc -import itertools import os import pathlib import typing diff --git a/pyproject.toml b/pyproject.toml index 766e88db..68524c06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,6 @@ maintainers = [ ] dependencies = [ "coloraide>=1.8.2", - "frozendict>=2.3.4", "graphviz>=0.20.1", "networkx>=2.6.2", "frozendict>=2.3.4", #TODO I think that typing extensions needs to be in here diff --git a/requirements.txt b/requirements.txt index 9cc8f1e6..5ea292bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,16 +2,16 @@ black==23.3.0 click==8.1.3 coloraide==1.8.2 coverage==6.4.4 +flake8==6.0.0 flake8-black==0.3.6 flake8-isort==6.0.0 Flake8-pyproject==1.2.3 -flake8==6.0.0 frozendict==2.3.4 graphviz==0.20.1 isort==5.10.1 mccabe==0.7.0 -mypy-extensions==1.0.0 mypy==1.1.1 +mypy-extensions==1.0.0 networkx==2.6.2 nose2==0.12.0 packaging==23.0 @@ -21,3 +21,5 @@ pycodestyle==2.10.0 pydot==1.4.2 pyflakes==3.0.1 pyparsing==3.0.9 +types-frozendict==2.0.9 +typing_extensions==4.5.0 From a48ffeff8fa2c13d81d62b08ec35b59017a3120d Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Fri, 17 Mar 2023 12:59:29 +0330 Subject: [PATCH 20/83] Added `get_state_name` --- automata/fa/fa.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 751eb16b..0ddca799 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -7,6 +7,7 @@ import typing import uuid from collections import defaultdict +from typing import Iterable import graphviz from coloraide import Color @@ -22,7 +23,7 @@ class FA(Automaton, metaclass=abc.ABCMeta): __slots__ = tuple() @staticmethod - def get_state_label(state_data): + def get_state_name(state_data): """ Get an string representation of a state. This is used for displaying and uses `str` for any unsupported python data types. From 3ee49fb3b9111df7fbe0585e9021fece010e2266 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Fri, 17 Mar 2023 13:44:18 +0330 Subject: [PATCH 21/83] Fixed reordering list and tuple --- automata/fa/fa.py | 1 - 1 file changed, 1 deletion(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 0ddca799..b22855b3 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -7,7 +7,6 @@ import typing import uuid from collections import defaultdict -from typing import Iterable import graphviz from coloraide import Color From fc6b09e3cf87c9553b0f739bf9f958d997e6feb9 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Fri, 17 Mar 2023 15:40:10 +0330 Subject: [PATCH 22/83] Added abstract methods for visualization --- automata/fa/dfa.py | 2 +- automata/fa/fa.py | 11 ++++++----- automata/fa/gnfa.py | 2 +- automata/fa/nfa.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 9d1aacf5..f44417be 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1560,7 +1560,7 @@ def iter_transitions(self): for symbol, to_ in lookup.items() ) - def is_accepting(self, state): + def is_accepted(self, state): return state in self.final_states def is_initial(self, state): diff --git a/automata/fa/fa.py b/automata/fa/fa.py index b22855b3..e064077d 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -7,6 +7,7 @@ import typing import uuid from collections import defaultdict +from typing import Any, Iterable import graphviz from coloraide import Color @@ -50,22 +51,22 @@ def get_state_name(state_data): return str(state_data) @abc.abstractmethod - def iter_states(self): + def iter_states(self) -> Iterable[Any]: """Iterate over all states in the automaton.""" @abc.abstractmethod - def iter_transitions(self): + def iter_transitions(self) -> Iterable[tuple[Any, Any, Any]]: """ Iterate over all transitions in the automaton. Each transition is a tuple of the form (from_state, to_state, symbol) """ @abc.abstractmethod - def is_accepting(self, state): + def is_accepted(self, state) -> bool: """Check if a state is an accepting state.""" @abc.abstractmethod - def is_initial(self, state): + def is_initial(self, state) -> bool: """Check if a state is an initial state.""" def show_diagram( @@ -149,7 +150,7 @@ def show_diagram( ) for state in self.iter_states(): - shape = "doublecircle" if self.is_accepting(state) else "circle" + shape = "doublecircle" if self.is_accepted(state) else "circle" node = self.get_state_label(state) graph.node(node, shape=shape, fontsize=font_size) diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 5480a4c7..051df97a 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -327,7 +327,7 @@ def iter_transitions(self): if symbol is not None ) - def is_accepting(self, state): + def is_accepted(self, state): return state == self.final_state def is_initial(self, state): diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 8b81d399..8af20375 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -997,7 +997,7 @@ def iter_transitions(self): for to_ in to_lookup ) - def is_accepting(self, state): + def is_accepted(self, state): return state in self.final_states def is_initial(self, state): From 5e55b99eaa6320c58557123af0443c491620f059 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Fri, 17 Mar 2023 22:02:28 +0330 Subject: [PATCH 23/83] Implemented graph visualization --- automata/fa/fa.py | 28 ++++++++++++++-------------- automata/fa/nfa.py | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index e064077d..e4e1fd4b 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -35,7 +35,7 @@ def get_state_name(state_data): return state_data if isinstance(state_data, (set, frozenset, list, tuple)): - inner = ", ".join(FA.get_state_label(sub_data) for sub_data in state_data) + inner = ", ".join(FA.get_state_name(sub_data) for sub_data in state_data) if isinstance(state_data, (set, frozenset)): if state_data: return "{" + inner + "}" @@ -51,22 +51,22 @@ def get_state_name(state_data): return str(state_data) @abc.abstractmethod - def iter_states(self) -> Iterable[Any]: + def iter_states(self) -> Iterable[FAStateT]: """Iterate over all states in the automaton.""" @abc.abstractmethod - def iter_transitions(self) -> Iterable[tuple[Any, Any, Any]]: + def iter_transitions(self) -> Iterable[tuple[FAStateT, FAStateT, Any]]: """ Iterate over all transitions in the automaton. Each transition is a tuple of the form (from_state, to_state, symbol) """ @abc.abstractmethod - def is_accepted(self, state) -> bool: + def is_accepted(self, state: FAStateT) -> bool: """Check if a state is an accepting state.""" @abc.abstractmethod - def is_initial(self, state) -> bool: + def is_initial(self, state: FAStateT) -> bool: """Check if a state is an initial state.""" def show_diagram( @@ -141,7 +141,7 @@ def show_diagram( shape="point", fontsize=font_size, ) - node = self.get_state_label(state) + node = self.get_state_name(state) graph.edge( null_node, node, @@ -151,7 +151,7 @@ def show_diagram( for state in self.iter_states(): shape = "doublecircle" if self.is_accepted(state) else "circle" - node = self.get_state_label(state) + node = self.get_state_name(state) graph.node(node, shape=shape, fontsize=font_size) is_edge_drawn = defaultdict(lambda: False) @@ -167,12 +167,12 @@ def show_diagram( path, start=1 ): color = interpolation(transition_index / len(path)) - label = self.get_edge_label(symbol) + label = self.get_edge_name(symbol) is_edge_drawn[from_state, to_state, symbol] = True graph.edge( - self.get_state_label(from_state), - self.get_state_label(to_state), + self.get_state_name(from_state), + self.get_state_name(to_state), label=f"<{label} [#{transition_index}]>", arrowsize=arrow_size, fontsize=font_size, @@ -185,9 +185,9 @@ def show_diagram( if is_edge_drawn[from_state, to_state, symbol]: continue - from_node = self.get_state_label(from_state) - to_node = self.get_state_label(to_state) - label = self.get_edge_label(symbol) + from_node = self.get_state_name(from_state) + to_node = self.get_state_name(to_state) + label = self.get_edge_name(symbol) edge_labels[from_node, to_node].append(label) for (from_node, to_node), labels in edge_labels.items(): @@ -221,7 +221,7 @@ def show_diagram( return graph - def get_edge_label(self, symbol): + def get_edge_name(self, symbol): return str(symbol) def _get_input_path(self, input_str): diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 8af20375..c54e6599 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -1003,7 +1003,7 @@ def is_accepted(self, state): def is_initial(self, state): return state == self.initial_state - def get_edge_label(self, symbol): + def get_edge_name(self, symbol): return "ε" if symbol == "" else str(symbol) def _get_input_path(self, input_str): From ad144a8f3f78b9d6d8d6e045c42a2a18abb09bd2 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Sat, 18 Mar 2023 00:20:29 +0330 Subject: [PATCH 24/83] Removed IpythonGraph class --- automata/fa/fa.py | 1 - 1 file changed, 1 deletion(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index e4e1fd4b..1983d944 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 """Classes and methods for working with all finite automata.""" - import abc import os import pathlib From 878de2a98454962902801879a6b7f077dc016e75 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Sat, 18 Mar 2023 00:40:37 +0330 Subject: [PATCH 25/83] Fixed `null_node` edge tooltip --- automata/fa/fa.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 1983d944..268545e3 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -22,7 +22,7 @@ class FA(Automaton, metaclass=abc.ABCMeta): __slots__ = tuple() @staticmethod - def get_state_name(state_data): + def get_state_name(state_data) -> str: """ Get an string representation of a state. This is used for displaying and uses `str` for any unsupported python data types. @@ -49,6 +49,9 @@ def get_state_name(state_data): return str(state_data) + def get_edge_name(self, symbol) -> str: + return str(symbol) + @abc.abstractmethod def iter_states(self) -> Iterable[FAStateT]: """Iterate over all states in the automaton.""" @@ -220,9 +223,6 @@ def show_diagram( return graph - def get_edge_name(self, symbol): - return str(symbol) - def _get_input_path(self, input_str): """Calculate the path taken by input.""" From 051808f5a016b93d6ac7a970c0b5de1f474699f6 Mon Sep 17 00:00:00 2001 From: axiom <20.mahdikh.0@gmail.com> Date: Tue, 21 Mar 2023 20:12:44 +0330 Subject: [PATCH 26/83] Removed unused imports --- automata/fa/fa.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 268545e3..814424ed 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -218,9 +218,6 @@ def show_diagram( view=view, ) - elif view: - graph.render(view=True) - return graph def _get_input_path(self, input_str): From 9b883fccdd65a915e1e074a8e379096a07752b31 Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Sun, 26 Mar 2023 20:07:11 +0330 Subject: [PATCH 27/83] Added basic visualizations from Visual Automata --- automata/fa/dfa.py | 2 +- automata/fa/fa.py | 4 ++-- automata/fa/gnfa.py | 2 +- automata/fa/nfa.py | 2 +- pyproject.toml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index f44417be..9d1aacf5 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1560,7 +1560,7 @@ def iter_transitions(self): for symbol, to_ in lookup.items() ) - def is_accepted(self, state): + def is_accepting(self, state): return state in self.final_states def is_initial(self, state): diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 814424ed..845254ad 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -64,7 +64,7 @@ def iter_transitions(self) -> Iterable[tuple[FAStateT, FAStateT, Any]]: """ @abc.abstractmethod - def is_accepted(self, state: FAStateT) -> bool: + def is_accepting(self, state: FAStateT) -> bool: """Check if a state is an accepting state.""" @abc.abstractmethod @@ -152,7 +152,7 @@ def show_diagram( ) for state in self.iter_states(): - shape = "doublecircle" if self.is_accepted(state) else "circle" + shape = "doublecircle" if self.is_accepting(state) else "circle" node = self.get_state_name(state) graph.node(node, shape=shape, fontsize=font_size) diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 051df97a..5480a4c7 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -327,7 +327,7 @@ def iter_transitions(self): if symbol is not None ) - def is_accepted(self, state): + def is_accepting(self, state): return state == self.final_state def is_initial(self, state): diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index c54e6599..87060767 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -997,7 +997,7 @@ def iter_transitions(self): for to_ in to_lookup ) - def is_accepted(self, state): + def is_accepting(self, state): return state in self.final_states def is_initial(self, state): diff --git a/pyproject.toml b/pyproject.toml index 68524c06..6ac1bf3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,9 @@ maintainers = [ ] dependencies = [ "coloraide>=1.8.2", + "frozendict>=2.3.4", "graphviz>=0.20.1", "networkx>=2.6.2", - "frozendict>=2.3.4", #TODO I think that typing extensions needs to be in here ] [project.urls] From a4ae8d8e87ffbf90e71616d600fd3e6be0c74323 Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Fri, 7 Apr 2023 18:19:32 +0330 Subject: [PATCH 28/83] Added step visualization for DFA --- automata/fa/dfa.py | 15 +++++++++++++-- automata/fa/fa.py | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 9d1aacf5..38139fb8 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1566,8 +1566,19 @@ def is_accepting(self, state): def is_initial(self, state): return state == self.initial_state - def _get_input_path(self, input_str): - """Calculate the path taken by input.""" + def _get_input_path(self, input_str) -> tuple[list[tuple[DFAStateT, DFAStateT, DFASymbolT], bool]]: + """ + Calculate the path taken by input. + + Args: + input_str (str): The input string to run on the DFA. + + Returns: + tuple[list[tuple[DFAStateT, DFAStateT, DFASymbolT], bool]]: A list + of all transitions taken in each step and a boolean indicating + whether the DFA accepted the input. + + """ state_history = [ state for state in self.read_input_stepwise(input_str, ignore_rejection=True) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 845254ad..77038361 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -22,7 +22,7 @@ class FA(Automaton, metaclass=abc.ABCMeta): __slots__ = tuple() @staticmethod - def get_state_name(state_data) -> str: + def get_state_name(state_data: FAStateT) -> str: """ Get an string representation of a state. This is used for displaying and uses `str` for any unsupported python data types. From fffa1fa4733385a2d3c7e92e40c638bfdc74cf6a Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Wed, 12 Apr 2023 13:01:24 +0330 Subject: [PATCH 29/83] Implemented step visualization for `NFA` --- automata/fa/dfa.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 38139fb8..59423aa4 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1566,7 +1566,9 @@ def is_accepting(self, state): def is_initial(self, state): return state == self.initial_state - def _get_input_path(self, input_str) -> tuple[list[tuple[DFAStateT, DFAStateT, DFASymbolT], bool]]: + def _get_input_path( + self, input_str + ) -> tuple[list[tuple[DFAStateT, DFAStateT, DFASymbolT], bool]]: """ Calculate the path taken by input. From c3fc3b4bd5355a34f4efc09ad7b07b7954892c25 Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Fri, 14 Apr 2023 16:31:21 +0330 Subject: [PATCH 30/83] Added back frozendict as a dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 6ac1bf3c..766e88db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "frozendict>=2.3.4", "graphviz>=0.20.1", "networkx>=2.6.2", + "frozendict>=2.3.4", #TODO I think that typing extensions needs to be in here ] [project.urls] From fd8e27eee485c5b721691f32fd704852fb339f36 Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Sat, 15 Apr 2023 19:29:29 +0330 Subject: [PATCH 31/83] Cleaned up api added for visualization --- automata/base/automaton.py | 8 ++--- automata/fa/dfa.py | 9 ----- automata/fa/fa.py | 72 ++++++++++++++++++-------------------- automata/fa/gnfa.py | 9 +++-- automata/fa/nfa.py | 70 ++++++++++++++++++++++-------------- 5 files changed, 85 insertions(+), 83 deletions(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 0581915f..391a742f 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -2,7 +2,7 @@ """Classes for working with all automata, including Turing machines.""" import abc -from typing import AbstractSet, Any, Dict, Generator, Mapping, NoReturn, Tuple +from typing import Any, Dict, Generator, Mapping, NoReturn, Set, Tuple from typing_extensions import Self @@ -21,10 +21,10 @@ class Automaton(metaclass=abc.ABCMeta): __slots__: Tuple[str, ...] = tuple() initial_state: AutomatonStateT - states: AbstractSet[AutomatonStateT] - final_states: AbstractSet[AutomatonStateT] + states: Set[AutomatonStateT] + final_states: Set[AutomatonStateT] transitions: AutomatonTransitionsT - input_symbols: AbstractSet[str] + input_symbols: Set[str] def __init__(self, **kwargs: Any) -> None: if not global_config.allow_mutable_automata: diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 59423aa4..5ed5216a 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1550,9 +1550,6 @@ def get_name_original(states: FrozenSet[DFAStateT]) -> DFAStateT: final_states=dfa_final_states, ) - def iter_states(self): - return iter(self.states) - def iter_transitions(self): return ( (from_, to_, symbol) @@ -1560,12 +1557,6 @@ def iter_transitions(self): for symbol, to_ in lookup.items() ) - def is_accepting(self, state): - return state in self.final_states - - def is_initial(self, state): - return state == self.initial_state - def _get_input_path( self, input_str ) -> tuple[list[tuple[DFAStateT, DFAStateT, DFASymbolT], bool]]: diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 77038361..769d7343 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -3,10 +3,9 @@ import abc import os import pathlib -import typing import uuid from collections import defaultdict -from typing import Any, Iterable +from typing import Any, Iterable, Optional, Union import graphviz from coloraide import Color @@ -52,9 +51,9 @@ def get_state_name(state_data: FAStateT) -> str: def get_edge_name(self, symbol) -> str: return str(symbol) - @abc.abstractmethod - def iter_states(self) -> Iterable[FAStateT]: - """Iterate over all states in the automaton.""" + @abc.abstractproperty + def states(self) -> set[FAStateT]: + """A set of all the automaton states.""" @abc.abstractmethod def iter_transitions(self) -> Iterable[tuple[FAStateT, FAStateT, Any]]: @@ -63,20 +62,20 @@ def iter_transitions(self) -> Iterable[tuple[FAStateT, FAStateT, Any]]: of the form (from_state, to_state, symbol) """ - @abc.abstractmethod - def is_accepting(self, state: FAStateT) -> bool: - """Check if a state is an accepting state.""" + @abc.abstractproperty + def final_states(self) -> frozenset[FAStateT]: + """A fronzenset of all the accepting states""" - @abc.abstractmethod - def is_initial(self, state: FAStateT) -> bool: - """Check if a state is an initial state.""" + @abc.abstractproperty + def initial_state(self) -> FAStateT: + """Initial state of the automaton""" def show_diagram( self, - input_str: str | None = None, - save_path: str | os.PathLike | None = None, + input_str: Optional[str] = None, + save_path: Union[str, os.PathLike, None] = None, *, - engine: typing.Optional[str] = None, + engine: Optional[str] = None, view=False, cleanup: bool = True, horizontal: bool = True, @@ -129,30 +128,26 @@ def show_diagram( else: graph.attr(rankdir="BT") - for state in self.iter_states(): - # every edge needs an origin node, so we add a null node for every - # initial state. - if self.is_initial(state): - # we use a random uuid to make sure that the null node has a - # unique id to avoid colliding with other states and null_nodes. - null_node = str(uuid.uuid4()) - graph.node( - null_node, - label="", - tooltip=".", - shape="point", - fontsize=font_size, - ) - node = self.get_state_name(state) - graph.edge( - null_node, - node, - tooltip="->" + node, - arrowsize=arrow_size, - ) + # we use a random uuid to make sure that the null node has a + # unique id to avoid colliding with other states. + null_node = str(uuid.uuid4()) + graph.node( + null_node, + label="", + tooltip=".", + shape="point", + fontsize=font_size, + ) + initial_node = self.get_state_name(self.initial_state) + graph.edge( + null_node, + initial_node, + tooltip="->" + initial_node, + arrowsize=arrow_size, + ) - for state in self.iter_states(): - shape = "doublecircle" if self.is_accepting(state) else "circle" + for state in self.states: + shape = "doublecircle" if state in self.final_states else "circle" node = self.get_state_name(state) graph.node(node, shape=shape, fontsize=font_size) @@ -220,6 +215,7 @@ def show_diagram( return graph + @abc.abstractmethod def _get_input_path(self, input_str): """Calculate the path taken by input.""" @@ -227,7 +223,7 @@ def _get_input_path(self, input_str): f"_get_input_path is not implemented for {self.__class__}" ) - def _ipython_display_(self): + def _ipython_display_(self) -> None: # IPython is imported here because this function is only called by # IPython. So if IPython is not installed, this function will not be # called, therefore no need to add ipython as dependency. diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 5480a4c7..f0f9362e 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -2,6 +2,7 @@ """Classes and methods for working with generalized non-deterministic finite automata.""" +from functools import cached_property from itertools import product from frozendict import frozendict @@ -327,8 +328,6 @@ def iter_transitions(self): if symbol is not None ) - def is_accepting(self, state): - return state == self.final_state - - def is_initial(self, state): - return state == self.initial_state + @cached_property + def final_states(self): + return frozenset({self.final_state}) diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 87060767..801635b0 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -7,7 +7,6 @@ from collections import deque from itertools import chain, count, product, repeat from typing import ( - AbstractSet, Any, Deque, Dict, @@ -33,7 +32,7 @@ NFAStateT = fa.FAStateT -NFAPathT = Mapping[str, AbstractSet[NFAStateT]] +NFAPathT = Mapping[str, Set[NFAStateT]] NFATransitionsT = Mapping[NFAStateT, NFAPathT] DEFAULT_REGEX_SYMBOLS = frozenset(chain(string.ascii_letters, string.digits)) @@ -57,11 +56,11 @@ class NFA(fa.FA): def __init__( self, *, - states: AbstractSet[NFAStateT], - input_symbols: AbstractSet[str], + states: Set[NFAStateT], + input_symbols: Set[str], transitions: NFATransitionsT, initial_state: NFAStateT, - final_states: AbstractSet[NFAStateT], + final_states: Set[NFAStateT], ) -> None: """Initialize a complete NFA.""" super().__init__( @@ -74,7 +73,7 @@ def __init__( ) def _compute_lambda_closures( - self, states: AbstractSet[NFAStateT], transitions: NFATransitionsT + self, states: Set[NFAStateT], transitions: NFATransitionsT ) -> Mapping[NFAStateT, FrozenSet[NFAStateT]]: """ Computes a dictionary of the lambda closures for this NFA, where each @@ -162,7 +161,7 @@ def _validate_transition_invalid_symbols( @classmethod def from_regex( - cls: Type[Self], regex: str, *, input_symbols: Optional[AbstractSet[str]] = None + cls: Type[Self], regex: str, *, input_symbols: Optional[Set[str]] = None ) -> Self: """Initialize this NFA as one equivalent to the given regular expression""" @@ -207,7 +206,7 @@ def validate(self) -> None: self._validate_final_states() def _get_next_current_states( - self, current_states: AbstractSet[NFAStateT], input_symbol: str + self, current_states: Set[NFAStateT], input_symbol: str ) -> FrozenSet[NFAStateT]: """Return the next set of current states given the current set.""" next_current_states: Set[NFAStateT] = set() @@ -249,7 +248,7 @@ def _compute_reachable_states( def _eliminate_lambda( self, - ) -> Tuple[AbstractSet[NFAStateT], NFATransitionsT, AbstractSet[NFAStateT]]: + ) -> Tuple[Set[NFAStateT], NFATransitionsT, Set[NFAStateT]]: """Internal helper function for eliminate lambda. Doesn't create final NFA.""" # Create new transitions and final states for running this algorithm @@ -311,7 +310,7 @@ def eliminate_lambda(self) -> Self: ) def _check_for_input_rejection( - self, current_states: AbstractSet[NFAStateT] + self, current_states: Set[NFAStateT] ) -> None: """Raise an error if the given config indicates rejected input.""" if current_states.isdisjoint(self.final_states): @@ -323,7 +322,7 @@ def _check_for_input_rejection( def read_input_stepwise( self, input_str: str - ) -> Generator[AbstractSet[NFAStateT], None, None]: + ) -> Generator[Set[NFAStateT], None, None]: """ Check if the given string is accepted by this NFA. @@ -340,8 +339,8 @@ def read_input_stepwise( @staticmethod def _get_state_maps( - state_set_a: AbstractSet[NFAStateT], - state_set_b: AbstractSet[NFAStateT], + state_set_a: Set[NFAStateT], + state_set_b: Set[NFAStateT], *, start: int = 0, ) -> Tuple[Dict[NFAStateT, int], Dict[NFAStateT, int]]: @@ -901,7 +900,7 @@ def transition(states_pair: NFAStatesPairT, symbol: str) -> NFAStatesPairT: @classmethod def edit_distance( cls: Type[Self], - input_symbols: AbstractSet[str], + input_symbols: Set[str], reference_str: str, max_edit_distance: int, *, @@ -986,9 +985,6 @@ def add_any_transition(start_state_dict, end_state): final_states=final_states, ) - def iter_states(self): - return iter(self.states) - def iter_transitions(self): return ( (from_, to_, symbol) @@ -997,12 +993,6 @@ def iter_transitions(self): for to_ in to_lookup ) - def is_accepting(self, state): - return state in self.final_states - - def is_initial(self, state): - return state == self.initial_state - def get_edge_name(self, symbol): return "ε" if symbol == "" else str(symbol) @@ -1012,38 +1002,64 @@ def _get_input_path(self, input_str): visiting = set() def gen_paths_for(state, step): + """ + Generate all the possible paths from state after taking step n. + """ symbol = input_str[step] transitions = self.transitions.get(state, {}) + # generate all the paths after taking input symbol and increasing + # input read length by 1 for next_state in transitions.get(symbol, set()): - path, accepted, steps = get_path_from(next_state, step + 1) + path, accepted, steps = find_best_path(next_state, step + 1) yield [(state, next_state, symbol)] + path, accepted, steps + 1 + # generate all the lambda paths from state for next_state in transitions.get("", set()): - path, accepted, steps = get_path_from(next_state, step) + path, accepted, steps = find_best_path(next_state, step) yield [(state, next_state, "")] + path, accepted, steps @functools.cache - def get_path_from(state, step): + def find_best_path(state, step): + """ + Try all the possible paths from state after taking step n. A path is + better if (with priority): + 1. It is an accepting path (ends in a final state) + 2. It reads more of the input (if the input is not accepted we + select the path such that we can stay on the nfa the longest) + 3. It has the fewest jumps (uses less lambda symbols) + + Returns a tuple of: + 1. the path taken + 2. wether the path was accepting + 3. the number of input symbols read by this path (or the number of + non-lambda transitions in the path) + """ if step >= len(input_str): return [], state in self.final_states, 0 if state in visiting: return [], False, 0 + # mark this state as being visited visiting.add(state) + # tracking variable for the shortest path shortest_path = [] + # tracking variable for the info about the path # accepting, max_steps, -min_jumps best_path = (False, 0, 0) + # iterate over all the paths for path, accepted, steps in gen_paths_for(state, step): + # create a tuple to compare this with the current best new_path = (accepted, steps, -len(path)) if new_path > best_path: shortest_path = path best_path = new_path + # mark this as complete visiting.remove(state) accepting, max_steps, _ = best_path return shortest_path, accepting, max_steps - path, accepting, _ = get_path_from(self.initial_state, 0) + path, accepting, _ = find_best_path(self.initial_state, 0) return path, accepting From c3e58be9e166a350ad6e61e3e6db7a8125e4208e Mon Sep 17 00:00:00 2001 From: Mahdi Khodabandeh <42507906+khoda81@users.noreply.github.com> Date: Wed, 26 Apr 2023 23:46:06 +0330 Subject: [PATCH 32/83] Changed back to `AbstractSet` --- automata/base/automaton.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 391a742f..0581915f 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -2,7 +2,7 @@ """Classes for working with all automata, including Turing machines.""" import abc -from typing import Any, Dict, Generator, Mapping, NoReturn, Set, Tuple +from typing import AbstractSet, Any, Dict, Generator, Mapping, NoReturn, Tuple from typing_extensions import Self @@ -21,10 +21,10 @@ class Automaton(metaclass=abc.ABCMeta): __slots__: Tuple[str, ...] = tuple() initial_state: AutomatonStateT - states: Set[AutomatonStateT] - final_states: Set[AutomatonStateT] + states: AbstractSet[AutomatonStateT] + final_states: AbstractSet[AutomatonStateT] transitions: AutomatonTransitionsT - input_symbols: Set[str] + input_symbols: AbstractSet[str] def __init__(self, **kwargs: Any) -> None: if not global_config.allow_mutable_automata: From e41dbe5049da5ceee05764daaae10e2133742d69 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 28 Apr 2023 23:00:54 -0500 Subject: [PATCH 33/83] Update automaton.py --- automata/base/automaton.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 0581915f..82bae34c 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -4,6 +4,7 @@ import abc from typing import AbstractSet, Any, Dict, Generator, Mapping, NoReturn, Tuple +from frozendict import frozendict from typing_extensions import Self import automata.base.config as global_config @@ -123,10 +124,29 @@ def copy(self) -> Self: """Create a deep copy of the automaton.""" return self.__class__(**self.input_parameters) + # Format the given value for string output via repr() or str(); this exists + # for the purpose of displaying + @staticmethod + def _get_repr_friendly_value(self, value: Any) -> Any: + """ + A helper function to convert the given value / structure into a fully + mutable one by recursively processing said structure and any of its + members, unfreezing them along the way + """ + if isinstance(value, frozenset): + return {self._get_repr_friendly_value(element) for element in value} + elif isinstance(value, frozendict): + return { + dict_key: self._get_repr_friendly_value(dict_value) + for dict_key, dict_value in value.items() + } + else: + return value + def __repr__(self) -> str: """Return a string representation of the automaton.""" values = ", ".join( - f"{attr_name}={attr_value!r}" + f"{attr_name}={self._get_repr_friendly_value(attr_value)!r}" for attr_name, attr_value in self.input_parameters.items() ) return f"{self.__class__.__qualname__}({values})" From 7cc87cbc0b1e18d0488f767e8c91a1b77167ae6e Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 28 Apr 2023 23:01:47 -0500 Subject: [PATCH 34/83] Update automaton.py --- automata/base/automaton.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 82bae34c..7f7332ab 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -126,7 +126,7 @@ def copy(self) -> Self: # Format the given value for string output via repr() or str(); this exists # for the purpose of displaying - @staticmethod + def _get_repr_friendly_value(self, value: Any) -> Any: """ A helper function to convert the given value / structure into a fully From aab3eb5d853987343de4eec3279fa29c1fc90f27 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 28 Apr 2023 23:22:29 -0500 Subject: [PATCH 35/83] More tests passing --- automata/fa/dfa.py | 4 ++-- automata/fa/fa.py | 10 ++++++---- automata/fa/gnfa.py | 6 +++++- automata/fa/nfa.py | 8 ++++---- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index cd835631..b1154645 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1550,7 +1550,7 @@ def get_name_original(states: FrozenSet[DFAStateT]) -> DFAStateT: final_states=dfa_final_states, ) - def iter_transitions(self): + def iter_transitions(self) -> Iterable[Tuple[DFAStateT, DFAStateT, str]]: return ( (from_, to_, symbol) for from_, lookup in self.transitions.items() @@ -1559,7 +1559,7 @@ def iter_transitions(self): def _get_input_path( self, input_str - ) -> tuple[list[tuple[DFAStateT, DFAStateT, DFASymbolT], bool]]: + ) -> Tuple[List[Tuple[DFAStateT, DFAStateT, DFASymbolT], bool]]: """ Calculate the path taken by input. diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 9385846c..4bc512ca 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -5,7 +5,7 @@ import pathlib import uuid from collections import defaultdict -from typing import Any, Iterable, Optional, Set, Union +from typing import Any, Iterable, List, Optional, Set, Tuple, Union import graphviz from coloraide import Color @@ -56,7 +56,7 @@ def states(self) -> set[FAStateT]: """A set of all the automaton states.""" @abc.abstractmethod - def iter_transitions(self) -> Iterable[tuple[FAStateT, FAStateT, Any]]: + def iter_transitions(self) -> Iterable[Tuple[FAStateT, FAStateT, str]]: """ Iterate over all transitions in the automaton. Each transition is a tuple of the form (from_state, to_state, symbol) @@ -216,7 +216,9 @@ def show_diagram( return graph @abc.abstractmethod - def _get_input_path(self, input_str): + def _get_input_path( + self, input_str: str + ) -> Tuple[List[Tuple[FAStateT, FAStateT, str]], bool]: """Calculate the path taken by input.""" raise NotImplementedError( @@ -230,7 +232,7 @@ def _ipython_display_(self) -> None: from IPython.display import display display(self.show_diagram()) - + @staticmethod def _add_new_state(state_set: Set[FAStateT], start: int = 0) -> int: """Adds new state to the state set and returns it""" diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index fd5bc6e1..ddafb13b 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -8,7 +8,6 @@ from typing import AbstractSet, Dict, Mapping, Optional, Set, Type, cast from frozendict import frozendict - from pydot import Dot, Edge, Node from typing_extensions import NoReturn, Self @@ -340,3 +339,8 @@ def iter_transitions(self): @cached_property def final_states(self): return frozenset({self.final_state}) + + def _get_input_path(self, input_str: str) -> NoReturn: + raise NotImplementedError( + f"_get_input_path is not implemented for {self.__class__}" + ) diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 06a9fa90..6d0af1d8 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -305,9 +305,7 @@ def eliminate_lambda(self) -> Self: final_states=reachable_final_states, ) - def _check_for_input_rejection( - self, current_states: Set[NFAStateT] - ) -> None: + def _check_for_input_rejection(self, current_states: Set[NFAStateT]) -> None: """Raise an error if the given config indicates rejected input.""" if current_states.isdisjoint(self.final_states): raise exceptions.RejectionException( @@ -981,7 +979,9 @@ def iter_transitions(self): def get_edge_name(self, symbol): return "ε" if symbol == "" else str(symbol) - def _get_input_path(self, input_str): + def _get_input_path( + self, input_str: str + ) -> Tuple[List[Tuple[NFAStateT, NFAStateT, str], bool]]: """Calculate the path taken by input.""" visiting = set() From 36083f28dda6be084b6757f91b591a82623419db Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 28 Apr 2023 23:25:07 -0500 Subject: [PATCH 36/83] Update fa.py --- automata/fa/fa.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 4bc512ca..1971f743 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -51,10 +51,6 @@ def get_state_name(state_data: FAStateT) -> str: def get_edge_name(self, symbol) -> str: return str(symbol) - @abc.abstractproperty - def states(self) -> set[FAStateT]: - """A set of all the automaton states.""" - @abc.abstractmethod def iter_transitions(self) -> Iterable[Tuple[FAStateT, FAStateT, str]]: """ @@ -62,14 +58,6 @@ def iter_transitions(self) -> Iterable[Tuple[FAStateT, FAStateT, str]]: of the form (from_state, to_state, symbol) """ - @abc.abstractproperty - def final_states(self) -> frozenset[FAStateT]: - """A fronzenset of all the accepting states""" - - @abc.abstractproperty - def initial_state(self) -> FAStateT: - """Initial state of the automaton""" - def show_diagram( self, input_str: Optional[str] = None, From 957c2d9927a05a19f4cd446bbd142474dd3563d1 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 28 Apr 2023 23:32:55 -0500 Subject: [PATCH 37/83] Update gnfa.py --- automata/fa/gnfa.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index ddafb13b..2e4a60d2 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -26,17 +26,13 @@ class GNFA(fa.FA): """A generalized nondeterministic finite automaton.""" - # The conventions of using __slots__ state that subclasses automatically - # inherit __slots__ from parent classes, so there's no need to redeclare - # slotted attributes for each subclass; however, because NFA has a - # 'final_states' attribute but GNFA has a 'final_state' attribute, we must - # redeclare them below to exclude 'final_states' __slots__ = ( "states", "input_symbols", "transitions", "initial_state", "final_state", + "__dict__", ) final_state: GNFAStateT From 7958451ecc45713fa9250f5d72d74a53456aee1b Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 28 Apr 2023 23:41:06 -0500 Subject: [PATCH 38/83] Change some types --- automata/fa/dfa.py | 8 +++++--- automata/fa/nfa.py | 29 ++++++++++++++++------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index b1154645..64de4a63 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -804,7 +804,7 @@ def predecessors( *, strict: bool = True, key: Optional[Callable[[Any], Any]] = None, - ) -> Iterable[str]: + ) -> Generator[str, None, None]: """ Generates all strings that come before the input string in lexicographical order. @@ -842,7 +842,7 @@ def successors( strict: bool = True, key: Optional[Callable[[Any], Any]] = None, reverse: bool = False, - ) -> Iterable[str]: + ) -> Generator[str, None, None]: """ Generates all strings that come after the input string in lexicographical order. Passing in None will generate all words. If @@ -1550,7 +1550,9 @@ def get_name_original(states: FrozenSet[DFAStateT]) -> DFAStateT: final_states=dfa_final_states, ) - def iter_transitions(self) -> Iterable[Tuple[DFAStateT, DFAStateT, str]]: + def iter_transitions( + self, + ) -> Generator[Tuple[DFAStateT, DFAStateT, str], None, None]: return ( (from_, to_, symbol) for from_, lookup in self.transitions.items() diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 6d0af1d8..394aaa9e 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -7,6 +7,7 @@ from collections import deque from itertools import chain, count, product, repeat from typing import ( + AbstractSet, Any, Deque, Dict, @@ -32,7 +33,7 @@ NFAStateT = fa.FAStateT -NFAPathT = Mapping[str, Set[NFAStateT]] +NFAPathT = Mapping[str, AbstractSet[NFAStateT]] NFATransitionsT = Mapping[NFAStateT, NFAPathT] DEFAULT_REGEX_SYMBOLS = frozenset(chain(string.ascii_letters, string.digits)) @@ -56,11 +57,11 @@ class NFA(fa.FA): def __init__( self, *, - states: Set[NFAStateT], - input_symbols: Set[str], + states: AbstractSet[NFAStateT], + input_symbols: AbstractSet[str], transitions: NFATransitionsT, initial_state: NFAStateT, - final_states: Set[NFAStateT], + final_states: AbstractSet[NFAStateT], ) -> None: """Initialize a complete NFA.""" super().__init__( @@ -73,7 +74,7 @@ def __init__( ) def _compute_lambda_closures( - self, states: Set[NFAStateT], transitions: NFATransitionsT + self, states: AbstractSet[NFAStateT], transitions: NFATransitionsT ) -> Mapping[NFAStateT, FrozenSet[NFAStateT]]: """ Computes a dictionary of the lambda closures for this NFA, where each @@ -157,7 +158,7 @@ def _validate_transition_invalid_symbols( @classmethod def from_regex( - cls: Type[Self], regex: str, *, input_symbols: Optional[Set[str]] = None + cls: Type[Self], regex: str, *, input_symbols: Optional[AbstractSet[str]] = None ) -> Self: """Initialize this NFA as one equivalent to the given regular expression""" @@ -202,7 +203,7 @@ def validate(self) -> None: self._validate_final_states() def _get_next_current_states( - self, current_states: Set[NFAStateT], input_symbol: str + self, current_states: AbstractSet[NFAStateT], input_symbol: str ) -> FrozenSet[NFAStateT]: """Return the next set of current states given the current set.""" next_current_states: Set[NFAStateT] = set() @@ -244,7 +245,7 @@ def _compute_reachable_states( def _eliminate_lambda( self, - ) -> Tuple[Set[NFAStateT], NFATransitionsT, Set[NFAStateT]]: + ) -> Tuple[AbstractSet[NFAStateT], NFATransitionsT, AbstractSet[NFAStateT]]: """Internal helper function for eliminate lambda. Doesn't create final NFA.""" # Create new transitions and final states for running this algorithm @@ -305,7 +306,9 @@ def eliminate_lambda(self) -> Self: final_states=reachable_final_states, ) - def _check_for_input_rejection(self, current_states: Set[NFAStateT]) -> None: + def _check_for_input_rejection( + self, current_states: AbstractSet[NFAStateT] + ) -> None: """Raise an error if the given config indicates rejected input.""" if current_states.isdisjoint(self.final_states): raise exceptions.RejectionException( @@ -316,7 +319,7 @@ def _check_for_input_rejection(self, current_states: Set[NFAStateT]) -> None: def read_input_stepwise( self, input_str: str - ) -> Generator[Set[NFAStateT], None, None]: + ) -> Generator[AbstractSet[NFAStateT], None, None]: """ Check if the given string is accepted by this NFA. @@ -333,8 +336,8 @@ def read_input_stepwise( @staticmethod def _get_state_maps( - state_set_a: Set[NFAStateT], - state_set_b: Set[NFAStateT], + state_set_a: AbstractSet[NFAStateT], + state_set_b: AbstractSet[NFAStateT], *, start: int = 0, ) -> Tuple[Dict[NFAStateT, int], Dict[NFAStateT, int]]: @@ -883,7 +886,7 @@ def transition(states_pair: NFAStatesPairT, symbol: str) -> NFAStatesPairT: @classmethod def edit_distance( cls: Type[Self], - input_symbols: Set[str], + input_symbols: AbstractSet[str], reference_str: str, max_edit_distance: int, *, From e36d2c628d90e4774e4117d950c5a0cd8e75ad5e Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 28 Apr 2023 23:53:04 -0500 Subject: [PATCH 39/83] Type fixes --- automata/fa/dfa.py | 2 +- automata/fa/fa.py | 6 +++--- automata/fa/gnfa.py | 17 ++++++++++++++--- automata/fa/nfa.py | 2 +- pyproject.toml | 2 +- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 64de4a63..1f69386a 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1561,7 +1561,7 @@ def iter_transitions( def _get_input_path( self, input_str - ) -> Tuple[List[Tuple[DFAStateT, DFAStateT, DFASymbolT], bool]]: + ) -> Tuple[List[Tuple[DFAStateT, DFAStateT, DFASymbolT]], bool]: """ Calculate the path taken by input. diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 1971f743..27a1d06c 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -5,7 +5,7 @@ import pathlib import uuid from collections import defaultdict -from typing import Any, Iterable, List, Optional, Set, Tuple, Union +from typing import Any, Generator, List, Optional, Set, Tuple, Union import graphviz from coloraide import Color @@ -52,7 +52,7 @@ def get_edge_name(self, symbol) -> str: return str(symbol) @abc.abstractmethod - def iter_transitions(self) -> Iterable[Tuple[FAStateT, FAStateT, str]]: + def iter_transitions(self) -> Generator[Tuple[FAStateT, FAStateT, str], None, None]: """ Iterate over all transitions in the automaton. Each transition is a tuple of the form (from_state, to_state, symbol) @@ -68,7 +68,7 @@ def show_diagram( cleanup: bool = True, horizontal: bool = True, reverse_orientation: bool = False, - fig_size: tuple = None, + fig_size: Optional[Tuple] = None, font_size: float = 14.0, arrow_size: float = 0.85, state_separation: float = 0.5, diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 2e4a60d2..fcf16301 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -5,10 +5,19 @@ from functools import cached_property from itertools import product -from typing import AbstractSet, Dict, Mapping, Optional, Set, Type, cast +from typing import ( + AbstractSet, + Dict, + Generator, + Mapping, + Optional, + Set, + Tuple, + Type, + cast, +) from frozendict import frozendict -from pydot import Dot, Edge, Node from typing_extensions import NoReturn, Self import automata.base.exceptions as exceptions @@ -324,7 +333,9 @@ def to_regex(self) -> str: def read_input_stepwise(self, input_str: str) -> NoReturn: raise NotImplementedError - def iter_transitions(self): + def iter_transitions( + self, + ) -> Generator[Tuple[GNFAStateT, GNFAStateT, str], None, None]: return ( (from_, to_, symbol) for from_, lookup in self.transitions.items() diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 394aaa9e..ebc5e1f6 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -984,7 +984,7 @@ def get_edge_name(self, symbol): def _get_input_path( self, input_str: str - ) -> Tuple[List[Tuple[NFAStateT, NFAStateT, str], bool]]: + ) -> Tuple[List[Tuple[NFAStateT, NFAStateT, str]], bool]: """Calculate the path taken by input.""" visiting = set() diff --git a/pyproject.toml b/pyproject.toml index 766e88db..3e021386 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ exclude = ["build"] module = [ "setuptools.*", "networkx.*", - "pydot.*" # TODO pydot dependency is going to get removed, can delete + "graphviz.*" # TODO pydot dependency is going to get removed, can delete ] ignore_missing_imports = true From 55f8120a918afd4ec0e784c19c73c88bef2bec6c Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 28 Apr 2023 23:54:01 -0500 Subject: [PATCH 40/83] Update nfa.py --- automata/fa/nfa.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index ebc5e1f6..5614ba02 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -971,7 +971,9 @@ def add_any_transition(start_state_dict, end_state): final_states=final_states, ) - def iter_transitions(self): + def iter_transitions( + self, + ) -> Generator[Tuple[NFAStateT, NFAStateT, str], None, None]: return ( (from_, to_, symbol) for from_, lookup in self.transitions.items() From 8513a89df21f245123ee21f5c0e514246c6f7757 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 28 Apr 2023 23:55:34 -0500 Subject: [PATCH 41/83] Simplify get edge name function --- automata/fa/fa.py | 5 +++-- automata/fa/nfa.py | 3 --- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 27a1d06c..288a2002 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -48,8 +48,9 @@ def get_state_name(state_data: FAStateT) -> str: return str(state_data) - def get_edge_name(self, symbol) -> str: - return str(symbol) + @staticmethod + def get_edge_name(symbol: str) -> str: + return "ε" if symbol == "" else str(symbol) @abc.abstractmethod def iter_transitions(self) -> Generator[Tuple[FAStateT, FAStateT, str], None, None]: diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 5614ba02..163bd7c9 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -981,9 +981,6 @@ def iter_transitions( for to_ in to_lookup ) - def get_edge_name(self, symbol): - return "ε" if symbol == "" else str(symbol) - def _get_input_path( self, input_str: str ) -> Tuple[List[Tuple[NFAStateT, NFAStateT, str]], bool]: From fc7bee3e055555556dbc1ab12dae86eb95cbd051 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 00:08:43 -0500 Subject: [PATCH 42/83] List functions --- automata/fa/dfa.py | 12 +++--------- pyproject.toml | 2 ++ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index 1f69386a..e7273b59 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -1574,15 +1574,9 @@ def _get_input_path( whether the DFA accepted the input. """ - state_history = [ - state - for state in self.read_input_stepwise(input_str, ignore_rejection=True) - ] - - path = [ - transition - for transition in zip(state_history, state_history[1:], input_str) - ] + + state_history = list(self.read_input_stepwise(input_str, ignore_rejection=True)) + path = list(zip(state_history, state_history[1:], input_str)) last_state = state_history[-1] if state_history else self.initial_state accepted = last_state in self.final_states diff --git a/pyproject.toml b/pyproject.toml index 3e021386..08ae4cd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,8 @@ dependencies = [ "frozendict>=2.3.4", #TODO I think that typing extensions needs to be in here ] +#TODO maybe add stuff from here?: https://blog.whtsky.me/tech/2021/dont-forget-py.typed-for-your-typed-python-package/#adding-pytyped + [project.urls] homepage = "https://github.com/caleb531/automata" documentation = "https://github.com/caleb531/automata#readme" From 55bd71128c2a785e155c76a71b6519c163c95938 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 00:56:01 -0500 Subject: [PATCH 43/83] Put import behind check --- automata/fa/fa.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 288a2002..335b4798 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -7,11 +7,19 @@ from collections import defaultdict from typing import Any, Generator, List, Optional, Set, Tuple, Union -import graphviz -from coloraide import Color - from automata.base.automaton import Automaton, AutomatonStateT +# Optional imports for use with visual functionality +try: + import coloraide + import graphviz + import IPython.display +except ImportError: + _visual_imports = False +else: + _visual_imports = True + + FAStateT = AutomatonStateT @@ -98,6 +106,9 @@ def show_diagram( Digraph: The graph in dot format. """ + if not _visual_imports: + raise ImportError("Missing visual imports.") + # Defining the graph. graph = graphviz.Digraph(strict=False, engine=engine) @@ -144,9 +155,13 @@ def show_diagram( if input_str is not None: path, is_accepted = self._get_input_path(input_str=input_str) - start_color = Color("#ff0") - end_color = Color("#0f0") if is_accepted else Color("#f00") - interpolation = Color.interpolate([start_color, end_color], space="srgb") + start_color = coloraide.Color("#ff0") + end_color = ( + coloraide.Color("#0f0") if is_accepted else coloraide.Color("#f00") + ) + interpolation = coloraide.Color.interpolate( + [start_color, end_color], space="srgb" + ) # find all transitions in the finite state machine with traversal. for transition_index, (from_state, to_state, symbol) in enumerate( @@ -215,12 +230,10 @@ def _get_input_path( ) def _ipython_display_(self) -> None: - # IPython is imported here because this function is only called by - # IPython. So if IPython is not installed, this function will not be - # called, therefore no need to add ipython as dependency. - from IPython.display import display + if not _visual_imports: + raise ImportError("Missing visual imports.") - display(self.show_diagram()) + IPython.display.display(self.show_diagram()) @staticmethod def _add_new_state(state_set: Set[FAStateT], start: int = 0) -> int: From 165d25b7361e1ae9e6215304999ef5ab3335d1cd Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 01:19:52 -0500 Subject: [PATCH 44/83] Fixed some conflicting variable names --- automata/fa/fa.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 335b4798..bd7816b0 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -70,7 +70,7 @@ def iter_transitions(self) -> Generator[Tuple[FAStateT, FAStateT, str], None, No def show_diagram( self, input_str: Optional[str] = None, - save_path: Union[str, os.PathLike, None] = None, + path: Union[str, os.PathLike, None] = None, *, engine: Optional[str] = None, view=False, @@ -81,15 +81,13 @@ def show_diagram( font_size: float = 14.0, arrow_size: float = 0.85, state_separation: float = 0.5, - ): + ) -> graphviz.Digraph: """ Generates the graph associated with the given DFA. Args: input_str (str, optional): String list of input symbols. Defaults to None. - - save_path (str or os.PathLike, optional): Path to output file. If + - path (str or os.PathLike, optional): Path to output file. If None, the output will not be saved. - - path (str, optional): Folder path for output file. Defaults to - None. - view (bool, optional): Storing and displaying the graph as a pdf. Defaults to False. - cleanup (bool, optional): Garbage collection. Defaults to True. @@ -117,8 +115,8 @@ def show_diagram( graph.attr(size=", ".join(map(str, fig_size))) graph.attr(ranksep=str(state_separation)) - font_size = str(font_size) - arrow_size = str(arrow_size) + font_size_str = str(font_size) + arrow_size_str = str(arrow_size) if horizontal: graph.attr(rankdir="LR") @@ -136,24 +134,24 @@ def show_diagram( label="", tooltip=".", shape="point", - fontsize=font_size, + fontsize=font_size_str, ) initial_node = self.get_state_name(self.initial_state) graph.edge( null_node, initial_node, tooltip="->" + initial_node, - arrowsize=arrow_size, + arrowsize=arrow_size_str, ) for state in self.states: shape = "doublecircle" if state in self.final_states else "circle" node = self.get_state_name(state) - graph.node(node, shape=shape, fontsize=font_size) + graph.node(node, shape=shape, fontsize=font_size_str) is_edge_drawn = defaultdict(lambda: False) if input_str is not None: - path, is_accepted = self._get_input_path(input_str=input_str) + input_path, is_accepted = self._get_input_path(input_str=input_str) start_color = coloraide.Color("#ff0") end_color = ( @@ -165,9 +163,9 @@ def show_diagram( # find all transitions in the finite state machine with traversal. for transition_index, (from_state, to_state, symbol) in enumerate( - path, start=1 + input_path, start=1 ): - color = interpolation(transition_index / len(path)) + color = interpolation(transition_index / len(input_path)) label = self.get_edge_name(symbol) is_edge_drawn[from_state, to_state, symbol] = True @@ -175,8 +173,8 @@ def show_diagram( self.get_state_name(from_state), self.get_state_name(to_state), label=f"<{label} [#{transition_index}]>", - arrowsize=arrow_size, - fontsize=font_size, + arrowsize=arrow_size_str, + fontsize=font_size_str, color=color.to_string(hex=True), penwidth="2.5", ) @@ -196,18 +194,20 @@ def show_diagram( from_node, to_node, label=",".join(sorted(labels)), - arrowsize=arrow_size, - fontsize=font_size, + arrowsize=arrow_size_str, + fontsize=font_size_str, ) # Write diagram to file. PNG, SVG, etc. - if save_path is not None: - save_path: pathlib.Path = pathlib.Path(save_path) + if path is not None: + save_path_final: pathlib.Path = pathlib.Path(path) - directory = save_path.parent + directory = save_path_final.parent directory.mkdir(parents=True, exist_ok=True) - filename = save_path.stem - format = save_path.suffix.split(".")[1] if save_path.suffix else None + filename = save_path_final.stem + format = ( + save_path_final.suffix.split(".")[1] if save_path_final.suffix else None + ) graph.render( directory=directory, From b00470e7810d5c49fe89f1a7635d8ec94dfadb27 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 01:56:07 -0500 Subject: [PATCH 45/83] Update fa.py --- automata/fa/fa.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index bd7816b0..a99888cb 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -40,7 +40,7 @@ def get_state_name(state_data: FAStateT) -> str: return state_data - if isinstance(state_data, (set, frozenset, list, tuple)): + elif isinstance(state_data, (set, frozenset, tuple)): inner = ", ".join(FA.get_state_name(sub_data) for sub_data in state_data) if isinstance(state_data, (set, frozenset)): if state_data: @@ -48,12 +48,9 @@ def get_state_name(state_data: FAStateT) -> str: else: return "∅" - if isinstance(state_data, tuple): + elif isinstance(state_data, tuple): return "(" + inner + ")" - if isinstance(state_data, list): - return "[" + inner + "]" - return str(state_data) @staticmethod From e52e2bd46c641b62e18e36fb3ca0312c991bd037 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 13:16:08 -0500 Subject: [PATCH 46/83] Started switch to pygraphviz --- automata/fa/fa.py | 62 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index a99888cb..dd86255b 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -5,7 +5,7 @@ import pathlib import uuid from collections import defaultdict -from typing import Any, Generator, List, Optional, Set, Tuple, Union +from typing import Any, Dict, Generator, Iterable, List, Optional, Set, Tuple, Union from automata.base.automaton import Automaton, AutomatonStateT @@ -14,6 +14,7 @@ import coloraide import graphviz import IPython.display + import pygraphviz as pgv except ImportError: _visual_imports = False else: @@ -78,7 +79,7 @@ def show_diagram( font_size: float = 14.0, arrow_size: float = 0.85, state_separation: float = 0.5, - ) -> graphviz.Digraph: + ) -> pgv.AGraph: """ Generates the graph associated with the given DFA. Args: @@ -105,28 +106,28 @@ def show_diagram( raise ImportError("Missing visual imports.") # Defining the graph. - graph = graphviz.Digraph(strict=False, engine=engine) + graph = pgv.AGraph(strict=False, directed=True) # TODO test fig_size if fig_size is not None: - graph.attr(size=", ".join(map(str, fig_size))) + graph.graph_attr.update(size=", ".join(map(str, fig_size))) - graph.attr(ranksep=str(state_separation)) + graph.graph_attr.update(ranksep=str(state_separation)) font_size_str = str(font_size) arrow_size_str = str(arrow_size) if horizontal: - graph.attr(rankdir="LR") + graph.graph_attr.update(rankdir="LR") if reverse_orientation: if horizontal: - graph.attr(rankdir="RL") + graph.graph_attr.update(rankdir="RL") else: - graph.attr(rankdir="BT") + graph.graph_attr.update(rankdir="BT") # we use a random uuid to make sure that the null node has a # unique id to avoid colliding with other states. null_node = str(uuid.uuid4()) - graph.node( + graph.add_node( null_node, label="", tooltip=".", @@ -134,7 +135,7 @@ def show_diagram( fontsize=font_size_str, ) initial_node = self.get_state_name(self.initial_state) - graph.edge( + graph.add_edge( null_node, initial_node, tooltip="->" + initial_node, @@ -144,7 +145,7 @@ def show_diagram( for state in self.states: shape = "doublecircle" if state in self.final_states else "circle" node = self.get_state_name(state) - graph.node(node, shape=shape, fontsize=font_size_str) + graph.add_node(node, shape=shape, fontsize=font_size_str) is_edge_drawn = defaultdict(lambda: False) if input_str is not None: @@ -166,7 +167,7 @@ def show_diagram( label = self.get_edge_name(symbol) is_edge_drawn[from_state, to_state, symbol] = True - graph.edge( + graph.add_edge( self.get_state_name(from_state), self.get_state_name(to_state), label=f"<{label} [#{transition_index}]>", @@ -187,7 +188,7 @@ def show_diagram( edge_labels[from_node, to_node].append(label) for (from_node, to_node), labels in edge_labels.items(): - graph.edge( + graph.add_edge( from_node, to_node, label=",".join(sorted(labels)), @@ -195,7 +196,14 @@ def show_diagram( fontsize=font_size_str, ) + # Set layout + if engine is None: + engine = "dot" + + graph.layout(prog=engine) + # Write diagram to file. PNG, SVG, etc. + # TODO gotta fix this if path is not None: save_path_final: pathlib.Path = pathlib.Path(path) @@ -226,11 +234,29 @@ def _get_input_path( f"_get_input_path is not implemented for {self.__class__}" ) - def _ipython_display_(self) -> None: - if not _visual_imports: - raise ImportError("Missing visual imports.") - - IPython.display.display(self.show_diagram()) + def _repr_mimebundle_( + self, + include: Optional[Iterable[str]] = None, + exclude: Optional[Iterable[str]] = None, + **_, + ) -> Dict[str, Union[bytes, str]]: + DEFAULT_FORMATS = {"image/svg+xml"} + # TODO add proper support for include/exclude + # DEFAULT_FORMATS = frozenset(("jpeg", "jpg", "png", "svg")) + + diagram_graph = self.show_diagram() + + include = set(include) if include is not None else DEFAULT_FORMATS + include -= set(exclude or set()) + + return {"image/svg+xml": diagram_graph.draw(format="svg").decode("utf-8")} + + # def _ipython_display_(self) -> None: + # if not _visual_imports: + # raise ImportError("Missing visual imports.") + # from IPython.display import SVG + # + # IPython.display.display(SVG(self.show_diagram().draw(format="svg"))) @staticmethod def _add_new_state(state_set: Set[FAStateT], start: int = 0) -> int: From 01a897edfca611c55986d94873ec6d2b3d8b2135 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 13:28:04 -0500 Subject: [PATCH 47/83] Update requirements --- automata/fa/fa.py | 2 -- requirements.txt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index dd86255b..07eb1fd7 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -12,8 +12,6 @@ # Optional imports for use with visual functionality try: import coloraide - import graphviz - import IPython.display import pygraphviz as pgv except ImportError: _visual_imports = False diff --git a/requirements.txt b/requirements.txt index 5ea292bd..73d5dc32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ flake8-black==0.3.6 flake8-isort==6.0.0 Flake8-pyproject==1.2.3 frozendict==2.3.4 -graphviz==0.20.1 +pygraphviz==1.10 isort==5.10.1 mccabe==0.7.0 mypy==1.1.1 From 479bbd2823031824920aaa250d531198bc08506e Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 13:30:32 -0500 Subject: [PATCH 48/83] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 73d5dc32..e60e3ecf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ flake8-black==0.3.6 flake8-isort==6.0.0 Flake8-pyproject==1.2.3 frozendict==2.3.4 -pygraphviz==1.10 +pygraphviz==1.9 isort==5.10.1 mccabe==0.7.0 mypy==1.1.1 From 7b9def20ba06811e666e3d6159f4b72090cf9109 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 13:48:40 -0500 Subject: [PATCH 49/83] Update tests.yml --- .github/workflows/tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 62b05cc9..c7b766c1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,8 +27,8 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install graphviz - run: sudo apt-get install graphviz + - name: Setup Graphviz + uses: ts-graphviz/setup-graphviz@v1 - name: Install dependencies run: | From b212b0566a497986013dda1c59acabcadf5fae6e Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 14:06:54 -0500 Subject: [PATCH 50/83] Modify tests --- .github/workflows/lint.yml | 4 ++-- automata/fa/fa.py | 15 +++++++-------- requirements.txt | 2 +- tests/test_nfa.py | 12 ++++++------ 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1cb65971..6ab7346b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,8 +23,8 @@ jobs: with: python-version: "3.11" - - name: Install graphviz - run: sudo apt-get install graphviz + - name: Setup Graphviz + uses: ts-graphviz/setup-graphviz@v1 - name: Install dependencies run: | diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 07eb1fd7..413089b1 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -205,19 +205,18 @@ def show_diagram( if path is not None: save_path_final: pathlib.Path = pathlib.Path(path) - directory = save_path_final.parent - directory.mkdir(parents=True, exist_ok=True) - filename = save_path_final.stem + # directory = save_path_final.parent + # directory.mkdir(parents=True, exist_ok=True) + # filename = save_path_final.stem format = ( save_path_final.suffix.split(".")[1] if save_path_final.suffix else None ) - graph.render( - directory=directory, - filename=filename, + graph.draw( + path=save_path_final, format=format, - cleanup=cleanup, - view=view, + # cleanup=cleanup, + # view=view, ) return graph diff --git a/requirements.txt b/requirements.txt index e60e3ecf..73d5dc32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ flake8-black==0.3.6 flake8-isort==6.0.0 Flake8-pyproject==1.2.3 frozendict==2.3.4 -pygraphviz==1.9 +pygraphviz==1.10 isort==5.10.1 mccabe==0.7.0 mypy==1.1.1 diff --git a/tests/test_nfa.py b/tests/test_nfa.py index 53699c1a..0c039a4f 100644 --- a/tests/test_nfa.py +++ b/tests/test_nfa.py @@ -596,13 +596,13 @@ def test_show_diagram_initial_final_same(self) -> None: final_states={"q0", "q1"}, ) graph = nfa.show_diagram() - self.assertEqual( - {node.get_name() for node in graph.get_nodes()}, {"q0", "q1", "q2"} + self.assertTrue( + {"q0", "q1", "q2"}.issubset({node.get_name() for node in graph.nodes()}) ) - self.assertEqual(graph.get_node("q0")[0].get_style(), "filled") - self.assertEqual(graph.get_node("q0")[0].get_peripheries(), 2) - self.assertEqual(graph.get_node("q1")[0].get_peripheries(), 2) - self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) + self.assertEqual(graph.get_node("q0").attr["style"], "filled") + # self.assertEqual(graph.get_node("q0")[0].get_peripheries(), 2) + # self.assertEqual(graph.get_node("q1")[0].get_peripheries(), 2) + # self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) self.assertEqual( { (edge.get_source(), edge.get_label(), edge.get_destination()) From 9f1f8af93e3ef729230372c0304cc093003deed4 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 14:11:15 -0500 Subject: [PATCH 51/83] Typing fixes --- automata/fa/fa.py | 1 + pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 413089b1..8ef065a8 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -77,6 +77,7 @@ def show_diagram( font_size: float = 14.0, arrow_size: float = 0.85, state_separation: float = 0.5, + show_None=None, # TODO fix this ) -> pgv.AGraph: """ Generates the graph associated with the given DFA. diff --git a/pyproject.toml b/pyproject.toml index 08ae4cd9..44dfb911 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ maintainers = [ dependencies = [ "coloraide>=1.8.2", "frozendict>=2.3.4", - "graphviz>=0.20.1", + "pygraphviz>=1.10", "networkx>=2.6.2", "frozendict>=2.3.4", #TODO I think that typing extensions needs to be in here ] @@ -38,7 +38,7 @@ exclude = ["build"] module = [ "setuptools.*", "networkx.*", - "graphviz.*" # TODO pydot dependency is going to get removed, can delete + "pygraphviz.*" # TODO pydot dependency is going to get removed, can delete ] ignore_missing_imports = true From 91b0af28f5fc9df63d25bbae149d17f51283504a Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 29 Apr 2023 14:15:46 -0500 Subject: [PATCH 52/83] Update fa.py --- automata/fa/fa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 8ef065a8..5a8df1a9 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -5,7 +5,7 @@ import pathlib import uuid from collections import defaultdict -from typing import Any, Dict, Generator, Iterable, List, Optional, Set, Tuple, Union +from typing import Dict, Generator, Iterable, List, Optional, Set, Tuple, Union from automata.base.automaton import Automaton, AutomatonStateT From d0c64520bd54944103ec383279dd446d24696eb2 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 09:29:35 -0600 Subject: [PATCH 53/83] Cleaned up display logic --- automata/fa/fa.py | 41 ++++++----------------------------------- automata/fa/gnfa.py | 4 ++-- automata/fa/nfa.py | 8 ++++++-- pyproject.toml | 12 +++++++----- 4 files changed, 21 insertions(+), 44 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 5a8df1a9..f3da0b7e 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -5,7 +5,7 @@ import pathlib import uuid from collections import defaultdict -from typing import Dict, Generator, Iterable, List, Optional, Set, Tuple, Union +from typing import Dict, Generator, List, Optional, Set, Tuple, Union from automata.base.automaton import Automaton, AutomatonStateT @@ -69,15 +69,13 @@ def show_diagram( path: Union[str, os.PathLike, None] = None, *, engine: Optional[str] = None, - view=False, - cleanup: bool = True, horizontal: bool = True, reverse_orientation: bool = False, fig_size: Optional[Tuple] = None, font_size: float = 14.0, arrow_size: float = 0.85, state_separation: float = 0.5, - show_None=None, # TODO fix this + show_None=True, # TODO fix this ) -> pgv.AGraph: """ Generates the graph associated with the given DFA. @@ -85,18 +83,14 @@ def show_diagram( input_str (str, optional): String list of input symbols. Defaults to None. - path (str or os.PathLike, optional): Path to output file. If None, the output will not be saved. - - view (bool, optional): Storing and displaying the graph as a pdf. - Defaults to False. - - cleanup (bool, optional): Garbage collection. Defaults to True. - horizontal (bool, optional): Direction of node layout. Defaults + - horizontal (bool, optional): Direction of node layout. Defaults to True. - reverse_orientation (bool, optional): Reverse direction of node layout. Defaults to False. - fig_size (tuple, optional): Figure size. Defaults to None. - font_size (float, optional): Font size. Defaults to 14.0. - arrow_size (float, optional): Arrow head size. Defaults to 0.85. - - state_separation (float, optional): Node distance. Defaults to 0 - 5. + - state_separation (float, optional): Node distance. Defaults to 0.5. Returns: Digraph: The graph in dot format. """ @@ -216,8 +210,6 @@ def show_diagram( graph.draw( path=save_path_final, format=format, - # cleanup=cleanup, - # view=view, ) return graph @@ -232,29 +224,8 @@ def _get_input_path( f"_get_input_path is not implemented for {self.__class__}" ) - def _repr_mimebundle_( - self, - include: Optional[Iterable[str]] = None, - exclude: Optional[Iterable[str]] = None, - **_, - ) -> Dict[str, Union[bytes, str]]: - DEFAULT_FORMATS = {"image/svg+xml"} - # TODO add proper support for include/exclude - # DEFAULT_FORMATS = frozenset(("jpeg", "jpg", "png", "svg")) - - diagram_graph = self.show_diagram() - - include = set(include) if include is not None else DEFAULT_FORMATS - include -= set(exclude or set()) - - return {"image/svg+xml": diagram_graph.draw(format="svg").decode("utf-8")} - - # def _ipython_display_(self) -> None: - # if not _visual_imports: - # raise ImportError("Missing visual imports.") - # from IPython.display import SVG - # - # IPython.display.display(SVG(self.show_diagram().draw(format="svg"))) + def _repr_mimebundle_(self, *args, **kwargs) -> Dict[str, Union[bytes, str]]: + return self.show_diagram()._repr_mimebundle_(*args, **kwargs) @staticmethod def _add_new_state(state_set: Set[FAStateT], start: int = 0) -> int: diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index fcf16301..30b4509d 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -344,8 +344,8 @@ def iter_transitions( ) @cached_property - def final_states(self): - return frozenset({self.final_state}) + def final_states(self) -> AbstractSet[GNFAStateT]: + return frozenset((self.final_state,)) def _get_input_path(self, input_str: str) -> NoReturn: raise NotImplementedError( diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 163bd7c9..873922f0 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -988,7 +988,9 @@ def _get_input_path( visiting = set() - def gen_paths_for(state, step): + def gen_paths_for( + state: NFAStateT, step: int + ) -> Generator[List[Tuple[NFAStateT, NFAStateT, str]], None, None]: """ Generate all the possible paths from state after taking step n. """ @@ -1006,7 +1008,9 @@ def gen_paths_for(state, step): yield [(state, next_state, "")] + path, accepted, steps @functools.cache - def find_best_path(state, step): + def find_best_path( + state: NFAStateT, step: int + ) -> List[Tuple[NFAStateT, NFAStateT, str]]: """ Try all the possible paths from state after taking step n. A path is better if (with priority): diff --git a/pyproject.toml b/pyproject.toml index 44dfb911..c1052840 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,13 +13,15 @@ maintainers = [ {name = 'Caleb Evans', email = 'caleb@calebevans.me'} ] dependencies = [ - "coloraide>=1.8.2", - "frozendict>=2.3.4", - "pygraphviz>=1.10", "networkx>=2.6.2", - "frozendict>=2.3.4", #TODO I think that typing extensions needs to be in here + "frozendict>=2.3.4", + "typing-extensions>=4.5.0" ] +# Per https://setuptools.pypa.io/en/latest/userguide/dependency_management.html#optional-dependencies +[project.optional-dependencies] +visual = ["coloraide>=1.8.2", "pygraphviz>=1.10"] + #TODO maybe add stuff from here?: https://blog.whtsky.me/tech/2021/dont-forget-py.typed-for-your-typed-python-package/#adding-pytyped [project.urls] @@ -38,7 +40,7 @@ exclude = ["build"] module = [ "setuptools.*", "networkx.*", - "pygraphviz.*" # TODO pydot dependency is going to get removed, can delete + "pygraphviz.*" ] ignore_missing_imports = true From a48387e4ff2b65537e584ebf999e52da4c85a5e7 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 09:33:46 -0600 Subject: [PATCH 54/83] Type fix --- automata/fa/nfa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 873922f0..a1e0cfd6 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -1010,7 +1010,7 @@ def gen_paths_for( @functools.cache def find_best_path( state: NFAStateT, step: int - ) -> List[Tuple[NFAStateT, NFAStateT, str]]: + ) -> Tuple[List[Tuple[NFAStateT, NFAStateT, str]], bool, int]: """ Try all the possible paths from state after taking step n. A path is better if (with priority): From b7ffb65276f26faddc36b56ceec0367eb08ab9ad Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 09:54:17 -0600 Subject: [PATCH 55/83] Type fix --- automata/fa/nfa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index a1e0cfd6..aae629ad 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -990,7 +990,7 @@ def _get_input_path( def gen_paths_for( state: NFAStateT, step: int - ) -> Generator[List[Tuple[NFAStateT, NFAStateT, str]], None, None]: + ) -> Generator[Tuple[List[Tuple[NFAStateT, NFAStateT, str]], bool, int], None, None]: """ Generate all the possible paths from state after taking step n. """ From d8dbcae41b4419846f054b0138e0836e538b3945 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 09:54:29 -0600 Subject: [PATCH 56/83] Update nfa.py --- automata/fa/nfa.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index aae629ad..8215a306 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -990,7 +990,9 @@ def _get_input_path( def gen_paths_for( state: NFAStateT, step: int - ) -> Generator[Tuple[List[Tuple[NFAStateT, NFAStateT, str]], bool, int], None, None]: + ) -> Generator[ + Tuple[List[Tuple[NFAStateT, NFAStateT, str]], bool, int], None, None + ]: """ Generate all the possible paths from state after taking step n. """ From 37c5e6af0e2aea85057b56858c3b57095db4089a Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 10:04:51 -0600 Subject: [PATCH 57/83] Removed caching because of mypy issues --- automata/fa/gnfa.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 30b4509d..0ebbf484 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -64,6 +64,7 @@ def __init__( ), initial_state=initial_state, final_state=final_state, + final_states=frozenset((final_state,)), ) # GNFA should NOT create the lambda closures via NFA.__post_init__() @@ -343,10 +344,6 @@ def iter_transitions( if symbol is not None ) - @cached_property - def final_states(self) -> AbstractSet[GNFAStateT]: - return frozenset((self.final_state,)) - def _get_input_path(self, input_str: str) -> NoReturn: raise NotImplementedError( f"_get_input_path is not implemented for {self.__class__}" From 119e9424bd574991c84f6a9e569e48a5b22ee6cc Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 10:07:06 -0600 Subject: [PATCH 58/83] Fixed GNFA freezing --- automata/fa/gnfa.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 0ebbf484..2f647867 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -55,16 +55,14 @@ def __init__( initial_state: GNFAStateT, final_state: GNFAStateT, ) -> None: - """Initialize a complete NFA.""" + """Initialize a complete GNFA.""" super(fa.FA, self).__init__( - states=frozenset(states), - input_symbols=frozenset(input_symbols), - transitions=frozendict( - {state: frozendict(paths) for state, paths in transitions.items()} - ), + states=states, + input_symbols=input_symbols, + transitions=transitions, initial_state=initial_state, final_state=final_state, - final_states=frozenset((final_state,)), + final_states={final_state}, ) # GNFA should NOT create the lambda closures via NFA.__post_init__() From abb13e3d9e654a3ccb8f28e2d240322ceffc2cfe Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 10:07:40 -0600 Subject: [PATCH 59/83] import fix --- automata/fa/gnfa.py | 1 - 1 file changed, 1 deletion(-) diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 2f647867..0e8dda37 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -3,7 +3,6 @@ automata.""" from __future__ import annotations -from functools import cached_property from itertools import product from typing import ( AbstractSet, From 5c44c0e459dd27035728a6b46a7bafccfdbf7631 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 10:08:46 -0600 Subject: [PATCH 60/83] Update gnfa.py --- automata/fa/gnfa.py | 1 - 1 file changed, 1 deletion(-) diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 0e8dda37..8326df0f 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -16,7 +16,6 @@ cast, ) -from frozendict import frozendict from typing_extensions import NoReturn, Self import automata.base.exceptions as exceptions From 854b753fe11584bca690477ae513ecaedb37904b Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 15:02:17 -0600 Subject: [PATCH 61/83] Updated DFA display test --- automata/fa/fa.py | 2 ++ tests/test_dfa.py | 44 ++++++++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index f3da0b7e..f660b066 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -1,5 +1,7 @@ #!/usr/bin/env python3 """Classes and methods for working with all finite automata.""" +from __future__ import annotations + import abc import os import pathlib diff --git a/tests/test_dfa.py b/tests/test_dfa.py index 75e268a6..a7ad9f0b 100644 --- a/tests/test_dfa.py +++ b/tests/test_dfa.py @@ -1519,26 +1519,30 @@ def test_show_diagram_initial_final_different(self) -> None: is not a final state. """ graph = self.dfa.show_diagram() - self.assertEqual( - {node.get_name() for node in graph.get_nodes()}, {"q0", "q1", "q2"} - ) - self.assertEqual(graph.get_node("q0")[0].get_style(), "filled") - self.assertEqual(graph.get_node("q1")[0].get_peripheries(), 2) - self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) - self.assertEqual( - { - (edge.get_source(), edge.get_label(), edge.get_destination()) - for edge in graph.get_edges() - }, - { - ("q0", "0", "q0"), - ("q0", "1", "q1"), - ("q1", "0", "q0"), - ("q1", "1", "q2"), - ("q2", "0", "q2"), - ("q2", "1", "q1"), - }, - ) + node_names = {node.get_name() for node in graph.nodes()} + self.assertTrue({"q0", "q1", "q2"}.issubset(node_names)) + self.assertEqual(4, len(node_names)) + + for state in self.dfa.states: + node = graph.get_node(state) + expected_shape = ( + "doublecircle" if state in self.dfa.final_states else "circle" + ) + self.assertEqual(node.attr["shape"], expected_shape) + + expected_transitions = { + ("q0", "0", "q0"), + ("q0", "1", "q1"), + ("q1", "0", "q0"), + ("q1", "1", "q2"), + ("q2", "0", "q2"), + ("q2", "1", "q1"), + } + seen_transitions = { + (edge[0], edge.attr["label"], edge[1]) for edge in graph.edges() + } + self.assertTrue(expected_transitions.issubset(seen_transitions)) + self.assertEqual(len(expected_transitions) + 1, len(seen_transitions)) def test_show_diagram_initial_final_same(self) -> None: """ From c8f60c416c332f2f12e26627116dd66291d58272 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 15:10:34 -0600 Subject: [PATCH 62/83] Updated DFA display tests --- tests/test_dfa.py | 49 +++++++++++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/tests/test_dfa.py b/tests/test_dfa.py index a7ad9f0b..e7c99f26 100644 --- a/tests/test_dfa.py +++ b/tests/test_dfa.py @@ -1520,8 +1520,8 @@ def test_show_diagram_initial_final_different(self) -> None: """ graph = self.dfa.show_diagram() node_names = {node.get_name() for node in graph.nodes()} - self.assertTrue({"q0", "q1", "q2"}.issubset(node_names)) - self.assertEqual(4, len(node_names)) + self.assertTrue(self.dfa.states.issubset(node_names)) + self.assertEqual(len(self.dfa.states)+1, len(node_names)) for state in self.dfa.states: node = graph.get_node(state) @@ -1562,28 +1562,31 @@ def test_show_diagram_initial_final_same(self) -> None: initial_state="q0", final_states={"q0", "q1"}, ) + graph = dfa.show_diagram() - self.assertEqual( - {node.get_name() for node in graph.get_nodes()}, {"q0", "q1", "q2"} - ) - self.assertEqual(graph.get_node("q0")[0].get_style(), "filled") - self.assertEqual(graph.get_node("q0")[0].get_peripheries(), 2) - self.assertEqual(graph.get_node("q1")[0].get_peripheries(), 2) - self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) - self.assertEqual( - { - (edge.get_source(), edge.get_label(), edge.get_destination()) - for edge in graph.get_edges() - }, - { - ("q0", "0", "q0"), - ("q0", "1", "q1"), - ("q1", "0", "q0"), - ("q1", "1", "q2"), - ("q2", "0", "q2"), - ("q2", "1", "q2"), - }, - ) + node_names = {node.get_name() for node in graph.nodes()} + self.assertTrue({"q0", "q1", "q2"}.issubset(node_names)) + self.assertEqual(len(dfa.states)+1, len(node_names)) + + for state in self.dfa.states: + node = graph.get_node(state) + expected_shape = ( + "doublecircle" if state in dfa.final_states else "circle" + ) + self.assertEqual(node.attr["shape"], expected_shape) + + expected_transitions = { + ("q0", "0", "q0"), + ("q0", "1", "q1"), + ("q1", "0", "q0"), + ("q1", "1", "q2"), + ("q2", "0,1", "q2"), + } + seen_transitions = { + (edge[0], edge.attr["label"], edge[1]) for edge in graph.edges() + } + self.assertTrue(expected_transitions.issubset(seen_transitions)) + self.assertEqual(len(expected_transitions) + 1, len(seen_transitions)) def test_show_diagram_write_file(self) -> None: """ From 820136f75e12f3f6b0680dd33b3c57e567fdb7f8 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 16:26:08 -0600 Subject: [PATCH 63/83] mypy fix --- automata/fa/fa.py | 1 + tests/test_dfa.py | 22 +++++++++++++++------- tests/test_nfa.py | 41 +++++++++++++++++++++++++++-------------- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index f660b066..dbe076bf 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -111,6 +111,7 @@ def show_diagram( font_size_str = str(font_size) arrow_size_str = str(arrow_size) + # TODO missing case here if horizontal: graph.graph_attr.update(rankdir="LR") if reverse_orientation: diff --git a/tests/test_dfa.py b/tests/test_dfa.py index e7c99f26..e30851c8 100644 --- a/tests/test_dfa.py +++ b/tests/test_dfa.py @@ -1520,8 +1520,8 @@ def test_show_diagram_initial_final_different(self) -> None: """ graph = self.dfa.show_diagram() node_names = {node.get_name() for node in graph.nodes()} - self.assertTrue(self.dfa.states.issubset(node_names)) - self.assertEqual(len(self.dfa.states)+1, len(node_names)) + self.assertTrue(set(self.dfa.states).issubset(node_names)) + self.assertEqual(len(self.dfa.states) + 1, len(node_names)) for state in self.dfa.states: node = graph.get_node(state) @@ -1544,6 +1544,11 @@ def test_show_diagram_initial_final_different(self) -> None: self.assertTrue(expected_transitions.issubset(seen_transitions)) self.assertEqual(len(expected_transitions) + 1, len(seen_transitions)) + source, symbol, dest = list(seen_transitions - expected_transitions)[0] + self.assertEqual(symbol, "") + self.assertEqual(dest, self.dfa.initial_state) + self.assertTrue(source not in self.dfa.states) + def test_show_diagram_initial_final_same(self) -> None: """ Should construct the diagram for a DFA whose initial state @@ -1565,14 +1570,12 @@ def test_show_diagram_initial_final_same(self) -> None: graph = dfa.show_diagram() node_names = {node.get_name() for node in graph.nodes()} - self.assertTrue({"q0", "q1", "q2"}.issubset(node_names)) - self.assertEqual(len(dfa.states)+1, len(node_names)) + self.assertTrue(set(dfa.states).issubset(node_names)) + self.assertEqual(len(dfa.states) + 1, len(node_names)) for state in self.dfa.states: node = graph.get_node(state) - expected_shape = ( - "doublecircle" if state in dfa.final_states else "circle" - ) + expected_shape = "doublecircle" if state in dfa.final_states else "circle" self.assertEqual(node.attr["shape"], expected_shape) expected_transitions = { @@ -1588,6 +1591,11 @@ def test_show_diagram_initial_final_same(self) -> None: self.assertTrue(expected_transitions.issubset(seen_transitions)) self.assertEqual(len(expected_transitions) + 1, len(seen_transitions)) + source, symbol, dest = list(seen_transitions - expected_transitions)[0] + self.assertEqual(symbol, "") + self.assertEqual(dest, dfa.initial_state) + self.assertTrue(source not in dfa.states) + def test_show_diagram_write_file(self) -> None: """ Should construct the diagram for a DFA diff --git a/tests/test_nfa.py b/tests/test_nfa.py index 0c039a4f..2d969a53 100644 --- a/tests/test_nfa.py +++ b/tests/test_nfa.py @@ -595,21 +595,34 @@ def test_show_diagram_initial_final_same(self) -> None: initial_state="q0", final_states={"q0", "q1"}, ) + graph = nfa.show_diagram() - self.assertTrue( - {"q0", "q1", "q2"}.issubset({node.get_name() for node in graph.nodes()}) - ) - self.assertEqual(graph.get_node("q0").attr["style"], "filled") - # self.assertEqual(graph.get_node("q0")[0].get_peripheries(), 2) - # self.assertEqual(graph.get_node("q1")[0].get_peripheries(), 2) - # self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) - self.assertEqual( - { - (edge.get_source(), edge.get_label(), edge.get_destination()) - for edge in graph.get_edges() - }, - {("q0", "a", "q1"), ("q1", "a", "q1"), ("q1", "", "q2"), ("q2", "b", "q0")}, - ) + node_names = {node.get_name() for node in graph.nodes()} + self.assertTrue(nfa.states.issubset(node_names)) + self.assertEqual(len(nfa.states) + 1, len(node_names)) + + for state in self.dfa.states: + node = graph.get_node(state) + expected_shape = "doublecircle" if state in nfa.final_states else "circle" + self.assertEqual(node.attr["shape"], expected_shape) + + expected_transitions = { + ("q0", "a", "q1"), + ("q1", "a", "q1"), + ("q1", "ε", "q2"), + ("q2", "b", "q0"), + } + seen_transitions = { + (edge[0], edge.attr["label"], edge[1]) for edge in graph.edges() + } + + self.assertTrue(expected_transitions.issubset(seen_transitions)) + self.assertEqual(len(expected_transitions) + 1, len(seen_transitions)) + + source, symbol, dest = list(seen_transitions - expected_transitions)[0] + self.assertEqual(symbol, "") + self.assertEqual(dest, self.nfa.initial_state) + self.assertTrue(source not in self.dfa.states) def test_show_diagram_write_file(self) -> None: """ From 1987dbe460545db6bfdd2b960682782d5944cb47 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 16:27:28 -0600 Subject: [PATCH 64/83] Update test_nfa.py --- tests/test_nfa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_nfa.py b/tests/test_nfa.py index 2d969a53..452e4d4f 100644 --- a/tests/test_nfa.py +++ b/tests/test_nfa.py @@ -598,7 +598,7 @@ def test_show_diagram_initial_final_same(self) -> None: graph = nfa.show_diagram() node_names = {node.get_name() for node in graph.nodes()} - self.assertTrue(nfa.states.issubset(node_names)) + self.assertTrue(set(nfa.states).issubset(node_names)) self.assertEqual(len(nfa.states) + 1, len(node_names)) for state in self.dfa.states: From 954b57e150462c597897e52e655c4a33e3a788ec Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 16:31:25 -0600 Subject: [PATCH 65/83] Commented out GNFA test to get coverage report --- tests/test_gnfa.py | 95 +++++++++++++++++++++++----------------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/tests/test_gnfa.py b/tests/test_gnfa.py index 54d1ac81..b4c6aea9 100644 --- a/tests/test_gnfa.py +++ b/tests/test_gnfa.py @@ -376,28 +376,28 @@ def test_show_diagram_showNone(self) -> None: """ Should construct the diagram for a GNFA when show_None = False """ - + # TODO update this gnfa = self.gnfa graph = gnfa.show_diagram(show_None=False) - self.assertEqual({node.get_name() for node in graph.get_nodes()}, gnfa.states) - self.assertEqual(graph.get_node(gnfa.initial_state)[0].get_style(), "filled") - self.assertEqual(graph.get_node(gnfa.final_state)[0].get_peripheries(), 2) - self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) - self.assertEqual( - { - (edge.get_source(), edge.get_label(), edge.get_destination()) - for edge in graph.get_edges() - }, - { - ("q0", "a", "q1"), - ("q1", "a", "q1"), - ("q1", "", "q2"), - ("q1", "", "q_f"), - ("q2", "b", "q0"), - ("q_in", "", "q0"), - }, - ) + # self.assertEqual({node.get_name() for node in graph.get_nodes()}, gnfa.states) + # self.assertEqual(graph.get_node(gnfa.initial_state)[0].get_style(), "filled") + # self.assertEqual(graph.get_node(gnfa.final_state)[0].get_peripheries(), 2) + # self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) + # self.assertEqual( + # { + # (edge.get_source(), edge.get_label(), edge.get_destination()) + # for edge in graph.get_edges() + # }, + # { + # ("q0", "a", "q1"), + # ("q1", "a", "q1"), + # ("q1", "", "q2"), + # ("q1", "", "q_f"), + # ("q2", "b", "q0"), + # ("q_in", "", "q0"), + # }, + # ) def test_show_diagram(self) -> None: """ @@ -406,35 +406,36 @@ def test_show_diagram(self) -> None: gnfa = self.gnfa + # TODO update this graph = gnfa.show_diagram() - self.assertEqual({node.get_name() for node in graph.get_nodes()}, gnfa.states) - self.assertEqual(graph.get_node(gnfa.initial_state)[0].get_style(), "filled") - self.assertEqual(graph.get_node(gnfa.final_state)[0].get_peripheries(), 2) - self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) - self.assertEqual( - { - (edge.get_source(), edge.get_label(), edge.get_destination()) - for edge in graph.get_edges() - }, - { - ("q_in", "", "q0"), - ("q0", "ø", "q2"), - ("q1", "", "q2"), - ("q0", "ø", "q_f"), - ("q1", "", "q_f"), - ("q_in", "ø", "q2"), - ("q_in", "ø", "q1"), - ("q1", "a", "q1"), - ("q2", "b", "q0"), - ("q2", "ø", "q2"), - ("q_in", "ø", "q_f"), - ("q2", "ø", "q1"), - ("q0", "ø", "q0"), - ("q2", "ø", "q_f"), - ("q0", "a", "q1"), - ("q1", "ø", "q0"), - }, - ) + # self.assertEqual({node.get_name() for node in graph.get_nodes()}, gnfa.states) + # self.assertEqual(graph.get_node(gnfa.initial_state)[0].get_style(), "filled") + # self.assertEqual(graph.get_node(gnfa.final_state)[0].get_peripheries(), 2) + # self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) + # self.assertEqual( + # { + # (edge.get_source(), edge.get_label(), edge.get_destination()) + # for edge in graph.get_edges() + # }, + # { + # ("q_in", "", "q0"), + # ("q0", "ø", "q2"), + # ("q1", "", "q2"), + # ("q0", "ø", "q_f"), + # ("q1", "", "q_f"), + # ("q_in", "ø", "q2"), + # ("q_in", "ø", "q1"), + # ("q1", "a", "q1"), + # ("q2", "b", "q0"), + # ("q2", "ø", "q2"), + # ("q_in", "ø", "q_f"), + # ("q2", "ø", "q1"), + # ("q0", "ø", "q0"), + # ("q2", "ø", "q_f"), + # ("q0", "a", "q1"), + # ("q1", "ø", "q0"), + # }, + # ) def test_show_diagram_write_file(self) -> None: """ From 2322f2418511baeafa38659d03a52f22749a54c1 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 16:35:58 -0600 Subject: [PATCH 66/83] lint --- tests/test_gnfa.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_gnfa.py b/tests/test_gnfa.py index b4c6aea9..84109896 100644 --- a/tests/test_gnfa.py +++ b/tests/test_gnfa.py @@ -377,9 +377,9 @@ def test_show_diagram_showNone(self) -> None: Should construct the diagram for a GNFA when show_None = False """ # TODO update this - gnfa = self.gnfa + # gnfa = self.gnfa - graph = gnfa.show_diagram(show_None=False) + # graph = gnfa.show_diagram(show_None=False) # self.assertEqual({node.get_name() for node in graph.get_nodes()}, gnfa.states) # self.assertEqual(graph.get_node(gnfa.initial_state)[0].get_style(), "filled") # self.assertEqual(graph.get_node(gnfa.final_state)[0].get_peripheries(), 2) @@ -404,10 +404,10 @@ def test_show_diagram(self) -> None: Should construct the diagram for a GNFA when show_None = True """ - gnfa = self.gnfa + # gnfa = self.gnfa # TODO update this - graph = gnfa.show_diagram() + # graph = gnfa.show_diagram() # self.assertEqual({node.get_name() for node in graph.get_nodes()}, gnfa.states) # self.assertEqual(graph.get_node(gnfa.initial_state)[0].get_style(), "filled") # self.assertEqual(graph.get_node(gnfa.final_state)[0].get_peripheries(), 2) From 89656e2c599d6a70bc31a3268261cb5d928f3089 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 19:21:24 -0600 Subject: [PATCH 67/83] Updated GNFA test cases --- automata/fa/fa.py | 1 - tests/test_gnfa.py | 93 ++++++++++++++++------------------------------ tests/test_nfa.py | 2 +- 3 files changed, 34 insertions(+), 62 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index dbe076bf..8cfa2a46 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -77,7 +77,6 @@ def show_diagram( font_size: float = 14.0, arrow_size: float = 0.85, state_separation: float = 0.5, - show_None=True, # TODO fix this ) -> pgv.AGraph: """ Generates the graph associated with the given DFA. diff --git a/tests/test_gnfa.py b/tests/test_gnfa.py index 84109896..cbc63460 100644 --- a/tests/test_gnfa.py +++ b/tests/test_gnfa.py @@ -372,70 +372,43 @@ def test_to_regex(self) -> None: # Test equality through DFA regex conversion self.assertEqual(dfa_1, dfa_2) - def test_show_diagram_showNone(self) -> None: - """ - Should construct the diagram for a GNFA when show_None = False - """ - # TODO update this - # gnfa = self.gnfa - - # graph = gnfa.show_diagram(show_None=False) - # self.assertEqual({node.get_name() for node in graph.get_nodes()}, gnfa.states) - # self.assertEqual(graph.get_node(gnfa.initial_state)[0].get_style(), "filled") - # self.assertEqual(graph.get_node(gnfa.final_state)[0].get_peripheries(), 2) - # self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) - # self.assertEqual( - # { - # (edge.get_source(), edge.get_label(), edge.get_destination()) - # for edge in graph.get_edges() - # }, - # { - # ("q0", "a", "q1"), - # ("q1", "a", "q1"), - # ("q1", "", "q2"), - # ("q1", "", "q_f"), - # ("q2", "b", "q0"), - # ("q_in", "", "q0"), - # }, - # ) - def test_show_diagram(self) -> None: """ - Should construct the diagram for a GNFA when show_None = True + Should construct the diagram for a GNFA. """ - # gnfa = self.gnfa - - # TODO update this - # graph = gnfa.show_diagram() - # self.assertEqual({node.get_name() for node in graph.get_nodes()}, gnfa.states) - # self.assertEqual(graph.get_node(gnfa.initial_state)[0].get_style(), "filled") - # self.assertEqual(graph.get_node(gnfa.final_state)[0].get_peripheries(), 2) - # self.assertEqual(graph.get_node("q2")[0].get_peripheries(), None) - # self.assertEqual( - # { - # (edge.get_source(), edge.get_label(), edge.get_destination()) - # for edge in graph.get_edges() - # }, - # { - # ("q_in", "", "q0"), - # ("q0", "ø", "q2"), - # ("q1", "", "q2"), - # ("q0", "ø", "q_f"), - # ("q1", "", "q_f"), - # ("q_in", "ø", "q2"), - # ("q_in", "ø", "q1"), - # ("q1", "a", "q1"), - # ("q2", "b", "q0"), - # ("q2", "ø", "q2"), - # ("q_in", "ø", "q_f"), - # ("q2", "ø", "q1"), - # ("q0", "ø", "q0"), - # ("q2", "ø", "q_f"), - # ("q0", "a", "q1"), - # ("q1", "ø", "q0"), - # }, - # ) + graph = self.gnfa.show_diagram() + + node_names = {node.get_name() for node in graph.nodes()} + self.assertTrue(set(self.gnfa.states).issubset(node_names)) + self.assertEqual(len(self.gnfa.states) + 1, len(node_names)) + + for state in self.dfa.states: + node = graph.get_node(state) + expected_shape = ( + "doublecircle" if state in self.gnfa.final_states else "circle" + ) + self.assertEqual(node.attr["shape"], expected_shape) + + expected_transitions = { + ("q_in", "ε", "q0"), + ("q1", "ε", "q2"), + ("q1", "ε", "q_f"), + ("q1", "a", "q1"), + ("q2", "b", "q0"), + ("q0", "a", "q1"), + } + seen_transitions = { + (edge[0], edge.attr["label"], edge[1]) for edge in graph.edges() + } + + self.assertTrue(expected_transitions.issubset(seen_transitions)) + self.assertEqual(len(expected_transitions) + 1, len(seen_transitions)) + + source, symbol, dest = list(seen_transitions - expected_transitions)[0] + self.assertEqual(symbol, "") + self.assertEqual(dest, self.gnfa.initial_state) + self.assertTrue(source not in self.gnfa.states) def test_show_diagram_write_file(self) -> None: """ diff --git a/tests/test_nfa.py b/tests/test_nfa.py index 452e4d4f..18c1b623 100644 --- a/tests/test_nfa.py +++ b/tests/test_nfa.py @@ -622,7 +622,7 @@ def test_show_diagram_initial_final_same(self) -> None: source, symbol, dest = list(seen_transitions - expected_transitions)[0] self.assertEqual(symbol, "") self.assertEqual(dest, self.nfa.initial_state) - self.assertTrue(source not in self.dfa.states) + self.assertTrue(source not in self.nfa.states) def test_show_diagram_write_file(self) -> None: """ From a1cd41310cd7bb81e8b747624e25673f94ec536f Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 19:30:17 -0600 Subject: [PATCH 68/83] Fix orientation --- automata/fa/fa.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 8cfa2a46..feadb916 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -93,7 +93,7 @@ def show_diagram( - arrow_size (float, optional): Arrow head size. Defaults to 0.85. - state_separation (float, optional): Node distance. Defaults to 0.5. Returns: - Digraph: The graph in dot format. + AGraph corresponding to the given automaton. """ if not _visual_imports: @@ -110,14 +110,12 @@ def show_diagram( font_size_str = str(font_size) arrow_size_str = str(arrow_size) - # TODO missing case here if horizontal: - graph.graph_attr.update(rankdir="LR") - if reverse_orientation: - if horizontal: - graph.graph_attr.update(rankdir="RL") - else: - graph.graph_attr.update(rankdir="BT") + rankdir = "RL" if reverse_orientation else "LR" + else: + rankdir = "BT" if reverse_orientation else "TB" + + graph.graph_attr.update(rankdir=rankdir) # we use a random uuid to make sure that the null node has a # unique id to avoid colliding with other states. From 15fad7828c52c6fe4d8e3815d7b464696c3eaefd Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 19:42:31 -0600 Subject: [PATCH 69/83] Update gnfa.py --- automata/fa/gnfa.py | 1 + 1 file changed, 1 insertion(+) diff --git a/automata/fa/gnfa.py b/automata/fa/gnfa.py index 8326df0f..eafba8f1 100644 --- a/automata/fa/gnfa.py +++ b/automata/fa/gnfa.py @@ -33,6 +33,7 @@ class GNFA(fa.FA): """A generalized nondeterministic finite automaton.""" + # Add __dict__ to deal with inheritance issue and the final_states attribute. __slots__ = ( "states", "input_symbols", From e0bbea07a7934a2af13736d7138c19e864f56deb Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 19:58:05 -0600 Subject: [PATCH 70/83] Layout typing --- automata/fa/fa.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index feadb916..f6ec5b8b 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -7,7 +7,7 @@ import pathlib import uuid from collections import defaultdict -from typing import Dict, Generator, List, Optional, Set, Tuple, Union +from typing import Dict, Generator, List, Literal, Optional, Set, Tuple, Union from automata.base.automaton import Automaton, AutomatonStateT @@ -22,6 +22,7 @@ FAStateT = AutomatonStateT +Layout = Literal["neato", "dot", "twopi", "circo", "fdp", "nop"] class FA(Automaton, metaclass=abc.ABCMeta): @@ -70,7 +71,7 @@ def show_diagram( input_str: Optional[str] = None, path: Union[str, os.PathLike, None] = None, *, - engine: Optional[str] = None, + layout: Layout = "dot", horizontal: bool = True, reverse_orientation: bool = False, fig_size: Optional[Tuple] = None, @@ -190,10 +191,7 @@ def show_diagram( ) # Set layout - if engine is None: - engine = "dot" - - graph.layout(prog=engine) + graph.layout(prog=layout) # Write diagram to file. PNG, SVG, etc. # TODO gotta fix this From f8ed207aec283a673c58dfcbc548fd9edce1ff5f Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 12 May 2023 19:59:18 -0600 Subject: [PATCH 71/83] Rename variable --- automata/fa/fa.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index f6ec5b8b..7445850a 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -22,7 +22,7 @@ FAStateT = AutomatonStateT -Layout = Literal["neato", "dot", "twopi", "circo", "fdp", "nop"] +LayoutMethod = Literal["neato", "dot", "twopi", "circo", "fdp", "nop"] class FA(Automaton, metaclass=abc.ABCMeta): @@ -71,7 +71,7 @@ def show_diagram( input_str: Optional[str] = None, path: Union[str, os.PathLike, None] = None, *, - layout: Layout = "dot", + layout_method: LayoutMethod = "dot", horizontal: bool = True, reverse_orientation: bool = False, fig_size: Optional[Tuple] = None, @@ -191,7 +191,7 @@ def show_diagram( ) # Set layout - graph.layout(prog=layout) + graph.layout(prog=layout_method) # Write diagram to file. PNG, SVG, etc. # TODO gotta fix this From 103420ded11060767f70f86280c90ba1b4464962 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Thu, 18 May 2023 18:19:34 -0500 Subject: [PATCH 72/83] Repr fixes and better state addition --- automata/base/automaton.py | 23 ++++++++++++----------- automata/fa/fa.py | 8 ++++---- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 7f7332ab..86015615 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -127,26 +127,27 @@ def copy(self) -> Self: # Format the given value for string output via repr() or str(); this exists # for the purpose of displaying - def _get_repr_friendly_value(self, value: Any) -> Any: + def _get_repr_friendly_string(self, value: Any) -> str: """ - A helper function to convert the given value / structure into a fully - mutable one by recursively processing said structure and any of its - members, unfreezing them along the way + A helper function to convert immutable data structures into strings for the + corresponding mutable ones. Makes things look nicer in the repr. """ if isinstance(value, frozenset): - return {self._get_repr_friendly_value(element) for element in value} + return repr({self._get_repr_friendly_string(element) for element in value}) elif isinstance(value, frozendict): - return { - dict_key: self._get_repr_friendly_value(dict_value) - for dict_key, dict_value in value.items() - } + return repr( + { + dict_key: self._get_repr_friendly_string(dict_value) + for dict_key, dict_value in value.items() + } + ) else: - return value + return repr(value) def __repr__(self) -> str: """Return a string representation of the automaton.""" values = ", ".join( - f"{attr_name}={self._get_repr_friendly_value(attr_value)!r}" + f"{attr_name}={self._get_repr_friendly_string(attr_value)}" for attr_name, attr_value in self.input_parameters.items() ) return f"{self.__class__.__qualname__}({values})" diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 7445850a..217ac088 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -136,10 +136,10 @@ def show_diagram( arrowsize=arrow_size_str, ) - for state in self.states: - shape = "doublecircle" if state in self.final_states else "circle" - node = self.get_state_name(state) - graph.add_node(node, shape=shape, fontsize=font_size_str) + nonfinal_states = map(self.get_state_name, self.states - self.final_states) + final_states = map(self.get_state_name, self.final_states) + graph.add_nodes_from(nonfinal_states, shape="circle", fontsize=font_size_str) + graph.add_nodes_from(final_states, shape="doublecircle", fontsize=font_size_str) is_edge_drawn = defaultdict(lambda: False) if input_str is not None: From e3c056372e513d9111acb5117007171c899b9a4a Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Thu, 18 May 2023 18:41:13 -0500 Subject: [PATCH 73/83] Fixed failing test --- automata/base/automaton.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 86015615..25a9895d 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -133,13 +133,21 @@ def _get_repr_friendly_string(self, value: Any) -> str: corresponding mutable ones. Makes things look nicer in the repr. """ if isinstance(value, frozenset): - return repr({self._get_repr_friendly_string(element) for element in value}) + return ( + "{" + + ", ".join( + self._get_repr_friendly_string(element) for element in value + ) + + "}" + ) elif isinstance(value, frozendict): - return repr( - { - dict_key: self._get_repr_friendly_string(dict_value) + return ( + "{" + + ", ".join( + f"{dict_key!r}: {self._get_repr_friendly_string(dict_value)}" for dict_key, dict_value in value.items() - } + ) + + "}" ) else: return repr(value) From 45cc69a6f221211b636ac1e4944508ec5b616e46 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Thu, 18 May 2023 18:56:26 -0500 Subject: [PATCH 74/83] Reverted repr friendly value change --- automata/base/automaton.py | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 25a9895d..0845ae60 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -127,35 +127,26 @@ def copy(self) -> Self: # Format the given value for string output via repr() or str(); this exists # for the purpose of displaying - def _get_repr_friendly_string(self, value: Any) -> str: + def _get_repr_friendly_value(self, value): """ - A helper function to convert immutable data structures into strings for the - corresponding mutable ones. Makes things look nicer in the repr. + A helper function to convert the given value / structure into a fully + mutable one by recursively processing said structure and any of its + members, unfreezing them along the way """ if isinstance(value, frozenset): - return ( - "{" - + ", ".join( - self._get_repr_friendly_string(element) for element in value - ) - + "}" - ) + return {self._get_repr_friendly_value(element) for element in value} elif isinstance(value, frozendict): - return ( - "{" - + ", ".join( - f"{dict_key!r}: {self._get_repr_friendly_string(dict_value)}" - for dict_key, dict_value in value.items() - ) - + "}" - ) + return { + dict_key: self._get_repr_friendly_value(dict_value) + for dict_key, dict_value in value.items() + } else: - return repr(value) + return value - def __repr__(self) -> str: + def __repr__(self): """Return a string representation of the automaton.""" values = ", ".join( - f"{attr_name}={self._get_repr_friendly_string(attr_value)}" + f"{attr_name}={self._get_repr_friendly_value(attr_value)!r}" for attr_name, attr_value in self.input_parameters.items() ) return f"{self.__class__.__qualname__}({values})" From 2cd52c269f82aa31612ac2924d9df4de6ad179d5 Mon Sep 17 00:00:00 2001 From: Caleb Evans Date: Thu, 18 May 2023 18:36:35 -0700 Subject: [PATCH 75/83] Restore missing type annotation to _get_repr_friendly_value() --- automata/base/automaton.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 0845ae60..0ef29198 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -127,7 +127,7 @@ def copy(self) -> Self: # Format the given value for string output via repr() or str(); this exists # for the purpose of displaying - def _get_repr_friendly_value(self, value): + def _get_repr_friendly_value(self, value: Any) -> Any: """ A helper function to convert the given value / structure into a fully mutable one by recursively processing said structure and any of its From 0fdf3d0a6ec73ea7433a1e174e865bc40a224106 Mon Sep 17 00:00:00 2001 From: Caleb Evans Date: Thu, 18 May 2023 18:37:15 -0700 Subject: [PATCH 76/83] Restore type annotation to __repr__ --- automata/base/automaton.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automata/base/automaton.py b/automata/base/automaton.py index 0ef29198..7f7332ab 100644 --- a/automata/base/automaton.py +++ b/automata/base/automaton.py @@ -143,7 +143,7 @@ def _get_repr_friendly_value(self, value: Any) -> Any: else: return value - def __repr__(self): + def __repr__(self) -> str: """Return a string representation of the automaton.""" values = ", ".join( f"{attr_name}={self._get_repr_friendly_value(attr_value)!r}" From b0ce2adaf2f11bc39ea3568e683853e2240f3a03 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 19 May 2023 11:40:24 -0500 Subject: [PATCH 77/83] changed config --- .coveragerc | 23 ----------------------- pyproject.toml | 28 ++++++++++++++++++++++++++++ requirements.txt | 2 +- 3 files changed, 29 insertions(+), 24 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 753322bd..00000000 --- a/.coveragerc +++ /dev/null @@ -1,23 +0,0 @@ -# Configuration for coverage.py (https://pypi.python.org/pypi/coverage) - -[run] -# Enable branch coverage -branch = True - -[report] - -# Regexes for lines to exclude from consideration -exclude_lines = - - pragma: no cover - - # Ignore non-runnable code - if __name__ == .__main__.: - - pass - - ... - -# Only check coverage for source files -include = - automata/*/*.py diff --git a/pyproject.toml b/pyproject.toml index c1052840..5efc5561 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,10 @@ dependencies = [ [project.optional-dependencies] visual = ["coloraide>=1.8.2", "pygraphviz>=1.10"] +# TODO add other setup options as needed +[tool.setuptools.package-data] +"automata" = ["py.typed"] + #TODO maybe add stuff from here?: https://blog.whtsky.me/tech/2021/dont-forget-py.typed-for-your-typed-python-package/#adding-pytyped [project.urls] @@ -52,3 +56,27 @@ extend-ignore = ["E203", "W503"] # Per [tool.isort] profile = "black" + + +# Configuration for coverage.py (https://pypi.python.org/pypi/coverage) + +[tool.coverage.run] +# Enable branch coverage +branch = true + +[tool.coverage.report] + +# TODO I commented this out because otherwise, the coverage report always read 100% +# Have to fix this and figure out what's going on + +# Regexes for lines to exclude from consideration +#exclude_lines = [ +# "pragma: no cover", +# # Ignore non-runnable code +# "if __name__ == .__main__.:", +# "pass", +# "...", +#] + +# Only check coverage for source files +include =["automata/*/*.py"] diff --git a/requirements.txt b/requirements.txt index 73d5dc32..9883044c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ black==23.3.0 click==8.1.3 coloraide==1.8.2 -coverage==6.4.4 +coverage[toml]==6.4.4 flake8==6.0.0 flake8-black==0.3.6 flake8-isort==6.0.0 From 22317ac972e19b9499fa7dde0891bb6c2f0282a5 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Fri, 19 May 2023 19:11:01 -0500 Subject: [PATCH 78/83] Update pyproject.toml --- pyproject.toml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5efc5561..50911852 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,13 +70,12 @@ branch = true # Have to fix this and figure out what's going on # Regexes for lines to exclude from consideration -#exclude_lines = [ -# "pragma: no cover", -# # Ignore non-runnable code -# "if __name__ == .__main__.:", -# "pass", -# "...", -#] +exclude_lines = [ + "pragma: no cover", + # Ignore non-runnable code + "if __name__ == .__main__.:", + "pass", +] # Only check coverage for source files include =["automata/*/*.py"] From 7cb48f5578840f875101c416ca81e2a423cd620b Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 20 May 2023 18:46:15 -0500 Subject: [PATCH 79/83] Added test for edge coloring --- automata/fa/dfa.py | 2 +- tests/test_dfa.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/automata/fa/dfa.py b/automata/fa/dfa.py index e7273b59..ee0f7b42 100644 --- a/automata/fa/dfa.py +++ b/automata/fa/dfa.py @@ -903,7 +903,7 @@ def successors( ) # Traverse to child if candidate is viable if candidate_state in coaccessible_nodes: - state_stack.append(cast(str, candidate_state)) + state_stack.append(candidate_state) char_stack.append(cast(str, candidate)) candidate = first_symbol else: diff --git a/tests/test_dfa.py b/tests/test_dfa.py index e30851c8..1777f691 100644 --- a/tests/test_dfa.py +++ b/tests/test_dfa.py @@ -1549,6 +1549,26 @@ def test_show_diagram_initial_final_different(self) -> None: self.assertEqual(dest, self.dfa.initial_state) self.assertTrue(source not in self.dfa.states) + def test_show_diagram_read_input(self) -> None: + """ + Should construct the diagram for a DFA reading input. + """ + input_strings = ["0111", "001", "01110011", "001011001", "1100", ""] + + for input_string in input_strings: + graph = self.dfa.show_diagram(input_str=input_string) + + # Get edges corresponding to input path + colored_edges = [ + edge for edge in graph.edges() if "color" in dict(edge.attr) + ] + colored_edges.sort(key=lambda edge: edge.attr["label"][2:]) + + edge_pairs = [ + edge[0:2] for edge in self.dfa._get_input_path(input_string)[0] + ] + self.assertEqual(edge_pairs, colored_edges) + def test_show_diagram_initial_final_same(self) -> None: """ Should construct the diagram for a DFA whose initial state From 6f1ec9fd7086e9ba7b5d65eb459ae627ae9cfcea Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Sat, 20 May 2023 19:17:44 -0500 Subject: [PATCH 80/83] nfa diagram tests --- automata/fa/fa.py | 4 ++-- tests/test_nfa.py | 50 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 217ac088..215beedd 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -42,9 +42,9 @@ def get_state_name(state_data: FAStateT) -> str: return state_data - elif isinstance(state_data, (set, frozenset, tuple)): + elif isinstance(state_data, (frozenset, tuple)): inner = ", ".join(FA.get_state_name(sub_data) for sub_data in state_data) - if isinstance(state_data, (set, frozenset)): + if isinstance(state_data, frozenset): if state_data: return "{" + inner + "}" else: diff --git a/tests/test_nfa.py b/tests/test_nfa.py index 18c1b623..17bd6b21 100644 --- a/tests/test_nfa.py +++ b/tests/test_nfa.py @@ -584,26 +584,16 @@ def test_show_diagram_initial_final_same(self) -> None: is also a final state. """ - nfa = NFA( - states={"q0", "q1", "q2"}, - input_symbols={"a", "b"}, - transitions={ - "q0": {"a": {"q1"}}, - "q1": {"a": {"q1"}, "": {"q2"}}, - "q2": {"b": {"q0"}}, - }, - initial_state="q0", - final_states={"q0", "q1"}, - ) - - graph = nfa.show_diagram() + graph = self.nfa.show_diagram() node_names = {node.get_name() for node in graph.nodes()} - self.assertTrue(set(nfa.states).issubset(node_names)) - self.assertEqual(len(nfa.states) + 1, len(node_names)) + self.assertTrue(set(self.nfa.states).issubset(node_names)) + self.assertEqual(len(self.nfa.states) + 1, len(node_names)) for state in self.dfa.states: node = graph.get_node(state) - expected_shape = "doublecircle" if state in nfa.final_states else "circle" + expected_shape = ( + "doublecircle" if state in self.nfa.final_states else "circle" + ) self.assertEqual(node.attr["shape"], expected_shape) expected_transitions = { @@ -624,6 +614,27 @@ def test_show_diagram_initial_final_same(self) -> None: self.assertEqual(dest, self.nfa.initial_state) self.assertTrue(source not in self.nfa.states) + def test_show_diagram_read_input(self) -> None: + """ + Should construct the diagram for a NFA reading input. + """ + input_strings = ["ababa", "bba", "aabba", "baaab", "bbaab", ""] + + for input_string in input_strings: + graph = self.nfa.show_diagram(input_str=input_string) + + # Get edges corresponding to input path + colored_edges = [ + edge for edge in graph.edges() if "color" in dict(edge.attr) + ] + colored_edges.sort(key=lambda edge: edge.attr["label"][2:]) + + edge_pairs = [ + edge[0:2] for edge in self.nfa._get_input_path(input_string)[0] + ] + + self.assertEqual(edge_pairs, colored_edges) + def test_show_diagram_write_file(self) -> None: """ Should construct the diagram for a NFA @@ -639,6 +650,13 @@ def test_show_diagram_write_file(self) -> None: self.assertTrue(os.path.exists(diagram_path)) os.remove(diagram_path) + # TODO this test case fails, there's a bug in the get_input_path for NFAs + # def test_get_input_path(self) -> None: + # input_str = "aba" + # + # was_accepted = self.nfa._get_input_path(input_str)[1] + # self.assertEqual(was_accepted, self.nfa.accepts_input(input_str)) + def test_add_new_state_type_integrity(self) -> None: """ Should properly add new state of different type than original states; From a8d84335134a435e2e364a998e89dd2d993434c5 Mon Sep 17 00:00:00 2001 From: Caleb Evans Date: Sat, 20 May 2023 17:39:53 -0700 Subject: [PATCH 81/83] Replace @functools.cache with @functools.lru_cache Per the documentation, @functools.cache "returns the same as lru_cache(maxsize=None)". See: --- automata/fa/nfa.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 8215a306..b69829bf 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -1009,7 +1009,7 @@ def gen_paths_for( path, accepted, steps = find_best_path(next_state, step) yield [(state, next_state, "")] + path, accepted, steps - @functools.cache + @functools.lru_cache(maxsize=None) def find_best_path( state: NFAStateT, step: int ) -> Tuple[List[Tuple[NFAStateT, NFAStateT, str]], bool, int]: From 10706a6406d9057a727e2ab49371a3f0b354c009 Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Mon, 5 Jun 2023 19:37:51 -0500 Subject: [PATCH 82/83] Rewrote broken algorithm and added test cases --- automata/fa/fa.py | 4 +- automata/fa/nfa.py | 140 +++++++++++++++++++++------------------------ tests/test_nfa.py | 15 +++-- 3 files changed, 77 insertions(+), 82 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 215beedd..73919724 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -98,7 +98,9 @@ def show_diagram( """ if not _visual_imports: - raise ImportError("Missing visual imports.") + raise ImportError( + "Missing visualization packages; please install coloraide and pygraphviz." + ) # Defining the graph. graph = pgv.AGraph(strict=False, directed=True) diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index b69829bf..43dd376d 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -35,7 +35,7 @@ NFAPathT = Mapping[str, AbstractSet[NFAStateT]] NFATransitionsT = Mapping[NFAStateT, NFAPathT] - +InputPathListT = List[Tuple[NFAStateT, NFAStateT, str]] DEFAULT_REGEX_SYMBOLS = frozenset(chain(string.ascii_letters, string.digits)) @@ -981,78 +981,68 @@ def iter_transitions( for to_ in to_lookup ) - def _get_input_path( - self, input_str: str - ) -> Tuple[List[Tuple[NFAStateT, NFAStateT, str]], bool]: - """Calculate the path taken by input.""" - - visiting = set() - - def gen_paths_for( - state: NFAStateT, step: int - ) -> Generator[ - Tuple[List[Tuple[NFAStateT, NFAStateT, str]], bool, int], None, None - ]: - """ - Generate all the possible paths from state after taking step n. - """ - symbol = input_str[step] - transitions = self.transitions.get(state, {}) - # generate all the paths after taking input symbol and increasing - # input read length by 1 - for next_state in transitions.get(symbol, set()): - path, accepted, steps = find_best_path(next_state, step + 1) - yield [(state, next_state, symbol)] + path, accepted, steps + 1 - - # generate all the lambda paths from state - for next_state in transitions.get("", set()): - path, accepted, steps = find_best_path(next_state, step) - yield [(state, next_state, "")] + path, accepted, steps - - @functools.lru_cache(maxsize=None) - def find_best_path( - state: NFAStateT, step: int - ) -> Tuple[List[Tuple[NFAStateT, NFAStateT, str]], bool, int]: - """ - Try all the possible paths from state after taking step n. A path is - better if (with priority): - 1. It is an accepting path (ends in a final state) - 2. It reads more of the input (if the input is not accepted we + def _get_input_path(self, input_str: str) -> Tuple[InputPathListT, bool]: + """ + Get best input path. A path is better if (with priority): + + 1. It is an accepting path (ends in a final state) + 2. It reads more of the input (if the input is not accepted we select the path such that we can stay on the nfa the longest) - 3. It has the fewest jumps (uses less lambda symbols) - - Returns a tuple of: - 1. the path taken - 2. wether the path was accepting - 3. the number of input symbols read by this path (or the number of - non-lambda transitions in the path) - """ - if step >= len(input_str): - return [], state in self.final_states, 0 - - if state in visiting: - return [], False, 0 - - # mark this state as being visited - visiting.add(state) - # tracking variable for the shortest path - shortest_path = [] - # tracking variable for the info about the path - # accepting, max_steps, -min_jumps - best_path = (False, 0, 0) - - # iterate over all the paths - for path, accepted, steps in gen_paths_for(state, step): - # create a tuple to compare this with the current best - new_path = (accepted, steps, -len(path)) - if new_path > best_path: - shortest_path = path - best_path = new_path - - # mark this as complete - visiting.remove(state) - accepting, max_steps, _ = best_path - return shortest_path, accepting, max_steps - - path, accepting, _ = find_best_path(self.initial_state, 0) - return path, accepting + 3. It has the fewest jumps (uses less lambda symbols) + + Returns a tuple of: + 1. the path taken + 2. wether the path was accepting + """ + + visited = set() + work_queue: Deque[Tuple[InputPathListT, NFAStateT, str]] = deque( + [([], self.initial_state, input_str)] + ) + + last_non_accepting_input: InputPathListT = [] + least_input_remaining = len(input_str) + + while work_queue: + visited_states, curr_state, remaining_input = work_queue.popleft() + + # First final state we hit is the best according to desired criteria + if curr_state in self.final_states and not remaining_input: + return visited_states, True + + # Otherwise, update longest non-accepting input + if len(remaining_input) < least_input_remaining: + least_input_remaining = len(remaining_input) + last_non_accepting_input = visited_states + + # First, get next states that result from reading from input + if remaining_input: + next_symbol = remaining_input[0] + rest = remaining_input[1:] if remaining_input else "" + + next_states_from_symbol = self.transitions[curr_state].get( + next_symbol, set() + ) + + for next_state in next_states_from_symbol: + if (next_state, rest) not in visited: + next_visited_states = visited_states.copy() + next_visited_states.append( + (curr_state, next_state, next_symbol) + ) + visited.add((next_state, rest)) + work_queue.append((next_visited_states, next_state, rest)) + + # Next, get next states resulting from lambda transition + next_states_from_lambda = self.transitions[curr_state].get("", set()) + + for next_state in next_states_from_lambda: + if (next_state, remaining_input) not in visited: + next_visited_states = visited_states.copy() + next_visited_states.append((curr_state, next_state, "")) + visited.add((next_state, remaining_input)) + work_queue.append( + (next_visited_states, next_state, remaining_input) + ) + + return last_non_accepting_input, False diff --git a/tests/test_nfa.py b/tests/test_nfa.py index 17bd6b21..eab2bfc8 100644 --- a/tests/test_nfa.py +++ b/tests/test_nfa.py @@ -650,12 +650,15 @@ def test_show_diagram_write_file(self) -> None: self.assertTrue(os.path.exists(diagram_path)) os.remove(diagram_path) - # TODO this test case fails, there's a bug in the get_input_path for NFAs - # def test_get_input_path(self) -> None: - # input_str = "aba" - # - # was_accepted = self.nfa._get_input_path(input_str)[1] - # self.assertEqual(was_accepted, self.nfa.accepts_input(input_str)) + def test_get_input_path(self) -> None: + input_strings = ["ababa", "bba", "aabba", "baaab", "bbaab", ""] + + for input_str in input_strings: + input_path, was_accepted = self.nfa._get_input_path(input_str) + self.assertEqual(was_accepted, self.nfa.accepts_input(input_str)) + + for start_vtx, end_vtx, symbol in input_path: + self.assertIn(end_vtx, self.nfa.transitions[start_vtx][symbol]) def test_add_new_state_type_integrity(self) -> None: """ From 13aaf955532b319daf11d98aeaff8cd5dda875cd Mon Sep 17 00:00:00 2001 From: Eliot Robson Date: Mon, 5 Jun 2023 19:40:41 -0500 Subject: [PATCH 83/83] lint --- automata/fa/fa.py | 3 ++- automata/fa/nfa.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/automata/fa/fa.py b/automata/fa/fa.py index 73919724..e438eb97 100644 --- a/automata/fa/fa.py +++ b/automata/fa/fa.py @@ -99,7 +99,8 @@ def show_diagram( if not _visual_imports: raise ImportError( - "Missing visualization packages; please install coloraide and pygraphviz." + "Missing visualization packages; " + "please install coloraide and pygraphviz." ) # Defining the graph. diff --git a/automata/fa/nfa.py b/automata/fa/nfa.py index 43dd376d..a30876d0 100644 --- a/automata/fa/nfa.py +++ b/automata/fa/nfa.py @@ -2,7 +2,6 @@ """Classes and methods for working with nondeterministic finite automata.""" from __future__ import annotations -import functools import string from collections import deque from itertools import chain, count, product, repeat