Skip to content

Commit

Permalink
Added show_diagram method for Push Down Automata
Browse files Browse the repository at this point in the history
  • Loading branch information
Vipul-Cariappa committed Oct 13, 2023
1 parent 44503d7 commit 5017e85
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 1 deletion.
10 changes: 10 additions & 0 deletions automata/pda/dpda.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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
11 changes: 11 additions & 0 deletions automata/pda/npda.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ def __init__(
acceptance_mode=acceptance_mode,
)

def iter_transitions(
self,
) -> Generator[Tuple[NPDAStateT, NPDAStateT, 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, op_ in stack_lookup.items()
for (to_, stack_push) in op_
)

def _validate_transition_invalid_symbols(
self, start_state: NPDAStateT, paths: NPDATransitionsT
) -> None:
Expand Down
183 changes: 182 additions & 1 deletion automata/pda/pda.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,34 @@
"""Classes and methods for working with all pushdown automata."""

import abc
from typing import AbstractSet, Literal
import os
import pathlib
import random
import uuid
from collections import defaultdict
from typing import AbstractSet, Generator, Literal, Optional, Tuple, Union

import automata.base.exceptions as exceptions
import automata.pda.exceptions as pda_exceptions
from automata.base.automaton import Automaton, AutomatonStateT, AutomatonTransitionsT
from automata.pda.configuration import PDAConfiguration
from automata.pda.stack import PDAStack

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

PDAStateT = AutomatonStateT
PDATransitionsT = AutomatonTransitionsT
PDAAcceptanceModeT = Literal["final_state", "empty_stack", "both"]

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


class PDA(Automaton, metaclass=abc.ABCMeta):
"""An abstract base class for pushdown automata."""
Expand All @@ -24,6 +40,171 @@ class PDA(Automaton, metaclass=abc.ABCMeta):
initial_stack_symbol: str
acceptance_mode: PDAAcceptanceModeT

@staticmethod
def _get_state_name(state_data: PDAStateT) -> 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(PDA._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(
input_symbol: str = "", stack_top_symbol: str = "", stack_push: str = ""
) -> str:
return (
("ε" if input_symbol == "" else str(input_symbol))
+ ", "
+ ("ε" if stack_top_symbol == "" else str(stack_top_symbol))
+ " | "
+ ("ε" if stack_push == "" else str(stack_push))
)

@abc.abstractmethod
def iter_transitions(
self,
) -> Generator[Tuple[PDAStateT, PDAStateT, Tuple[str, str, str]], None, None]:
"""
Iterate over all transitions in the automaton. Each transition is a tuple
of the form (from_state, to_state, (input_symbol, stack_top_symbol, stack_push_symbols))
"""

def show_diagram(
self,
path: Union[str, os.PathLike, None] = None,
*,
layout_method: LayoutMethod = "dot",
horizontal: bool = True,
reverse_orientation: bool = False,
fig_size: Union[Tuple[float, float], Tuple[float], None] = None,
font_size: float = 14.0,
arrow_size: float = 0.85,
state_separation: float = 0.5,
) -> pgv.AGraph:
"""
Generates the graph associated with the given PDA.
Args:
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
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.
Returns:
AGraph corresponding to the given automaton.
"""

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))
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"

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="",
tooltip=".",
shape="point",
fontsize=font_size_str,
)
initial_node = self._get_state_name(self.initial_state)
graph.add_edge(
null_node,
initial_node,
tooltip="->" + initial_node,
arrowsize=arrow_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)

edge_labels = defaultdict(list)
for from_state, to_state, symbol in self.iter_transitions():
if is_edge_drawn[from_state, to_state, symbol]:
continue

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():
graph.add_edge(
from_node,
to_node,
label="\n".join(sorted(labels)),
arrowsize=arrow_size_str,
fontsize=font_size_str,
)

# Set layout
graph.layout(prog=layout_method)

# Write diagram to file. PNG, SVG, etc.
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,
)

return graph

def _validate_transition_invalid_input_symbols(
self, start_state: PDAStateT, input_symbol: str
) -> None:
Expand Down

0 comments on commit 5017e85

Please sign in to comment.