Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added show_diagram method for Push Down Automata #177

Merged
merged 20 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):
caleb531 marked this conversation as resolved.
Show resolved Hide resolved
"""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