Drawing automata

Every automaton, weighted automaton and graph describes itself in the language of Graphviz with to_dot(). Two functions draw it, with the Graphviz program dot :

  • autolib.to_svg(automaton, highlight=None) returns the drawing as SVG text;

  • autolib.render(automaton, path, highlight=None) writes it to a file, whose extension gives the format : .svg, .png, .pdf…

highlight is a path, WeightedPath or MultiWeightedPath, or a list of edges : it is drawn in colour, with its states.

Graphviz installs apart from Python : apt install graphviz, brew install graphviz, conda install graphviz, winget install graphviz, or from graphviz.org.

In Jupyter

An automaton alone in a cell shows its drawing when Graphviz is installed, and its text otherwise :

from autolib import Automaton
Automaton("(0|1)*0(0|1)")

An automaton

Double circles are the accepting states, the point leads to the initial state, and the labels are sets of symbols.

import autolib
from autolib import Automaton

a = Automaton("(0|1)*0(0|1)")            # the second symbol from the end is 0
autolib.render(a, "automaton.svg")

The automaton of (0|1)*0(0|1)

A weighted automaton and its best path

Edges show symbols / cost, accepting states their final cost. Parallel edges stay distinct : the bus and the bike both read 1.

import autolib
from autolib import WeightedAutomaton

b = WeightedAutomaton.Builder()
home, work = b.add_state(), b.add_state()
b.set_initial(home)
b.set_accept(work, 0.5)                   # final cost
b.add_transition(home, work, 1, 30.0)     # bus
b.add_transition(home, work, 1, 20.0)     # bike
b.add_transition(home, work, 2, 45.0)     # on foot
b.add_transition(work, work, 0, 5.0)      # coffee
commute = b.build()

best = commute.best_word_of_length(2)                   # [1, 0] : bike, then coffee
autolib.render(commute, "best_path.svg", highlight=commute.best_path(best))

The best path of length two

Epsilon edges

Epsilon edges, reading nothing, are dashed. Here the automaton of the words at most one edit away from ab, the path of xab highlighted : an insertion, then two matches.

import autolib
from autolib import IntervalSet, WeightedAutomaton

letters = IntervalSet(range(ord("a"), ord("z") + 1))
target, b = "ab", WeightedAutomaton.Builder()
state = {(i, e): b.add_state() for i in range(len(target) + 1) for e in range(2)}
b.set_initial(state[0, 0])
for (i, e), s in state.items():
    if i == len(target):
        b.set_accept(s)
    if i < len(target):
        b.add_transition(s, state[i + 1, e], ord(target[i]), 0.0)        # match
    if e == 0:
        b.add_transition(s, state[i, 1], letters, 1.0)                   # insertion
        if i < len(target):
            b.add_transition(s, state[i + 1, 1], letters, 1.0)           # substitution
            b.add_epsilon(s, state[i + 1, 1], 1.0)                       # deletion, dashed
typos = b.build()

closest = typos.best_path([ord(c) for c in "xab"])     # one insertion
autolib.render(typos, "edit_distance.svg", highlight=closest)

Words at most one edit from ab

Several costs

Edges show their vectors of costs. Here the fastest journey of the Pareto front :

import autolib
from autolib import MultiWeightedAutomaton

b = MultiWeightedAutomaton.Builder(2)                    # minutes, euros
home, station, office = b.add_state(), b.add_state(), b.add_state()
b.set_initial(home)
b.set_accept(office)
b.add_transition(home, station, 0, [15, 0])              # walk
b.add_transition(home, station, 0, [5, 8])               # taxi
b.add_transition(station, office, 0, [40, 4])            # slow train
b.add_transition(station, office, 0, [20, 15])           # fast train
journey = b.build()

fastest = min(journey.pareto_front(), key=lambda path: path.cost(0))
autolib.render(journey, "pareto.svg", highlight=fastest)

The fastest journey of the Pareto front

Other tools

to_dot() is plain text : the Python package graphviz, NetworkX (nx.nx_pydot.read_dot) or any Graphviz viewer read it.

from autolib import Automaton
print(Automaton("12*").to_dot())