geometry_tools.automata.fsa

Work with finite-state automata.

Finite-state automata are really just finite directed labeled graphs, subject to the constraint that each vertex v has at most one outgoing edge with a given label. One or more of the vertices are "start states;" a word w in the set of edge labels is "accepted" if there is an edge path in the graph (starting at a start state) whose sequence of edge labels gives w.

This module provides the FSA class, which can be used to perform simple tasks with finite-state automata. A common use-case is enumerating the list of words accepted by a particular automaton:

from geometry_tools.automata import fsa

# load a word-acceptor automaton for the (3,3,4) triangle group
cox_aut = fsa.load_builtin("cox334.wa")

# list all the words accepted by this automaton with length at most 3.
list(cox_aut.enumerate_words(3))
 ['',
 'a',
 'b',
 'c',
 'ab',
 'ac',
 'ba',
 'bc',
 'ca',
 'cb',
 'aba',
 'abc',
 'aca',
 'acb',
 'bac',
 'bca',
 'bcb',
 'cab',
 'cac',
 'cba']
  1"""Work with finite-state automata.
  2
  3Finite-state automata are really just finite directed labeled graphs,
  4subject to the constraint that each vertex `v` has at most one
  5outgoing edge with a given label. One or more of the vertices are
  6"start states;" a word `w` in the set of edge labels is "accepted" if
  7there is an edge path in the graph (starting at a start state) whose
  8sequence of edge labels gives `w`.
  9
 10This module provides the `FSA` class, which can be used to perform
 11simple tasks with finite-state automata. A common use-case is
 12enumerating the list of words accepted by a particular automaton:
 13
 14```python
 15from geometry_tools.automata import fsa
 16
 17# load a word-acceptor automaton for the (3,3,4) triangle group
 18cox_aut = fsa.load_builtin("cox334.wa")
 19
 20# list all the words accepted by this automaton with length at most 3.
 21list(cox_aut.enumerate_words(3))
 22
 23```
 24     ['',
 25     'a',
 26     'b',
 27     'c',
 28     'ab',
 29     'ac',
 30     'ba',
 31     'bc',
 32     'ca',
 33     'cb',
 34     'aba',
 35     'abc',
 36     'aca',
 37     'acb',
 38     'bac',
 39     'bca',
 40     'bcb',
 41     'cab',
 42     'cac',
 43     'cba']
 44
 45    """
 46import os
 47import copy
 48import importlib.resources
 49from collections import deque, defaultdict
 50
 51
 52from . import kbmag_utils, gap_parse
 53from .. import automata
 54from .. import utils
 55
 56from ..utils import words
 57
 58BUILTIN_DIR = "builtin"
 59
 60
 61class FSAException(Exception):
 62    pass
 63
 64class FSA:
 65    """FSA: a finite state-automaton.
 66
 67    The underlying data structure of a finite-state automaton is a
 68    python dictionary of the form:
 69
 70    ```
 71    {
 72      vertex1: {label_a: neighbor_a, label_b: neighbor_b, ...},
 73      vertex2: ....,
 74    }
 75    ```
 76
 77    That is, `vertex1` is connected to `neighbor_a` by an edge with
 78    label `label_a`, etc.
 79
 80    The automaton can be constructed by passing it a dictionary of
 81    this form, or by adding vertices/edges individually through the
 82    built-in methods.
 83
 84    """
 85    def __init__(self, vert_dict={}, start_vertices=[], graph_dict=True):
 86        """
 87
 88        Parameters
 89        ----------
 90        vert_dict : dict
 91            dictionary of vertices used to build the automaton. By
 92            default, this dictionary should have the format specified
 93            above.
 94
 95        start_vertices : list
 96            list of vertices which are valid start states
 97            for this automaton. Currently, the `FSA` class will not
 98            correctly handle lists of length > 1.
 99
100        graph_dict : bool
101            If `True` (the default), expect a dictionary of the format
102            listed above. If `False`, instead expect a dictionary with format:
103
104            ```
105            {
106              vertex1: {neighbor_a: [neighbor_a_label1, neighbor_a_label2, ...],
107                        neighbor_b: [neighbor_b_label1, neighbor_b_label2, ...], ....},
108              vertex2: ....,
109            }
110            ```
111        """
112        if graph_dict:
113            self._from_graph_dict(vert_dict)
114        else:
115            self._out_dict = FSA._defaultify_out_dict(vert_dict)
116
117            self._build_in_dict()
118            self._build_graph_dict()
119
120        self.start_vertices = start_vertices
121
122    @staticmethod
123    def _defaultify_out_dict(out_dict):
124        return {
125            key: defaultdict(list, copy.deepcopy(value))
126            for key, value in out_dict.items()
127        }
128
129    def _from_graph_dict(self, graph_dict):
130        self._graph_dict = copy.deepcopy(graph_dict)
131        hidden = _hidden_vertices(graph_dict)
132        for v in hidden:
133            self._graph_dict[v] = {}
134
135        out_dict = {v : {} for v in self._graph_dict}
136
137        for v, neighbors in self._graph_dict.items():
138            out_dict[v] = defaultdict(list)
139            for label, neighbor in neighbors.items():
140                out_dict[v][neighbor].append(label)
141
142        self._out_dict = out_dict
143        self._build_in_dict()
144
145    def _build_graph_dict(self):
146        label_dict = {v:{} for v in self._out_dict}
147        for v, neighbors_out in self._out_dict.items():
148            for w, labels in neighbors_out.items():
149                for label in labels:
150                    label_dict[v][label] = w
151        self._graph_dict = label_dict
152
153    def _build_in_dict(self):
154        in_dict = defaultdict(dict)
155        for v, neighbors_out in self._out_dict.items():
156            for w, labels in neighbors_out.items():
157                in_dict[w][v] = labels
158        self._in_dict = in_dict
159
160    def __str__(self):
161        return "FSA with dict:\n{}".format(self._graph_dict.__str__())
162
163    def __repr__(self):
164        return "FSA({})".format(self._graph_dict.__repr__())
165
166    @property
167    def out_dict(self):
168        return self._out_dict
169
170    @property
171    def graph_dict(self):
172        return self._graph_dict
173
174    @property
175    def in_dict(self):
176        return self._in_dict
177
178    def has_edge(self, tail, head):
179        return len(self._out_dict[tail][head]) > 0
180
181    def edges_out(self, vertex):
182        """Get the list of edges directed away from a vertex.
183        """
184        for w, labels in self._out_dict[vertex].items():
185            for label in labels:
186                yield (vertex, w, label)
187
188    def edges_in(self, vertex):
189        """Get the list of edges directed towards a vertex."""
190        for w, labels in self._in_dict[vertex].items():
191            for label in labels:
192                yield (w, vertex, label)
193
194    def neighbors_out(self, vertex):
195        """Get the list of outward neighbors of a vertex."""
196        return self._out_dict[vertex].keys()
197
198    def neighbors_in(self, vertex):
199        """Get the list of inward neighbors of a vertex."""
200        return self._in_dict[vertex].keys()
201
202    def edge_label(self, tail, head):
203        """Get the label of an edge from its tail and head vertex.
204
205        This function will raise an exception if there is not a unique
206        edge between `tail` and `head`.
207
208        """
209        if len(self._out_dict[tail][head]) == 1:
210            return self._out_dict[tail][head][0]
211        else:
212            raise ValueError("ambiguous edge specification: there is not exactly"
213                            f" one edge between {tail} and {head}" )
214
215    def edge_labels(self, tail, head):
216        """Get the list of labels associated to all the edges between `tail`
217        and `head`.
218
219        """
220        return self._out_dict[tail][head]
221
222    def add_vertices(self, vertices):
223        """Add vertices to the FSA.
224        """
225        for v in vertices:
226            if v not in self._out_dict:
227                self._out_dict[v] = defaultdict(list)
228                self._in_dict[v] = defaultdict(list)
229                self._graph_dict[v] = defaultdict(dict)
230
231    def add_edges(self, edges, elist=False,
232                  ignore_redundant=True):
233        """Add edges to the FSA.
234
235        Parameters
236        ----------
237        edges : list
238            a list of tuples of the form `(tail, head, label)`,
239            specifying an edge to add.
240
241        elist : bool
242            if `True`, then `label` should be interpreted as a single
243            edge label. Otherwise, `label` is interpreted as a list of
244            labels for multiple edges to be added between vertices.
245
246        """
247        for e in edges:
248            tail, head, label = e
249
250            # no-op if vertices are already in FSA
251            self.add_vertices([tail, head])
252
253            if head not in self._out_dict[tail]:
254                self._out_dict[tail][head] = []
255                self._in_dict[head][tail] = []
256
257            if ignore_redundant and label in self._out_dict[tail][head]:
258                continue
259
260            if elist:
261                self._out_dict[tail][head] += label
262                self._in_dict[head][tail] += label
263                for l in label:
264                    self._graph_dict[tail][l] = head
265
266            else:
267                self._out_dict[tail][head].append(label)
268                self._in_dict[head][tail].append(label)
269                self._graph_dict[tail][label] = head
270
271    def delete_vertices(self, vertices):
272        """Delete several vertices from the FSA.
273        """
274        for v in vertices:
275            self.delete_vertex(v)
276
277    def delete_vertex(self, vertex):
278        """Delete a single vertex from the FSA.
279        """
280        for w in self.neighbors_out(vertex):
281            self._in_dict[w].pop(vertex)
282        for w in self.neighbors_in(vertex):
283            self._out_dict[w].pop(vertex)
284
285            for lab in list(self._graph_dict[w]):
286                if self._graph_dict[w][lab] == vertex:
287                    self._graph_dict[w].pop(lab)
288
289        self._out_dict.pop(vertex)
290        self._in_dict.pop(vertex)
291        self._graph_dict.pop(vertex)
292
293    def vertices(self):
294        return self._out_dict.keys()
295
296    def edges(self, with_labels=False):
297        """Get all of the edges of this FSA
298
299        Yields
300        ------
301        tuple
302            An ordered pair of the form `(tail, head)` for each edge
303            in the automaton
304
305        """
306        for v, nbrs in self._graph_dict.items():
307            for label, w in nbrs.items():
308                if with_labels:
309                    yield (v, w, label)
310                else:
311                    yield (v, w)
312
313    def recurrent(self, inplace=False):
314        """Find a version of the automaton which is recurrent, i.e. which has
315        no (forward or backward) dead ends.
316
317        Parameters
318        ----------
319        inplace : bool
320            if `True`, modify the automaton in-place. Otherwise,
321            return a recurrent copy.
322
323        Returns
324        -------
325        FSA or None
326            A recurrent version of the automaton if `inplace` is
327            `True`, otherwise `None`.
328
329        """
330
331        to_modify = self
332
333        if not inplace:
334            to_modify = copy.deepcopy(self)
335
336        still_pruning = True
337        while still_pruning:
338            still_pruning = False
339            vertices = list(to_modify._out_dict.keys())
340            for v in vertices:
341                if (len(to_modify._out_dict[v]) == 0 or
342                    len(to_modify._in_dict[v]) == 0):
343                    to_modify.delete_vertex(v)
344                    still_pruning = True
345
346        if not inplace:
347            return to_modify
348
349    def remove_long_paths(self, root=None, edge_ties=True,
350                          return_distances=False):
351        """Find a version of the automaton which does not backtrack.
352
353        This function is the opposite of `make_recurrent`: it finds a
354        subgraph of the automaton which is the union of
355        shortest-length paths from the root vertex to each node. This
356        will be a directed acyclic graph (in fact, a tree if
357        `edge_ties` is false).
358
359        It is mostly useful for visualizing the structure of the
360        automaton.
361
362        Parameters
363        ----------
364        edge_ties : bool
365            if `True`, if two edges give rise to paths of the
366            same length, include both of them in the new graph.
367
368    """
369        H = FSA({})
370        H.add_vertices(self.vertices())
371
372        #Dijkstra's algorithm!
373        distance = {}
374        marked = {v:False for v in self.vertices()}
375
376        if root is None:
377            root = self.start_vertices[0]
378
379        marked[root] = True
380        distance[root] = 0
381        vertex_queue = deque([root])
382        while len(vertex_queue) > 0:
383            v = vertex_queue.popleft()
384
385            to_visit = [w for w in self.neighbors_out(v) if not marked[w]]
386            for w in to_visit:
387                marked[w] = True
388                distance[w] = distance[v] + 1
389
390            short_nbrs = to_visit
391            if edge_ties:
392                short_nbrs = [w for w in self.neighbors_out(v)
393                               if distance[w] == distance[v] + 1]
394
395            H.add_edges([(v, w, self.edge_labels(v,w)) for w in short_nbrs],
396                        elist= True)
397            vertex_queue.extend(to_visit)
398
399        return H
400
401    def initial_accepted_subword(self, word):
402        """Find an initial subword of a given word which is accepted by the
403        automaton.
404
405        Parameters
406        ----------
407        word : string
408            The word the automaton should try to accept
409
410        Returns
411        -------
412        string
413            An initial subword of `word` which is accepted by the
414            automaton, with maximal length. This may be either the
415            empty word or `word` itself.
416
417    """
418        vertex = self.start_vertices[0]
419        subword = ""
420        for letter in word:
421            try:
422                vertex = self.graph_dict[vertex][letter]
423            except KeyError:
424                return subword
425            subword += letter
426
427        return subword
428
429    def initial_rejected_subword(self, word):
430        """Find an initial subword of a given word which is accepted by the
431        automaton, or None if the given word is accepted.
432
433        Parameters
434        ----------
435        word : string
436            The word the automaton should try to accept.
437
438        Returns
439        -------
440        string
441            An initial subword of `word` which is rejected by the
442            automaton, with minimal length. If the word is accepted,
443            return None.
444
445        """
446        vertex = self.start_vertices[0]
447        subword = ""
448        for letter in word:
449            try:
450                vertex = self.graph_dict[vertex][letter]
451            except KeyError:
452                return subword + letter
453            subword += letter
454
455        return subword
456
457
458    def follow_word(self, word, start_vertex=None):
459        """Find the final state of the automaton when it tries to accept a word
460
461        Parameters
462        ----------
463        word : string
464            The word the automaton should try to accept
465
466        start_vertex: object
467            The start state for the automaton. If `None` (the
468            default), use the automaton's default start state.
469
470        Returns
471        -------
472        object
473            The state of the automaton when it accepts `word`
474
475        Raises
476        ------
477        FSAException
478            Raised if word is not accepted by this automaton.
479
480        """
481
482        if start_vertex is None:
483            start_vertex = self.start_vertices[0]
484        vertex = start_vertex
485
486        for letter in word:
487            try:
488                vertex = self.graph_dict[vertex][letter]
489            except KeyError:
490                raise FSAException(
491                    f"The word '{word}' is not accepted by the automaton."
492                )
493
494        return vertex
495
496    def enumerate_fixed_length_paths(self, length, start_vertex=None,
497                                     with_states=False):
498        """Enumerate all words of a fixed length accepted by the automaton,
499        starting at a given state.
500
501        Parameters
502        ----------
503        length : int
504            the length of the words we want to enumerate
505
506        start_vertex : object
507            which vertex to start at. If `None`, use the
508            default starting state for the automaton.
509
510        with_states : bool
511            if `True`, also yield the state of the
512            automaton at each accepted word.
513
514        Yields
515        ------
516        string or (string, object)
517            If `with_states` is false, yield the sequence of words
518            accepted (in some arbitrary order). Otherwise, yield pairs
519            of the form `(word, end_state)`, where `end_state` is the
520            final state the automaton reaches when accepting `word`.
521        """
522        if start_vertex is None:
523            start_vertex = self.start_vertices[0]
524
525        if length <= 0:
526            if with_states:
527                yield ("", start_vertex)
528            else:
529                yield ""
530        else:
531            for word, vertex in self.enumerate_fixed_length_paths(
532                    length - 1, start_vertex=start_vertex, with_states=True):
533                for label, neighbor in self._graph_dict[vertex].items():
534                    if with_states:
535                        yield (word + label, neighbor)
536                    else:
537                        yield word + label
538
539    def enumerate_words(self, max_length, start_vertex=None,
540                        with_states=False):
541        """Enumerate all words up to a given length accepted by the automaton.
542
543        Parameters
544        ----------
545        max_length : int
546            maximum length of a word to enumerate
547        start_vertex : object
548            the start vertex for accepting words. If `None`, use the
549            automaton's start vertex
550        with_states : bool
551            if `True`, also yield the state of the automaton at each
552            accepted word
553
554        Yields
555        ------
556        string or (string, object)
557            If `with_states` is false, yield the sequence of words
558            accepted (in some arbitrary order). Otherwise, yield pairs
559            of the form `(word, end_state)`, where `end_state` is the
560            final state the automaton reaches when accepting `word`.
561        """
562
563        for i in range(max_length + 1):
564            for word in self.enumerate_fixed_length_paths(
565                    i, start_vertex=start_vertex, with_states=with_states):
566                yield word
567
568    def rename_generators(self, rename_map, inplace=True):
569        """Get a new automaton whose edge labels have been renamed by a
570        specified map.
571
572        Parameters
573        ----------
574        rename_map : dict
575            dictionary giving the mapping from old labels to new
576            labels. Must provide a mapping for every label appearing
577            in the original automaton.
578
579        inplace : bool
580            If True (the default), modify the current automaton
581            in-place. If False, construct and return a new automaton
582            and leave this one unchanged.
583
584        Returns
585        -------
586        FSA or None
587            If inplace is False, return a new automaton with renamed
588            edge labels. Otherwise, None.
589
590    """
591        new_dict = {
592            vertex: {
593                rename_map[label]: neighbor
594                for label, neighbor in neighbors.items()
595            }
596            for vertex, neighbors in self._graph_dict.items()
597        }
598        if inplace:
599            self._from_graph_dict(new_dict)
600        else:
601            return FSA(new_dict, self.start_vertices)
602
603    def automaton_multiple(self, multiple):
604        """Get a new automaton, which accepts words in the language of this
605        automaton whose length is a multiple of k for some fixed k.
606
607        Parameters
608        ----------
609        multiple : int
610            Integer k such that every word accepted by the new
611            automaton has length kn for some n.
612
613        Returns
614        -------
615        FSA
616            A finite-state automaton which accepts exactly the words
617            accepted by self whose length is a multiple of k.
618
619        """
620        to_visit = deque(self.start_vertices)
621        visited = {
622            v : False for v in self.vertices()
623        }
624        new_automaton = FSA(start_vertices=self.start_vertices)
625        while(len(to_visit) > 0):
626            v = to_visit.popleft()
627            visited[v] = True
628            new_automaton.add_vertices([v])
629
630            for word, neighbor in self.enumerate_fixed_length_paths(
631                multiple,
632                start_vertex=v,
633                with_states=True
634            ):
635                new_automaton.add_vertices([neighbor])
636                new_automaton.add_edges([(v, neighbor, word)],
637                                        elist=False)
638
639                if not visited[neighbor]:
640                    to_visit.append(neighbor)
641
642        return new_automaton
643
644    def even_automaton(self):
645        """Get a new automaton which accepts exactly the the words accepted by
646        this automaton which have even length.
647
648        Alias for automaton_multiple(2).
649
650        Returns
651        -------
652        FSA
653            A finite-state automaton which accepts exactly the words
654            accepted by self whose length is even.
655
656        """
657        return self.automaton_multiple(2)
658
659    def accepts(self, word, start_vertex=None):
660        """Determine if this automaton accepts a given word.
661
662        Parameters
663        ----------
664        word : string
665            word to test for acceptance
666        start_vertex : object
667            The start state for the automaton. If `None` (the
668            default), any start state is allowed.
669
670        Returns
671        -------
672        bool
673            True if word is accepted by the automaton, False
674            otherwise.
675
676        """
677        start_vertices = [start_vertex]
678        if start_vertex is None:
679            start_vertices = self.start_vertices
680
681        for vertex in start_vertices:
682            try:
683                self.follow_word(word, vertex)
684                return True
685            except FSAException:
686                pass
687
688        return False
689
690
691
692def load_kbmag_file(filename) -> FSA:
693
694    """Build a finite-state automaton from a GAP record file produced by
695    the kbmag program.
696
697    Parameters
698    ----------
699    filename : string
700        the GAP record file containing automaton data.
701
702    Returns
703    -------
704    FSA
705        The automaton described by the
706        file.
707
708    """
709    gap_record = gap_parse.load_record_file(filename)
710    return _from_gap_record(gap_record)
711
712
713def _from_gap_record(gap_record) -> FSA:
714    """Load an automaton from a GAP record
715
716    Parameters
717    ----------
718    gap_record : dict
719        Preparsed GAP record to load
720
721    Returns
722    -------
723    FSA
724        finite-state automaton represented by this record
725
726    """
727
728    for fsa_dict in gap_record.values():
729        if fsa_dict["isFSA"] == "true":
730            labels = fsa_dict["alphabet"]["names"]
731            transitions = fsa_dict["table"]["transitions"]
732            initial = fsa_dict["initial"]
733
734            out_dict = kbmag_utils.build_dict(
735                transitions, labels, to_filter=[0]
736            )
737
738            return FSA(out_dict, start_vertices=initial)
739
740
741def load_builtin(filename):
742    """Load a finite-state automaton built in to the automata subpackage.
743
744    Parameters
745    ----------
746    filename : string
747        Name of the automaton file to load
748
749    Returns
750    -------
751    FSA
752        finite-state automaton read from this file
753
754    """
755    try:
756        builtin_path = importlib.resources.files(automata) / BUILTIN_DIR / filename
757        with open(builtin_path) as builtin_file:
758            aut_string = builtin_file.read()
759    except AttributeError as e:
760        # we still support python 3.8 since it's not EOL yet
761        from pkg_resources import resource_string as resource_bytes
762        aut_bytes = resource_bytes(
763            __name__, "{}/{}".format(BUILTIN_DIR, filename)
764        )
765        aut_string = aut_bytes.decode('utf-8')
766
767
768    record, _ = gap_parse.parse_record(aut_string)
769    return _from_gap_record(record)
770
771
772def list_builtins():
773    """Return a list of all the automata included with the automata
774    subpackage.
775
776    """
777    try:
778        builtin_dir = importlib.resources.files(automata) / BUILTIN_DIR
779        return [
780            os.path.basename(name)
781            for name in builtin_dir.iterdir()
782        ]
783    except AttributeError as e:
784        # we still support python 3.8 since it's not EOL
785        from pkg_resources import resource_listdir
786        return resource_listdir(__name__, BUILTIN_DIR)
787
788def free_automaton(generating_set):
789    """Return an automaton accepting freely reduced words in a set of
790    generators
791
792    Parameters
793    ----------
794    generating_set : iterable of strings
795        Names of the generators for this free group
796
797    Returns
798    -------
799    FSA
800        Automaton accepting freely reduced words in the generators
801        (and their inverses)
802
803    """
804    generators = list(generating_set) + [
805        words.invert_gen(g) for g in generating_set
806    ]
807    graph = {
808        g : {h:h for h in generators
809             if words.invert_gen(h) != g}
810        for g in [''] + generators
811    }
812    fsa = FSA(graph, start_vertices=[''])
813    return fsa
814
815def _hidden_vertices(graph_dict):
816    vertices = list(graph_dict)
817    hidden = []
818    for v, neighbors in graph_dict.items():
819        for _, neighbor in neighbors.items():
820            if (neighbor not in vertices and
821                neighbor not in hidden):
822                hidden.append(neighbor)
823
824    return hidden
BUILTIN_DIR = 'builtin'
class FSAException(builtins.Exception):
62class FSAException(Exception):
63    pass

Common base class for all non-exit exceptions.

Inherited Members
builtins.Exception
Exception
builtins.BaseException
with_traceback
add_note
args
class FSA:
 65class FSA:
 66    """FSA: a finite state-automaton.
 67
 68    The underlying data structure of a finite-state automaton is a
 69    python dictionary of the form:
 70
 71    ```
 72    {
 73      vertex1: {label_a: neighbor_a, label_b: neighbor_b, ...},
 74      vertex2: ....,
 75    }
 76    ```
 77
 78    That is, `vertex1` is connected to `neighbor_a` by an edge with
 79    label `label_a`, etc.
 80
 81    The automaton can be constructed by passing it a dictionary of
 82    this form, or by adding vertices/edges individually through the
 83    built-in methods.
 84
 85    """
 86    def __init__(self, vert_dict={}, start_vertices=[], graph_dict=True):
 87        """
 88
 89        Parameters
 90        ----------
 91        vert_dict : dict
 92            dictionary of vertices used to build the automaton. By
 93            default, this dictionary should have the format specified
 94            above.
 95
 96        start_vertices : list
 97            list of vertices which are valid start states
 98            for this automaton. Currently, the `FSA` class will not
 99            correctly handle lists of length > 1.
100
101        graph_dict : bool
102            If `True` (the default), expect a dictionary of the format
103            listed above. If `False`, instead expect a dictionary with format:
104
105            ```
106            {
107              vertex1: {neighbor_a: [neighbor_a_label1, neighbor_a_label2, ...],
108                        neighbor_b: [neighbor_b_label1, neighbor_b_label2, ...], ....},
109              vertex2: ....,
110            }
111            ```
112        """
113        if graph_dict:
114            self._from_graph_dict(vert_dict)
115        else:
116            self._out_dict = FSA._defaultify_out_dict(vert_dict)
117
118            self._build_in_dict()
119            self._build_graph_dict()
120
121        self.start_vertices = start_vertices
122
123    @staticmethod
124    def _defaultify_out_dict(out_dict):
125        return {
126            key: defaultdict(list, copy.deepcopy(value))
127            for key, value in out_dict.items()
128        }
129
130    def _from_graph_dict(self, graph_dict):
131        self._graph_dict = copy.deepcopy(graph_dict)
132        hidden = _hidden_vertices(graph_dict)
133        for v in hidden:
134            self._graph_dict[v] = {}
135
136        out_dict = {v : {} for v in self._graph_dict}
137
138        for v, neighbors in self._graph_dict.items():
139            out_dict[v] = defaultdict(list)
140            for label, neighbor in neighbors.items():
141                out_dict[v][neighbor].append(label)
142
143        self._out_dict = out_dict
144        self._build_in_dict()
145
146    def _build_graph_dict(self):
147        label_dict = {v:{} for v in self._out_dict}
148        for v, neighbors_out in self._out_dict.items():
149            for w, labels in neighbors_out.items():
150                for label in labels:
151                    label_dict[v][label] = w
152        self._graph_dict = label_dict
153
154    def _build_in_dict(self):
155        in_dict = defaultdict(dict)
156        for v, neighbors_out in self._out_dict.items():
157            for w, labels in neighbors_out.items():
158                in_dict[w][v] = labels
159        self._in_dict = in_dict
160
161    def __str__(self):
162        return "FSA with dict:\n{}".format(self._graph_dict.__str__())
163
164    def __repr__(self):
165        return "FSA({})".format(self._graph_dict.__repr__())
166
167    @property
168    def out_dict(self):
169        return self._out_dict
170
171    @property
172    def graph_dict(self):
173        return self._graph_dict
174
175    @property
176    def in_dict(self):
177        return self._in_dict
178
179    def has_edge(self, tail, head):
180        return len(self._out_dict[tail][head]) > 0
181
182    def edges_out(self, vertex):
183        """Get the list of edges directed away from a vertex.
184        """
185        for w, labels in self._out_dict[vertex].items():
186            for label in labels:
187                yield (vertex, w, label)
188
189    def edges_in(self, vertex):
190        """Get the list of edges directed towards a vertex."""
191        for w, labels in self._in_dict[vertex].items():
192            for label in labels:
193                yield (w, vertex, label)
194
195    def neighbors_out(self, vertex):
196        """Get the list of outward neighbors of a vertex."""
197        return self._out_dict[vertex].keys()
198
199    def neighbors_in(self, vertex):
200        """Get the list of inward neighbors of a vertex."""
201        return self._in_dict[vertex].keys()
202
203    def edge_label(self, tail, head):
204        """Get the label of an edge from its tail and head vertex.
205
206        This function will raise an exception if there is not a unique
207        edge between `tail` and `head`.
208
209        """
210        if len(self._out_dict[tail][head]) == 1:
211            return self._out_dict[tail][head][0]
212        else:
213            raise ValueError("ambiguous edge specification: there is not exactly"
214                            f" one edge between {tail} and {head}" )
215
216    def edge_labels(self, tail, head):
217        """Get the list of labels associated to all the edges between `tail`
218        and `head`.
219
220        """
221        return self._out_dict[tail][head]
222
223    def add_vertices(self, vertices):
224        """Add vertices to the FSA.
225        """
226        for v in vertices:
227            if v not in self._out_dict:
228                self._out_dict[v] = defaultdict(list)
229                self._in_dict[v] = defaultdict(list)
230                self._graph_dict[v] = defaultdict(dict)
231
232    def add_edges(self, edges, elist=False,
233                  ignore_redundant=True):
234        """Add edges to the FSA.
235
236        Parameters
237        ----------
238        edges : list
239            a list of tuples of the form `(tail, head, label)`,
240            specifying an edge to add.
241
242        elist : bool
243            if `True`, then `label` should be interpreted as a single
244            edge label. Otherwise, `label` is interpreted as a list of
245            labels for multiple edges to be added between vertices.
246
247        """
248        for e in edges:
249            tail, head, label = e
250
251            # no-op if vertices are already in FSA
252            self.add_vertices([tail, head])
253
254            if head not in self._out_dict[tail]:
255                self._out_dict[tail][head] = []
256                self._in_dict[head][tail] = []
257
258            if ignore_redundant and label in self._out_dict[tail][head]:
259                continue
260
261            if elist:
262                self._out_dict[tail][head] += label
263                self._in_dict[head][tail] += label
264                for l in label:
265                    self._graph_dict[tail][l] = head
266
267            else:
268                self._out_dict[tail][head].append(label)
269                self._in_dict[head][tail].append(label)
270                self._graph_dict[tail][label] = head
271
272    def delete_vertices(self, vertices):
273        """Delete several vertices from the FSA.
274        """
275        for v in vertices:
276            self.delete_vertex(v)
277
278    def delete_vertex(self, vertex):
279        """Delete a single vertex from the FSA.
280        """
281        for w in self.neighbors_out(vertex):
282            self._in_dict[w].pop(vertex)
283        for w in self.neighbors_in(vertex):
284            self._out_dict[w].pop(vertex)
285
286            for lab in list(self._graph_dict[w]):
287                if self._graph_dict[w][lab] == vertex:
288                    self._graph_dict[w].pop(lab)
289
290        self._out_dict.pop(vertex)
291        self._in_dict.pop(vertex)
292        self._graph_dict.pop(vertex)
293
294    def vertices(self):
295        return self._out_dict.keys()
296
297    def edges(self, with_labels=False):
298        """Get all of the edges of this FSA
299
300        Yields
301        ------
302        tuple
303            An ordered pair of the form `(tail, head)` for each edge
304            in the automaton
305
306        """
307        for v, nbrs in self._graph_dict.items():
308            for label, w in nbrs.items():
309                if with_labels:
310                    yield (v, w, label)
311                else:
312                    yield (v, w)
313
314    def recurrent(self, inplace=False):
315        """Find a version of the automaton which is recurrent, i.e. which has
316        no (forward or backward) dead ends.
317
318        Parameters
319        ----------
320        inplace : bool
321            if `True`, modify the automaton in-place. Otherwise,
322            return a recurrent copy.
323
324        Returns
325        -------
326        FSA or None
327            A recurrent version of the automaton if `inplace` is
328            `True`, otherwise `None`.
329
330        """
331
332        to_modify = self
333
334        if not inplace:
335            to_modify = copy.deepcopy(self)
336
337        still_pruning = True
338        while still_pruning:
339            still_pruning = False
340            vertices = list(to_modify._out_dict.keys())
341            for v in vertices:
342                if (len(to_modify._out_dict[v]) == 0 or
343                    len(to_modify._in_dict[v]) == 0):
344                    to_modify.delete_vertex(v)
345                    still_pruning = True
346
347        if not inplace:
348            return to_modify
349
350    def remove_long_paths(self, root=None, edge_ties=True,
351                          return_distances=False):
352        """Find a version of the automaton which does not backtrack.
353
354        This function is the opposite of `make_recurrent`: it finds a
355        subgraph of the automaton which is the union of
356        shortest-length paths from the root vertex to each node. This
357        will be a directed acyclic graph (in fact, a tree if
358        `edge_ties` is false).
359
360        It is mostly useful for visualizing the structure of the
361        automaton.
362
363        Parameters
364        ----------
365        edge_ties : bool
366            if `True`, if two edges give rise to paths of the
367            same length, include both of them in the new graph.
368
369    """
370        H = FSA({})
371        H.add_vertices(self.vertices())
372
373        #Dijkstra's algorithm!
374        distance = {}
375        marked = {v:False for v in self.vertices()}
376
377        if root is None:
378            root = self.start_vertices[0]
379
380        marked[root] = True
381        distance[root] = 0
382        vertex_queue = deque([root])
383        while len(vertex_queue) > 0:
384            v = vertex_queue.popleft()
385
386            to_visit = [w for w in self.neighbors_out(v) if not marked[w]]
387            for w in to_visit:
388                marked[w] = True
389                distance[w] = distance[v] + 1
390
391            short_nbrs = to_visit
392            if edge_ties:
393                short_nbrs = [w for w in self.neighbors_out(v)
394                               if distance[w] == distance[v] + 1]
395
396            H.add_edges([(v, w, self.edge_labels(v,w)) for w in short_nbrs],
397                        elist= True)
398            vertex_queue.extend(to_visit)
399
400        return H
401
402    def initial_accepted_subword(self, word):
403        """Find an initial subword of a given word which is accepted by the
404        automaton.
405
406        Parameters
407        ----------
408        word : string
409            The word the automaton should try to accept
410
411        Returns
412        -------
413        string
414            An initial subword of `word` which is accepted by the
415            automaton, with maximal length. This may be either the
416            empty word or `word` itself.
417
418    """
419        vertex = self.start_vertices[0]
420        subword = ""
421        for letter in word:
422            try:
423                vertex = self.graph_dict[vertex][letter]
424            except KeyError:
425                return subword
426            subword += letter
427
428        return subword
429
430    def initial_rejected_subword(self, word):
431        """Find an initial subword of a given word which is accepted by the
432        automaton, or None if the given word is accepted.
433
434        Parameters
435        ----------
436        word : string
437            The word the automaton should try to accept.
438
439        Returns
440        -------
441        string
442            An initial subword of `word` which is rejected by the
443            automaton, with minimal length. If the word is accepted,
444            return None.
445
446        """
447        vertex = self.start_vertices[0]
448        subword = ""
449        for letter in word:
450            try:
451                vertex = self.graph_dict[vertex][letter]
452            except KeyError:
453                return subword + letter
454            subword += letter
455
456        return subword
457
458
459    def follow_word(self, word, start_vertex=None):
460        """Find the final state of the automaton when it tries to accept a word
461
462        Parameters
463        ----------
464        word : string
465            The word the automaton should try to accept
466
467        start_vertex: object
468            The start state for the automaton. If `None` (the
469            default), use the automaton's default start state.
470
471        Returns
472        -------
473        object
474            The state of the automaton when it accepts `word`
475
476        Raises
477        ------
478        FSAException
479            Raised if word is not accepted by this automaton.
480
481        """
482
483        if start_vertex is None:
484            start_vertex = self.start_vertices[0]
485        vertex = start_vertex
486
487        for letter in word:
488            try:
489                vertex = self.graph_dict[vertex][letter]
490            except KeyError:
491                raise FSAException(
492                    f"The word '{word}' is not accepted by the automaton."
493                )
494
495        return vertex
496
497    def enumerate_fixed_length_paths(self, length, start_vertex=None,
498                                     with_states=False):
499        """Enumerate all words of a fixed length accepted by the automaton,
500        starting at a given state.
501
502        Parameters
503        ----------
504        length : int
505            the length of the words we want to enumerate
506
507        start_vertex : object
508            which vertex to start at. If `None`, use the
509            default starting state for the automaton.
510
511        with_states : bool
512            if `True`, also yield the state of the
513            automaton at each accepted word.
514
515        Yields
516        ------
517        string or (string, object)
518            If `with_states` is false, yield the sequence of words
519            accepted (in some arbitrary order). Otherwise, yield pairs
520            of the form `(word, end_state)`, where `end_state` is the
521            final state the automaton reaches when accepting `word`.
522        """
523        if start_vertex is None:
524            start_vertex = self.start_vertices[0]
525
526        if length <= 0:
527            if with_states:
528                yield ("", start_vertex)
529            else:
530                yield ""
531        else:
532            for word, vertex in self.enumerate_fixed_length_paths(
533                    length - 1, start_vertex=start_vertex, with_states=True):
534                for label, neighbor in self._graph_dict[vertex].items():
535                    if with_states:
536                        yield (word + label, neighbor)
537                    else:
538                        yield word + label
539
540    def enumerate_words(self, max_length, start_vertex=None,
541                        with_states=False):
542        """Enumerate all words up to a given length accepted by the automaton.
543
544        Parameters
545        ----------
546        max_length : int
547            maximum length of a word to enumerate
548        start_vertex : object
549            the start vertex for accepting words. If `None`, use the
550            automaton's start vertex
551        with_states : bool
552            if `True`, also yield the state of the automaton at each
553            accepted word
554
555        Yields
556        ------
557        string or (string, object)
558            If `with_states` is false, yield the sequence of words
559            accepted (in some arbitrary order). Otherwise, yield pairs
560            of the form `(word, end_state)`, where `end_state` is the
561            final state the automaton reaches when accepting `word`.
562        """
563
564        for i in range(max_length + 1):
565            for word in self.enumerate_fixed_length_paths(
566                    i, start_vertex=start_vertex, with_states=with_states):
567                yield word
568
569    def rename_generators(self, rename_map, inplace=True):
570        """Get a new automaton whose edge labels have been renamed by a
571        specified map.
572
573        Parameters
574        ----------
575        rename_map : dict
576            dictionary giving the mapping from old labels to new
577            labels. Must provide a mapping for every label appearing
578            in the original automaton.
579
580        inplace : bool
581            If True (the default), modify the current automaton
582            in-place. If False, construct and return a new automaton
583            and leave this one unchanged.
584
585        Returns
586        -------
587        FSA or None
588            If inplace is False, return a new automaton with renamed
589            edge labels. Otherwise, None.
590
591    """
592        new_dict = {
593            vertex: {
594                rename_map[label]: neighbor
595                for label, neighbor in neighbors.items()
596            }
597            for vertex, neighbors in self._graph_dict.items()
598        }
599        if inplace:
600            self._from_graph_dict(new_dict)
601        else:
602            return FSA(new_dict, self.start_vertices)
603
604    def automaton_multiple(self, multiple):
605        """Get a new automaton, which accepts words in the language of this
606        automaton whose length is a multiple of k for some fixed k.
607
608        Parameters
609        ----------
610        multiple : int
611            Integer k such that every word accepted by the new
612            automaton has length kn for some n.
613
614        Returns
615        -------
616        FSA
617            A finite-state automaton which accepts exactly the words
618            accepted by self whose length is a multiple of k.
619
620        """
621        to_visit = deque(self.start_vertices)
622        visited = {
623            v : False for v in self.vertices()
624        }
625        new_automaton = FSA(start_vertices=self.start_vertices)
626        while(len(to_visit) > 0):
627            v = to_visit.popleft()
628            visited[v] = True
629            new_automaton.add_vertices([v])
630
631            for word, neighbor in self.enumerate_fixed_length_paths(
632                multiple,
633                start_vertex=v,
634                with_states=True
635            ):
636                new_automaton.add_vertices([neighbor])
637                new_automaton.add_edges([(v, neighbor, word)],
638                                        elist=False)
639
640                if not visited[neighbor]:
641                    to_visit.append(neighbor)
642
643        return new_automaton
644
645    def even_automaton(self):
646        """Get a new automaton which accepts exactly the the words accepted by
647        this automaton which have even length.
648
649        Alias for automaton_multiple(2).
650
651        Returns
652        -------
653        FSA
654            A finite-state automaton which accepts exactly the words
655            accepted by self whose length is even.
656
657        """
658        return self.automaton_multiple(2)
659
660    def accepts(self, word, start_vertex=None):
661        """Determine if this automaton accepts a given word.
662
663        Parameters
664        ----------
665        word : string
666            word to test for acceptance
667        start_vertex : object
668            The start state for the automaton. If `None` (the
669            default), any start state is allowed.
670
671        Returns
672        -------
673        bool
674            True if word is accepted by the automaton, False
675            otherwise.
676
677        """
678        start_vertices = [start_vertex]
679        if start_vertex is None:
680            start_vertices = self.start_vertices
681
682        for vertex in start_vertices:
683            try:
684                self.follow_word(word, vertex)
685                return True
686            except FSAException:
687                pass
688
689        return False

FSA: a finite state-automaton.

The underlying data structure of a finite-state automaton is a python dictionary of the form:

{
  vertex1: {label_a: neighbor_a, label_b: neighbor_b, ...},
  vertex2: ....,
}

That is, vertex1 is connected to neighbor_a by an edge with label label_a, etc.

The automaton can be constructed by passing it a dictionary of this form, or by adding vertices/edges individually through the built-in methods.

FSA(vert_dict={}, start_vertices=[], graph_dict=True)
 86    def __init__(self, vert_dict={}, start_vertices=[], graph_dict=True):
 87        """
 88
 89        Parameters
 90        ----------
 91        vert_dict : dict
 92            dictionary of vertices used to build the automaton. By
 93            default, this dictionary should have the format specified
 94            above.
 95
 96        start_vertices : list
 97            list of vertices which are valid start states
 98            for this automaton. Currently, the `FSA` class will not
 99            correctly handle lists of length > 1.
100
101        graph_dict : bool
102            If `True` (the default), expect a dictionary of the format
103            listed above. If `False`, instead expect a dictionary with format:
104
105            ```
106            {
107              vertex1: {neighbor_a: [neighbor_a_label1, neighbor_a_label2, ...],
108                        neighbor_b: [neighbor_b_label1, neighbor_b_label2, ...], ....},
109              vertex2: ....,
110            }
111            ```
112        """
113        if graph_dict:
114            self._from_graph_dict(vert_dict)
115        else:
116            self._out_dict = FSA._defaultify_out_dict(vert_dict)
117
118            self._build_in_dict()
119            self._build_graph_dict()
120
121        self.start_vertices = start_vertices
Parameters
  • vert_dict (dict): dictionary of vertices used to build the automaton. By default, this dictionary should have the format specified above.
  • start_vertices (list): list of vertices which are valid start states for this automaton. Currently, the FSA class will not correctly handle lists of length > 1.
  • graph_dict (bool): If True (the default), expect a dictionary of the format listed above. If False, instead expect a dictionary with format:
    {
      vertex1: {neighbor_a: [neighbor_a_label1, neighbor_a_label2, ...],
                neighbor_b: [neighbor_b_label1, neighbor_b_label2, ...], ....},
      vertex2: ....,
    }
    
start_vertices
out_dict
167    @property
168    def out_dict(self):
169        return self._out_dict
graph_dict
171    @property
172    def graph_dict(self):
173        return self._graph_dict
in_dict
175    @property
176    def in_dict(self):
177        return self._in_dict
def has_edge(self, tail, head):
179    def has_edge(self, tail, head):
180        return len(self._out_dict[tail][head]) > 0
def edges_out(self, vertex):
182    def edges_out(self, vertex):
183        """Get the list of edges directed away from a vertex.
184        """
185        for w, labels in self._out_dict[vertex].items():
186            for label in labels:
187                yield (vertex, w, label)

Get the list of edges directed away from a vertex.

def edges_in(self, vertex):
189    def edges_in(self, vertex):
190        """Get the list of edges directed towards a vertex."""
191        for w, labels in self._in_dict[vertex].items():
192            for label in labels:
193                yield (w, vertex, label)

Get the list of edges directed towards a vertex.

def neighbors_out(self, vertex):
195    def neighbors_out(self, vertex):
196        """Get the list of outward neighbors of a vertex."""
197        return self._out_dict[vertex].keys()

Get the list of outward neighbors of a vertex.

def neighbors_in(self, vertex):
199    def neighbors_in(self, vertex):
200        """Get the list of inward neighbors of a vertex."""
201        return self._in_dict[vertex].keys()

Get the list of inward neighbors of a vertex.

def edge_label(self, tail, head):
203    def edge_label(self, tail, head):
204        """Get the label of an edge from its tail and head vertex.
205
206        This function will raise an exception if there is not a unique
207        edge between `tail` and `head`.
208
209        """
210        if len(self._out_dict[tail][head]) == 1:
211            return self._out_dict[tail][head][0]
212        else:
213            raise ValueError("ambiguous edge specification: there is not exactly"
214                            f" one edge between {tail} and {head}" )

Get the label of an edge from its tail and head vertex.

This function will raise an exception if there is not a unique edge between tail and head.

def edge_labels(self, tail, head):
216    def edge_labels(self, tail, head):
217        """Get the list of labels associated to all the edges between `tail`
218        and `head`.
219
220        """
221        return self._out_dict[tail][head]

Get the list of labels associated to all the edges between tail and head.

def add_vertices(self, vertices):
223    def add_vertices(self, vertices):
224        """Add vertices to the FSA.
225        """
226        for v in vertices:
227            if v not in self._out_dict:
228                self._out_dict[v] = defaultdict(list)
229                self._in_dict[v] = defaultdict(list)
230                self._graph_dict[v] = defaultdict(dict)

Add vertices to the FSA.

def add_edges(self, edges, elist=False, ignore_redundant=True):
232    def add_edges(self, edges, elist=False,
233                  ignore_redundant=True):
234        """Add edges to the FSA.
235
236        Parameters
237        ----------
238        edges : list
239            a list of tuples of the form `(tail, head, label)`,
240            specifying an edge to add.
241
242        elist : bool
243            if `True`, then `label` should be interpreted as a single
244            edge label. Otherwise, `label` is interpreted as a list of
245            labels for multiple edges to be added between vertices.
246
247        """
248        for e in edges:
249            tail, head, label = e
250
251            # no-op if vertices are already in FSA
252            self.add_vertices([tail, head])
253
254            if head not in self._out_dict[tail]:
255                self._out_dict[tail][head] = []
256                self._in_dict[head][tail] = []
257
258            if ignore_redundant and label in self._out_dict[tail][head]:
259                continue
260
261            if elist:
262                self._out_dict[tail][head] += label
263                self._in_dict[head][tail] += label
264                for l in label:
265                    self._graph_dict[tail][l] = head
266
267            else:
268                self._out_dict[tail][head].append(label)
269                self._in_dict[head][tail].append(label)
270                self._graph_dict[tail][label] = head

Add edges to the FSA.

Parameters
  • edges (list): a list of tuples of the form (tail, head, label), specifying an edge to add.
  • elist (bool): if True, then label should be interpreted as a single edge label. Otherwise, label is interpreted as a list of labels for multiple edges to be added between vertices.
def delete_vertices(self, vertices):
272    def delete_vertices(self, vertices):
273        """Delete several vertices from the FSA.
274        """
275        for v in vertices:
276            self.delete_vertex(v)

Delete several vertices from the FSA.

def delete_vertex(self, vertex):
278    def delete_vertex(self, vertex):
279        """Delete a single vertex from the FSA.
280        """
281        for w in self.neighbors_out(vertex):
282            self._in_dict[w].pop(vertex)
283        for w in self.neighbors_in(vertex):
284            self._out_dict[w].pop(vertex)
285
286            for lab in list(self._graph_dict[w]):
287                if self._graph_dict[w][lab] == vertex:
288                    self._graph_dict[w].pop(lab)
289
290        self._out_dict.pop(vertex)
291        self._in_dict.pop(vertex)
292        self._graph_dict.pop(vertex)

Delete a single vertex from the FSA.

def vertices(self):
294    def vertices(self):
295        return self._out_dict.keys()
def edges(self, with_labels=False):
297    def edges(self, with_labels=False):
298        """Get all of the edges of this FSA
299
300        Yields
301        ------
302        tuple
303            An ordered pair of the form `(tail, head)` for each edge
304            in the automaton
305
306        """
307        for v, nbrs in self._graph_dict.items():
308            for label, w in nbrs.items():
309                if with_labels:
310                    yield (v, w, label)
311                else:
312                    yield (v, w)

Get all of the edges of this FSA

Yields
  • tuple: An ordered pair of the form (tail, head) for each edge in the automaton
def recurrent(self, inplace=False):
314    def recurrent(self, inplace=False):
315        """Find a version of the automaton which is recurrent, i.e. which has
316        no (forward or backward) dead ends.
317
318        Parameters
319        ----------
320        inplace : bool
321            if `True`, modify the automaton in-place. Otherwise,
322            return a recurrent copy.
323
324        Returns
325        -------
326        FSA or None
327            A recurrent version of the automaton if `inplace` is
328            `True`, otherwise `None`.
329
330        """
331
332        to_modify = self
333
334        if not inplace:
335            to_modify = copy.deepcopy(self)
336
337        still_pruning = True
338        while still_pruning:
339            still_pruning = False
340            vertices = list(to_modify._out_dict.keys())
341            for v in vertices:
342                if (len(to_modify._out_dict[v]) == 0 or
343                    len(to_modify._in_dict[v]) == 0):
344                    to_modify.delete_vertex(v)
345                    still_pruning = True
346
347        if not inplace:
348            return to_modify

Find a version of the automaton which is recurrent, i.e. which has no (forward or backward) dead ends.

Parameters
  • inplace (bool): if True, modify the automaton in-place. Otherwise, return a recurrent copy.
Returns
  • FSA or None: A recurrent version of the automaton if inplace is True, otherwise None.
def remove_long_paths(self, root=None, edge_ties=True, return_distances=False):
350    def remove_long_paths(self, root=None, edge_ties=True,
351                          return_distances=False):
352        """Find a version of the automaton which does not backtrack.
353
354        This function is the opposite of `make_recurrent`: it finds a
355        subgraph of the automaton which is the union of
356        shortest-length paths from the root vertex to each node. This
357        will be a directed acyclic graph (in fact, a tree if
358        `edge_ties` is false).
359
360        It is mostly useful for visualizing the structure of the
361        automaton.
362
363        Parameters
364        ----------
365        edge_ties : bool
366            if `True`, if two edges give rise to paths of the
367            same length, include both of them in the new graph.
368
369    """
370        H = FSA({})
371        H.add_vertices(self.vertices())
372
373        #Dijkstra's algorithm!
374        distance = {}
375        marked = {v:False for v in self.vertices()}
376
377        if root is None:
378            root = self.start_vertices[0]
379
380        marked[root] = True
381        distance[root] = 0
382        vertex_queue = deque([root])
383        while len(vertex_queue) > 0:
384            v = vertex_queue.popleft()
385
386            to_visit = [w for w in self.neighbors_out(v) if not marked[w]]
387            for w in to_visit:
388                marked[w] = True
389                distance[w] = distance[v] + 1
390
391            short_nbrs = to_visit
392            if edge_ties:
393                short_nbrs = [w for w in self.neighbors_out(v)
394                               if distance[w] == distance[v] + 1]
395
396            H.add_edges([(v, w, self.edge_labels(v,w)) for w in short_nbrs],
397                        elist= True)
398            vertex_queue.extend(to_visit)
399
400        return H

Find a version of the automaton which does not backtrack.

This function is the opposite of make_recurrent: it finds a subgraph of the automaton which is the union of shortest-length paths from the root vertex to each node. This will be a directed acyclic graph (in fact, a tree if edge_ties is false).

It is mostly useful for visualizing the structure of the automaton.

Parameters
  • edge_ties (bool): if True, if two edges give rise to paths of the same length, include both of them in the new graph.
def initial_accepted_subword(self, word):
402    def initial_accepted_subword(self, word):
403        """Find an initial subword of a given word which is accepted by the
404        automaton.
405
406        Parameters
407        ----------
408        word : string
409            The word the automaton should try to accept
410
411        Returns
412        -------
413        string
414            An initial subword of `word` which is accepted by the
415            automaton, with maximal length. This may be either the
416            empty word or `word` itself.
417
418    """
419        vertex = self.start_vertices[0]
420        subword = ""
421        for letter in word:
422            try:
423                vertex = self.graph_dict[vertex][letter]
424            except KeyError:
425                return subword
426            subword += letter
427
428        return subword

Find an initial subword of a given word which is accepted by the automaton.

Parameters
  • word (string): The word the automaton should try to accept
Returns
  • string: An initial subword of word which is accepted by the automaton, with maximal length. This may be either the empty word or word itself.
def initial_rejected_subword(self, word):
430    def initial_rejected_subword(self, word):
431        """Find an initial subword of a given word which is accepted by the
432        automaton, or None if the given word is accepted.
433
434        Parameters
435        ----------
436        word : string
437            The word the automaton should try to accept.
438
439        Returns
440        -------
441        string
442            An initial subword of `word` which is rejected by the
443            automaton, with minimal length. If the word is accepted,
444            return None.
445
446        """
447        vertex = self.start_vertices[0]
448        subword = ""
449        for letter in word:
450            try:
451                vertex = self.graph_dict[vertex][letter]
452            except KeyError:
453                return subword + letter
454            subword += letter
455
456        return subword

Find an initial subword of a given word which is accepted by the automaton, or None if the given word is accepted.

Parameters
  • word (string): The word the automaton should try to accept.
Returns
  • string: An initial subword of word which is rejected by the automaton, with minimal length. If the word is accepted, return None.
def follow_word(self, word, start_vertex=None):
459    def follow_word(self, word, start_vertex=None):
460        """Find the final state of the automaton when it tries to accept a word
461
462        Parameters
463        ----------
464        word : string
465            The word the automaton should try to accept
466
467        start_vertex: object
468            The start state for the automaton. If `None` (the
469            default), use the automaton's default start state.
470
471        Returns
472        -------
473        object
474            The state of the automaton when it accepts `word`
475
476        Raises
477        ------
478        FSAException
479            Raised if word is not accepted by this automaton.
480
481        """
482
483        if start_vertex is None:
484            start_vertex = self.start_vertices[0]
485        vertex = start_vertex
486
487        for letter in word:
488            try:
489                vertex = self.graph_dict[vertex][letter]
490            except KeyError:
491                raise FSAException(
492                    f"The word '{word}' is not accepted by the automaton."
493                )
494
495        return vertex

Find the final state of the automaton when it tries to accept a word

Parameters
  • word (string): The word the automaton should try to accept
  • start_vertex (object): The start state for the automaton. If None (the default), use the automaton's default start state.
Returns
  • object: The state of the automaton when it accepts word
Raises
  • FSAException: Raised if word is not accepted by this automaton.
def enumerate_fixed_length_paths(self, length, start_vertex=None, with_states=False):
497    def enumerate_fixed_length_paths(self, length, start_vertex=None,
498                                     with_states=False):
499        """Enumerate all words of a fixed length accepted by the automaton,
500        starting at a given state.
501
502        Parameters
503        ----------
504        length : int
505            the length of the words we want to enumerate
506
507        start_vertex : object
508            which vertex to start at. If `None`, use the
509            default starting state for the automaton.
510
511        with_states : bool
512            if `True`, also yield the state of the
513            automaton at each accepted word.
514
515        Yields
516        ------
517        string or (string, object)
518            If `with_states` is false, yield the sequence of words
519            accepted (in some arbitrary order). Otherwise, yield pairs
520            of the form `(word, end_state)`, where `end_state` is the
521            final state the automaton reaches when accepting `word`.
522        """
523        if start_vertex is None:
524            start_vertex = self.start_vertices[0]
525
526        if length <= 0:
527            if with_states:
528                yield ("", start_vertex)
529            else:
530                yield ""
531        else:
532            for word, vertex in self.enumerate_fixed_length_paths(
533                    length - 1, start_vertex=start_vertex, with_states=True):
534                for label, neighbor in self._graph_dict[vertex].items():
535                    if with_states:
536                        yield (word + label, neighbor)
537                    else:
538                        yield word + label

Enumerate all words of a fixed length accepted by the automaton, starting at a given state.

Parameters
  • length (int): the length of the words we want to enumerate
  • start_vertex (object): which vertex to start at. If None, use the default starting state for the automaton.
  • with_states (bool): if True, also yield the state of the automaton at each accepted word.
Yields
  • string or (string, object): If with_states is false, yield the sequence of words accepted (in some arbitrary order). Otherwise, yield pairs of the form (word, end_state), where end_state is the final state the automaton reaches when accepting word.
def enumerate_words(self, max_length, start_vertex=None, with_states=False):
540    def enumerate_words(self, max_length, start_vertex=None,
541                        with_states=False):
542        """Enumerate all words up to a given length accepted by the automaton.
543
544        Parameters
545        ----------
546        max_length : int
547            maximum length of a word to enumerate
548        start_vertex : object
549            the start vertex for accepting words. If `None`, use the
550            automaton's start vertex
551        with_states : bool
552            if `True`, also yield the state of the automaton at each
553            accepted word
554
555        Yields
556        ------
557        string or (string, object)
558            If `with_states` is false, yield the sequence of words
559            accepted (in some arbitrary order). Otherwise, yield pairs
560            of the form `(word, end_state)`, where `end_state` is the
561            final state the automaton reaches when accepting `word`.
562        """
563
564        for i in range(max_length + 1):
565            for word in self.enumerate_fixed_length_paths(
566                    i, start_vertex=start_vertex, with_states=with_states):
567                yield word

Enumerate all words up to a given length accepted by the automaton.

Parameters
  • max_length (int): maximum length of a word to enumerate
  • start_vertex (object): the start vertex for accepting words. If None, use the automaton's start vertex
  • with_states (bool): if True, also yield the state of the automaton at each accepted word
Yields
  • string or (string, object): If with_states is false, yield the sequence of words accepted (in some arbitrary order). Otherwise, yield pairs of the form (word, end_state), where end_state is the final state the automaton reaches when accepting word.
def rename_generators(self, rename_map, inplace=True):
569    def rename_generators(self, rename_map, inplace=True):
570        """Get a new automaton whose edge labels have been renamed by a
571        specified map.
572
573        Parameters
574        ----------
575        rename_map : dict
576            dictionary giving the mapping from old labels to new
577            labels. Must provide a mapping for every label appearing
578            in the original automaton.
579
580        inplace : bool
581            If True (the default), modify the current automaton
582            in-place. If False, construct and return a new automaton
583            and leave this one unchanged.
584
585        Returns
586        -------
587        FSA or None
588            If inplace is False, return a new automaton with renamed
589            edge labels. Otherwise, None.
590
591    """
592        new_dict = {
593            vertex: {
594                rename_map[label]: neighbor
595                for label, neighbor in neighbors.items()
596            }
597            for vertex, neighbors in self._graph_dict.items()
598        }
599        if inplace:
600            self._from_graph_dict(new_dict)
601        else:
602            return FSA(new_dict, self.start_vertices)

Get a new automaton whose edge labels have been renamed by a specified map.

Parameters
  • rename_map (dict): dictionary giving the mapping from old labels to new labels. Must provide a mapping for every label appearing in the original automaton.
  • inplace (bool): If True (the default), modify the current automaton in-place. If False, construct and return a new automaton and leave this one unchanged.
Returns
  • FSA or None: If inplace is False, return a new automaton with renamed edge labels. Otherwise, None.
def automaton_multiple(self, multiple):
604    def automaton_multiple(self, multiple):
605        """Get a new automaton, which accepts words in the language of this
606        automaton whose length is a multiple of k for some fixed k.
607
608        Parameters
609        ----------
610        multiple : int
611            Integer k such that every word accepted by the new
612            automaton has length kn for some n.
613
614        Returns
615        -------
616        FSA
617            A finite-state automaton which accepts exactly the words
618            accepted by self whose length is a multiple of k.
619
620        """
621        to_visit = deque(self.start_vertices)
622        visited = {
623            v : False for v in self.vertices()
624        }
625        new_automaton = FSA(start_vertices=self.start_vertices)
626        while(len(to_visit) > 0):
627            v = to_visit.popleft()
628            visited[v] = True
629            new_automaton.add_vertices([v])
630
631            for word, neighbor in self.enumerate_fixed_length_paths(
632                multiple,
633                start_vertex=v,
634                with_states=True
635            ):
636                new_automaton.add_vertices([neighbor])
637                new_automaton.add_edges([(v, neighbor, word)],
638                                        elist=False)
639
640                if not visited[neighbor]:
641                    to_visit.append(neighbor)
642
643        return new_automaton

Get a new automaton, which accepts words in the language of this automaton whose length is a multiple of k for some fixed k.

Parameters
  • multiple (int): Integer k such that every word accepted by the new automaton has length kn for some n.
Returns
  • FSA: A finite-state automaton which accepts exactly the words accepted by self whose length is a multiple of k.
def even_automaton(self):
645    def even_automaton(self):
646        """Get a new automaton which accepts exactly the the words accepted by
647        this automaton which have even length.
648
649        Alias for automaton_multiple(2).
650
651        Returns
652        -------
653        FSA
654            A finite-state automaton which accepts exactly the words
655            accepted by self whose length is even.
656
657        """
658        return self.automaton_multiple(2)

Get a new automaton which accepts exactly the the words accepted by this automaton which have even length.

Alias for automaton_multiple(2).

Returns
  • FSA: A finite-state automaton which accepts exactly the words accepted by self whose length is even.
def accepts(self, word, start_vertex=None):
660    def accepts(self, word, start_vertex=None):
661        """Determine if this automaton accepts a given word.
662
663        Parameters
664        ----------
665        word : string
666            word to test for acceptance
667        start_vertex : object
668            The start state for the automaton. If `None` (the
669            default), any start state is allowed.
670
671        Returns
672        -------
673        bool
674            True if word is accepted by the automaton, False
675            otherwise.
676
677        """
678        start_vertices = [start_vertex]
679        if start_vertex is None:
680            start_vertices = self.start_vertices
681
682        for vertex in start_vertices:
683            try:
684                self.follow_word(word, vertex)
685                return True
686            except FSAException:
687                pass
688
689        return False

Determine if this automaton accepts a given word.

Parameters
  • word (string): word to test for acceptance
  • start_vertex (object): The start state for the automaton. If None (the default), any start state is allowed.
Returns
  • bool: True if word is accepted by the automaton, False otherwise.
def load_kbmag_file(filename) -> FSA:
693def load_kbmag_file(filename) -> FSA:
694
695    """Build a finite-state automaton from a GAP record file produced by
696    the kbmag program.
697
698    Parameters
699    ----------
700    filename : string
701        the GAP record file containing automaton data.
702
703    Returns
704    -------
705    FSA
706        The automaton described by the
707        file.
708
709    """
710    gap_record = gap_parse.load_record_file(filename)
711    return _from_gap_record(gap_record)

Build a finite-state automaton from a GAP record file produced by the kbmag program.

Parameters
  • filename (string): the GAP record file containing automaton data.
Returns
  • FSA: The automaton described by the file.
def load_builtin(filename):
742def load_builtin(filename):
743    """Load a finite-state automaton built in to the automata subpackage.
744
745    Parameters
746    ----------
747    filename : string
748        Name of the automaton file to load
749
750    Returns
751    -------
752    FSA
753        finite-state automaton read from this file
754
755    """
756    try:
757        builtin_path = importlib.resources.files(automata) / BUILTIN_DIR / filename
758        with open(builtin_path) as builtin_file:
759            aut_string = builtin_file.read()
760    except AttributeError as e:
761        # we still support python 3.8 since it's not EOL yet
762        from pkg_resources import resource_string as resource_bytes
763        aut_bytes = resource_bytes(
764            __name__, "{}/{}".format(BUILTIN_DIR, filename)
765        )
766        aut_string = aut_bytes.decode('utf-8')
767
768
769    record, _ = gap_parse.parse_record(aut_string)
770    return _from_gap_record(record)

Load a finite-state automaton built in to the automata subpackage.

Parameters
  • filename (string): Name of the automaton file to load
Returns
  • FSA: finite-state automaton read from this file
def list_builtins():
773def list_builtins():
774    """Return a list of all the automata included with the automata
775    subpackage.
776
777    """
778    try:
779        builtin_dir = importlib.resources.files(automata) / BUILTIN_DIR
780        return [
781            os.path.basename(name)
782            for name in builtin_dir.iterdir()
783        ]
784    except AttributeError as e:
785        # we still support python 3.8 since it's not EOL
786        from pkg_resources import resource_listdir
787        return resource_listdir(__name__, BUILTIN_DIR)

Return a list of all the automata included with the automata subpackage.

def free_automaton(generating_set):
789def free_automaton(generating_set):
790    """Return an automaton accepting freely reduced words in a set of
791    generators
792
793    Parameters
794    ----------
795    generating_set : iterable of strings
796        Names of the generators for this free group
797
798    Returns
799    -------
800    FSA
801        Automaton accepting freely reduced words in the generators
802        (and their inverses)
803
804    """
805    generators = list(generating_set) + [
806        words.invert_gen(g) for g in generating_set
807    ]
808    graph = {
809        g : {h:h for h in generators
810             if words.invert_gen(h) != g}
811        for g in [''] + generators
812    }
813    fsa = FSA(graph, start_vertices=[''])
814    return fsa

Return an automaton accepting freely reduced words in a set of generators

Parameters
  • generating_set (iterable of strings): Names of the generators for this free group
Returns
  • FSA: Automaton accepting freely reduced words in the generators (and their inverses)