1+ from graphviz import Digraph
2+ from .fsa import FSA , Transition
3+
4+ import matplotlib .pyplot as plt
5+ import networkx as nx
6+
7+ # plt is not thread safe, but graphviz is harder to install for cross-env compiles
8+ # this is sequentially safe anyway
9+ # notice that if relative path, it is relative to where you execute the code,
10+ # and invalid fsa always look weird as nx+plt cannot find a equilibrium
11+ def draw_fsa (fsa :FSA , show = False , save = True , filename = "./fsa.png" ):
12+ # 1. Initialize Graph and collect all possible states
13+ G = nx .MultiDiGraph ()
14+
15+ # Start with declared states, but use a set to allow for "orphans"
16+ all_states = set (fsa .states )
17+
18+ # Always include initial and accept states in the set (even if missing from fsa.states)
19+ if fsa .initial_state :
20+ all_states .add (fsa .initial_state )
21+ all_states .update (fsa .accept_states )
22+
23+ # 2. Add Transitions and discover states used in transitions
24+ for t in fsa .transitions :
25+ all_states .add (t .from_state )
26+ all_states .add (t .to_state )
27+ G .add_edge (t .from_state , t .to_state , label = t .symbol )
28+
29+ # Ensure all discovered states are in the graph
30+ G .add_nodes_from (all_states )
31+
32+ # 3. Layout and Figure
33+ # Kamada-Kawai is more stable than Spring for state machines
34+ pos = nx .kamada_kawai_layout (G )
35+ plt .figure (figsize = (10 , 7 ))
36+
37+ # 4. Draw Nodes with specific styling
38+ for node in G .nodes ():
39+ # Default Style
40+ color = 'skyblue'
41+ linewidth = 1.0
42+ edgecolor = 'black'
43+ label_suffix = ""
44+
45+ # Initial State Styling (Green fill)
46+ if node == fsa .initial_state :
47+ color = '#90ee90' # Light green
48+ label_suffix = "\n (start)"
49+
50+ # Accept State Styling (Double-circle effect/Thick border)
51+ if node in fsa .accept_states :
52+ linewidth = 4.0
53+ edgecolor = '#ff4500' # Orange-red border for visibility
54+
55+ nx .draw_networkx_nodes (
56+ G , pos ,
57+ nodelist = [node ],
58+ node_color = color ,
59+ edgecolors = edgecolor ,
60+ linewidths = linewidth ,
61+ node_size = 2500
62+ )
63+
64+ # 5. Draw Edges (Arcing handles multi-edges/non-determinism)
65+ nx .draw_networkx_edges (
66+ G , pos ,
67+ arrowstyle = '->' ,
68+ arrowsize = 25 ,
69+ connectionstyle = 'arc3,rad=0.15' ,
70+ edge_color = 'gray'
71+ )
72+
73+ # 6. Edge and Node Labels
74+ # Use bracket notation for the 'label' key in the data dict
75+ edge_labels = {(u , v ): d ['label' ] for u , v , k , d in G .edges (data = True , keys = True )}
76+ nx .draw_networkx_edge_labels (G , pos , edge_labels = edge_labels , font_size = 10 )
77+
78+ node_labels = {node : f"{ node } { '(start)' if node == fsa .initial_state else '' } " for node in G .nodes ()}
79+ nx .draw_networkx_labels (G , pos , labels = node_labels , font_size = 11 , font_weight = 'bold' )
80+
81+ plt .title (f"FSA:\n (Accept states marked with thick borders)" )
82+ plt .axis ('off' )
83+
84+ # 7. Save and Clean Up
85+ if save :
86+ plt .savefig (filename , bbox_inches = 'tight' )
87+
88+ if show :
89+ plt .show ()
90+
91+ plt .close ()
92+ print (f"Process complete. Memory flushed." )
93+
94+ def make_fsa (states , alphabet , transitions , initial , accept ):
95+ return FSA (
96+ states = states ,
97+ alphabet = alphabet ,
98+ transitions = [
99+ Transition (** t ) for t in transitions
100+ ],
101+ initial_state = initial ,
102+ accept_states = accept ,
103+ )
104+
105+ if __name__ == "__main__" :
106+ fsa = make_fsa (
107+ states = ["q0" , "q1" ],
108+ alphabet = ["a" ],
109+ transitions = [
110+ {"from_state" : "q0" , "to_state" : "q0" , "symbol" : "a" },
111+ {"from_state" : "q0" , "to_state" : "q1" , "symbol" : "a" }, # Non-deterministic
112+ ],
113+ initial = "q0" ,
114+ accept = ["q1" ],
115+ )
116+ draw_fsa (fsa , False , True , "./valid.png" )
117+ fsa = make_fsa (
118+ states = ["q0" ],
119+ alphabet = ["a" ],
120+ transitions = [],
121+ initial = "q0" ,
122+ accept = ["q1" ], # q1 is not in states
123+ )
124+ draw_fsa (fsa , False , True , "./invalid.png" )
125+
0 commit comments