Skip to content

Commit

Permalink
Merge 9d357e9 into 35b68fd
Browse files Browse the repository at this point in the history
  • Loading branch information
Vipul-Cariappa committed Oct 21, 2023
2 parents 35b68fd + 9d357e9 commit c18be72
Show file tree
Hide file tree
Showing 16 changed files with 952 additions and 67 deletions.
27 changes: 27 additions & 0 deletions automata/base/automaton.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,33 @@ class Automaton(metaclass=abc.ABCMeta):
transitions: AutomatonTransitionsT
input_symbols: AbstractSet[str]

@staticmethod
def _get_state_name(state_data: Any) -> str:
"""
Get a 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

elif isinstance(state_data, (frozenset, tuple)):
inner = ", ".join(
Automaton._get_state_name(sub_data) for sub_data in state_data
)
if isinstance(state_data, frozenset):
if state_data:
return "{" + inner + "}"
else:
return "∅"

elif isinstance(state_data, tuple):
return "(" + inner + ")"

return str(state_data)

def __init__(self, **kwargs: Any) -> None:
if not global_config.allow_mutable_automata:
for attr_name, attr_value in kwargs.items():
Expand Down
6 changes: 6 additions & 0 deletions automata/base/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,9 @@ class InfiniteLanguageException(AutomatonException):
"""The operation cannot be performed because the language is infinite"""

pass


class DiagramException(Exception):
"""The diagram cannot be produced"""

pass
96 changes: 95 additions & 1 deletion automata/base/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,40 @@
#!/usr/bin/env python3
"""Miscellaneous utility functions and classes."""
from __future__ import annotations

import os
import pathlib
import random
import uuid
from collections import defaultdict
from itertools import count, tee, zip_longest
from typing import Any, Callable, Dict, Generic, Iterable, List, Set, Tuple, TypeVar
from typing import (
Any,
Callable,
Dict,
Generic,
Iterable,
List,
Literal,
Set,
Tuple,
TypeVar,
Union,
)

from frozendict import frozendict

# Optional imports for use with visual functionality
try:
import pygraphviz as pgv
except ImportError:
_visual_imports = False
else:
_visual_imports = True


LayoutMethod = Literal["neato", "dot", "twopi", "circo", "fdp", "nop"]


def freeze_value(value: Any) -> Any:
"""
Expand Down Expand Up @@ -41,6 +69,72 @@ def get_renaming_function(counter: count) -> Callable[[Any], int]:
return defaultdict(counter.__next__).__getitem__


def create_unique_random_id() -> str:
# To be able to set the random seed, took code from:
# https://nathanielknight.ca/articles/consistent_random_uuids_in_python.html
return str(
uuid.UUID(bytes=bytes(random.getrandbits(8) for _ in range(16)), version=4)
)


def create_graph(
horizontal: bool = True,
reverse_orientation: bool = False,
fig_size: Union[Tuple[float, float], Tuple[float], None] = None,
state_separation: float = 0.5,
) -> pgv.AGraph:
"""Creates and returns a graph object
Args:
- 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.
- state_separation (float, optional): Node distance. Defaults to 0.5.
Returns:
AGraph with the given configuration.
"""
if not _visual_imports:
raise ImportError(
"Missing visualization packages; "
"please install coloraide and pygraphviz."
)

# Defining the graph.
graph = pgv.AGraph(strict=False, directed=True)

if fig_size is not None:
graph.graph_attr.update(size=", ".join(map(str, fig_size)))

graph.graph_attr.update(ranksep=str(state_separation))

if horizontal:
rankdir = "RL" if reverse_orientation else "LR"
else:
rankdir = "BT" if reverse_orientation else "TB"

graph.graph_attr.update(rankdir=rankdir)

return graph


def save_graph(
graph: pgv.AGraph,
path: Union[str, os.PathLike],
) -> None:
"""Write `graph` to file given by `path`. PNG, SVG, etc.
Returns the same graph."""

save_path_final: pathlib.Path = pathlib.Path(path)

format = save_path_final.suffix.split(".")[1] if save_path_final.suffix else None

graph.draw(
path=save_path_final,
format=format,
)


T = TypeVar("T")


Expand Down
79 changes: 19 additions & 60 deletions automata/fa/fa.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@

import abc
import os
import pathlib
import random
import uuid
from collections import defaultdict
from typing import Any, Dict, Generator, List, Literal, Optional, Set, Tuple, Union
from typing import Any, Dict, Generator, List, Optional, Set, Tuple, Union

from automata.base.automaton import Automaton, AutomatonStateT
from automata.base.utils import (
LayoutMethod,
create_graph,
create_unique_random_id,
save_graph,
)

# Optional imports for use with visual functionality
try:
Expand All @@ -23,39 +26,13 @@


FAStateT = AutomatonStateT
LayoutMethod = Literal["neato", "dot", "twopi", "circo", "fdp", "nop"]


class FA(Automaton, metaclass=abc.ABCMeta):
"""An abstract base class for finite automata."""

__slots__ = tuple()

@staticmethod
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.
"""
if isinstance(state_data, str):
if state_data == "":
return "λ"

return state_data

elif isinstance(state_data, (frozenset, tuple)):
inner = ", ".join(FA._get_state_name(sub_data) for sub_data in state_data)
if isinstance(state_data, frozenset):
if state_data:
return "{" + inner + "}"
else:
return "∅"

elif isinstance(state_data, tuple):
return "(" + inner + ")"

return str(state_data)

@staticmethod
def _get_edge_name(symbol: str) -> str:
return "ε" if symbol == "" else str(symbol)
Expand All @@ -67,6 +44,10 @@ def iter_transitions(self) -> Generator[Tuple[FAStateT, FAStateT, str], None, No
of the form (from_state, to_state, symbol)
"""

raise NotImplementedError(
f"iter_transitions is not implemented for {self.__class__}"
)

def show_diagram(
self,
input_str: Optional[str] = None,
Expand All @@ -83,7 +64,7 @@ def show_diagram(
"""
Generates the graph associated with the given DFA.
Args:
input_str (str, optional): String list of input symbols. Defaults to None.
- 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.
- horizontal (bool, optional): Direction of node layout. Defaults
Expand All @@ -105,29 +86,16 @@ def show_diagram(
)

# Defining the graph.
graph = pgv.AGraph(strict=False, directed=True)

if fig_size is not None:
graph.graph_attr.update(size=", ".join(map(str, fig_size)))
graph = create_graph(
horizontal, reverse_orientation, fig_size, state_separation
)

graph.graph_attr.update(ranksep=str(state_separation))
font_size_str = str(font_size)
arrow_size_str = str(arrow_size)

if horizontal:
rankdir = "RL" if reverse_orientation else "LR"
else:
rankdir = "BT" if reverse_orientation else "TB"
# create unique id to avoid colliding with other states
null_node = create_unique_random_id()

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.
# To be able to set the random seed, took code from:
# https://nathanielknight.ca/articles/consistent_random_uuids_in_python.html
null_node = str(
uuid.UUID(bytes=bytes(random.getrandbits(8) for _ in range(16)), version=4)
)
graph.add_node(
null_node,
label="",
Expand Down Expand Up @@ -200,18 +168,9 @@ def show_diagram(
# Set layout
graph.layout(prog=layout_method)

# Write diagram to file. PNG, SVG, etc.
# Write diagram to file
if path is not None:
save_path_final: pathlib.Path = pathlib.Path(path)

format = (
save_path_final.suffix.split(".")[1] if save_path_final.suffix else None
)

graph.draw(
path=save_path_final,
format=format,
)
save_graph(graph, path)

return graph

Expand Down
12 changes: 12 additions & 0 deletions automata/pda/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""Classes and methods for working with PDA configurations."""

from dataclasses import dataclass
from typing import Any

from automata.base.automaton import AutomatonStateT
from automata.pda.stack import PDAStack
Expand All @@ -27,3 +28,14 @@ def __repr__(self) -> str:
return "{}({!r}, {!r}, {!r})".format(
self.__class__.__name__, self.state, self.remaining_input, self.stack
)

def __eq__(self, other: Any) -> bool:
"""Return True if two PDAConfiguration are equivalent"""
if not isinstance(other, PDAConfiguration):
return NotImplemented

return (
self.state == other.state
and self.remaining_input == other.remaining_input
and self.stack == other.stack
)
38 changes: 37 additions & 1 deletion automata/pda/dpda.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#!/usr/bin/env python3
"""Classes and methods for working with deterministic pushdown automata."""

from typing import AbstractSet, Generator, Mapping, Optional, Set, Tuple, Union
from typing import AbstractSet, Generator, List, Mapping, Optional, Set, Tuple, Union

import automata.base.exceptions as exceptions
import automata.pda.exceptions as pda_exceptions
import automata.pda.pda as pda
from automata.base.utils import pairwise
from automata.pda.configuration import PDAConfiguration
from automata.pda.stack import PDAStack

Expand Down Expand Up @@ -53,6 +54,16 @@ def __init__(
acceptance_mode=acceptance_mode,
)

def iter_transitions(
self,
) -> Generator[Tuple[DPDAStateT, DPDAStateT, Tuple[str, str, str]], None, None]:
return (
(from_, to_, (input_symbol, stack_symbol, "".join(stack_push)))
for from_, input_lookup in self.transitions.items()
for input_symbol, stack_lookup in input_lookup.items()
for stack_symbol, (to_, stack_push) in stack_lookup.items()
)

def _validate_transition_invalid_symbols(
self, start_state: DPDAStateT, paths: DPDATransitionsT
) -> None:
Expand Down Expand Up @@ -151,6 +162,31 @@ def _get_next_configuration(self, old_config: PDAConfiguration) -> PDAConfigurat
)
return new_config

def _get_input_path(
self, input_str: str
) -> Tuple[List[Tuple[PDAConfiguration, PDAConfiguration]], bool]:
"""
Calculate the path taken by input.
Args:
input_str (str): The input string to run on the DPDA.
Returns:
Tuple[List[Tuple[PDAConfiguration, PDAConfiguration]], bool]: A list
of all transitions taken in each step and a boolean indicating
whether the DPDA accepted the input.
"""

state_history = list(self.read_input_stepwise(input_str))

path = list(pairwise(state_history))

last_state = state_history[-1] if state_history else self.initial_state
accepted = last_state in self.final_states

return path, accepted

def read_input_stepwise(
self, input_str: str
) -> Generator[PDAConfiguration, None, None]:
Expand Down
Loading

0 comments on commit c18be72

Please sign in to comment.