Coverage for /home/marcofavorito/workfolder/pythomata/pythomata/impl/symbolic.py : 90%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""
3An implementation of a symbolic automaton.
5For further details, see:
6- Applications of Symbolic Finite Automata
7 https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/ciaa13.pdf
8- Symbolic Automata Constraint Solving
9 https://link.springer.com/chapter/10.1007%2F978-3-642-16242-8_45
10- Rex: Symbolic Regular Expression Explorer
11 https://www.microsoft.com/en-us/research/wp-content/uploads/2010/04/rex-ICST.pdf
12"""
13import itertools
14import operator
15from typing import Set, Dict, Union, Any, Optional, FrozenSet, Tuple
17import graphviz
18import sympy
19from sympy import Symbol, simplify, satisfiable, And, Not, Or
20from sympy.logic.boolalg import BooleanFunction, BooleanTrue, BooleanFalse
21from sympy.parsing.sympy_parser import parse_expr
23from pythomata._internal_utils import greatest_fixpoint
24from pythomata.core import FiniteAutomaton, SymbolType
25from pythomata.utils import iter_powerset
27PropInt = Dict[Union[str, Symbol], bool]
30class SymbolicAutomaton(FiniteAutomaton[int, PropInt]):
31 """A symbolic automaton."""
33 def __init__(self):
34 """Initialize a Symbolic automaton."""
35 self._initial_states = set()
36 self._states = set()
37 self._final_states = set() # type: Set[int]
38 self._state_counter = 0
40 self._transition_function = {} # type: Dict[int, Dict[int, BooleanFunction]]
41 self._deterministic = None # type: Optional[bool]
43 @property
44 def states(self) -> Set[int]:
45 """Get the states."""
46 return self._states
48 @property
49 def final_states(self) -> Set[int]:
50 """Get the final states."""
51 return self._final_states
53 @property
54 def initial_states(self) -> Set[int]:
55 """Get the initial states."""
56 return self._initial_states
58 @property
59 def is_deterministic(self) -> Optional[bool]:
60 """Check if the automaton is deterministic.
62 :return True if the automaton is deterministic, False if it is not, and None if we don't know.
63 """
64 return self._deterministic
66 def get_successors(self, state: int, symbol: PropInt) -> Set[int]:
67 """Get the successor states.."""
68 if state not in self.states:
69 raise ValueError("State not in set of states.")
70 if not self._is_valid_symbol(symbol):
71 raise ValueError("Symbol {} is not valid.".format(symbol))
72 successors = set()
73 transition_iterator = self._transition_function.get(state, {}).items()
74 for successor, guard in transition_iterator:
75 subexpr = guard.subs(symbol)
76 subexpr = subexpr.replace(sympy.Symbol, BooleanFalse)
77 if subexpr == True: # noqa: E712
78 successors.add(successor)
79 return successors
81 def create_state(self) -> int:
82 """Create a new state."""
83 new_state = self._state_counter
84 self.states.add(new_state)
85 self._state_counter += 1
86 return new_state
88 def remove_state(self, state: int) -> None:
89 """Remove a state."""
90 if state not in self.states:
91 raise ValueError("State {} not found.".format(state))
93 self._transition_function.pop(state, None)
94 for s in self._transition_function:
95 self._transition_function[s].pop(state, None)
97 def set_final_state(self, state: int, is_final: bool) -> None:
98 """Set a state to be final."""
99 if state not in self.states:
100 raise ValueError("State {} not found.".format(state))
101 if is_final:
102 self.final_states.add(state)
103 else:
104 try:
105 self.final_states.remove(state)
106 except KeyError:
107 pass
109 def set_initial_state(self, state: int, is_initial: bool) -> None:
110 """Set a state to be an initial state."""
111 if state not in self.states:
112 raise ValueError("State {} not found.".format(state))
113 if is_initial:
114 self.initial_states.add(state)
115 else:
116 try:
117 self.initial_states.remove(state)
118 except KeyError:
119 pass
121 def add_transition(self, state1: int, guard: Union[BooleanFunction, str], state2: int) -> None:
122 """
123 Add a transition.
125 :param state1: the start state of the transition.
126 :param guard: the guard of the transition.
127 it can be either a sympy.logic.boolalg.BooleanFunction object
128 or a string that can be parsed with sympy.parsing.sympy_parser.parse_expr.
129 :param state2:
130 :return:
131 """
132 assert state1 in self.states
133 assert state2 in self.states
134 if isinstance(guard, str):
135 guard = simplify(parse_expr(guard))
136 other_guard = self._transition_function.get(state1, {}).get(state2, None)
137 if other_guard is None:
138 self._transition_function.setdefault(state1, {})[state2] = guard
139 else:
140 # take the OR of the two guards.
141 self._transition_function[state1][state2] = simplify(other_guard | guard)
143 self._deterministic = None
145 def _is_valid_symbol(self, symbol: Any) -> bool:
146 """Return true if the given symbol is valid, false otherwise."""
147 try:
148 assert isinstance(symbol, dict)
149 assert all(isinstance(k, str) for k in symbol.keys())
150 assert all(isinstance(v, bool) for v in symbol.values())
151 except AssertionError:
152 return False
153 return True
155 def complete(self) -> 'SymbolicAutomaton':
156 """Complete the automaton."""
157 states = set(self.states)
158 initial_states = self.initial_states
159 final_states = self.final_states
160 transitions = set()
161 sink_state = None
162 for source in states:
163 transitions_from_source = self._transition_function.get(source, {})
164 transitions.update(set(map(lambda x: (source, x[1], x[0]), transitions_from_source.items())))
165 guards = transitions_from_source.values()
166 guards_negation = simplify(Not(Or(*guards)))
167 if satisfiable(guards_negation) is not False:
168 sink_state = len(states) if sink_state is None else sink_state
169 transitions.add((source, guards_negation, sink_state))
171 if sink_state is not None:
172 states.add(sink_state)
173 transitions.add((sink_state, BooleanTrue(), sink_state))
174 return SymbolicAutomaton._from_transitions(states, initial_states, final_states, transitions)
176 def is_complete(self) -> bool:
177 """
178 Check whether the automaton is complete.
180 :return: True if the automaton is complete, False otherwise.
181 """
182 # all the state must have an outgoing transition.
183 if not all(state in self._transition_function.keys() for state in self.states):
184 return False
186 for source in self._transition_function:
187 guards = self._transition_function[source].values()
188 negated_guards = Not(Or(guards))
189 if satisfiable(negated_guards):
190 return False
192 return True
194 def determinize(self) -> 'SymbolicAutomaton':
195 """Do determinize."""
196 if self._deterministic:
197 return self
199 frozen_initial_states = frozenset(self.initial_states) # type: FrozenSet[int]
200 stack = [frozen_initial_states]
201 visited = {frozen_initial_states}
202 final_macro_states = {frozen_initial_states} if frozen_initial_states.intersection(
203 self.final_states) != set() else set() # type: Set[FrozenSet[int]]
204 moves = set()
206 # given an iterable of transitions (i.e. triples (source, guard, destination),
207 # get the guard
208 def getguard(x):
209 return map(operator.itemgetter(1), x)
211 # given ... (as before)
212 # get the target
213 def gettarget(x):
214 return map(operator.itemgetter(2), x)
216 while len(stack) > 0:
217 macro_source = stack.pop()
218 transitions = set([(source, guard, dest)
219 for source in macro_source
220 for dest, guard in self._transition_function.get(source, {}).items()])
221 for transitions_subset in map(frozenset, iter_powerset(transitions)):
222 if len(transitions_subset) == 0:
223 continue
224 transitions_subset_negated = transitions.difference(transitions_subset)
225 phi_positive = And(*getguard(transitions_subset))
226 phi_negative = And(*map(Not, getguard(transitions_subset_negated)))
227 phi = phi_positive & phi_negative
228 if sympy.satisfiable(phi) is not False:
229 macro_dest = frozenset(gettarget(transitions_subset)) # type: FrozenSet[int]
230 moves.add((macro_source, phi, macro_dest))
231 if macro_dest not in visited:
232 visited.add(macro_dest)
233 stack.append(macro_dest)
234 if macro_dest.intersection(self.final_states) != set():
235 final_macro_states.add(macro_dest)
237 return self._from_transitions(visited, {frozen_initial_states}, set(final_macro_states), moves,
238 deterministic=True)
240 def minimize(self) -> FiniteAutomaton[int, PropInt]:
241 """Minimize."""
242 dfa = self.determinize().complete()
243 equivalence_relation = set.union(
244 {(p, q) for p, q in itertools.product(dfa.final_states, repeat=2)},
245 {(p, q) for p, q in itertools.product(dfa.states.difference(dfa.final_states), repeat=2)}
246 )
248 def greatest_fixpoint_condition(el: Tuple[int, int], current_set: Set):
249 """Condition to say whether the pair must be removed from the bisimulation relation."""
250 # unpack the two states
251 s_source, t_source = el
252 for (s_dest, s_guard) in dfa._transition_function.get(s_source, {}).items():
253 for (t_dest, t_guard) in dfa._transition_function.get(t_source, {}).items():
254 if t_dest != s_dest \
255 and (s_dest, t_dest) not in current_set \
256 and satisfiable(And(s_guard, t_guard)) is not False:
257 return True
259 # TODO to improve.
260 result = greatest_fixpoint(equivalence_relation, condition=greatest_fixpoint_condition)
261 state2class = {} # type: Dict[int, FrozenSet[int]]
262 for a, b in result:
263 union = state2class.get(a, {a}).union(state2class.get(b, {b}))
264 for element in union:
265 state2class[element] = union
267 state2class = {k: frozenset(v) for k, v in state2class.items()}
268 equivalence_classes = set(map(lambda x: frozenset(x), state2class.values()))
269 class2newstate = dict((ec, i) for i, ec in enumerate(equivalence_classes))
271 new_states = set(class2newstate.values())
272 old_initial_state = next(iter(dfa.initial_states)) # since "dfa" is determinized, there's just one
273 initial_states = {class2newstate[state2class[old_initial_state]]}
274 final_states = {class2newstate[state2class[final_state]] for final_state in dfa.final_states}
275 transitions = set()
277 for old_source in dfa._transition_function:
278 for old_dest, guard in dfa._transition_function[old_source].items():
279 new_source = class2newstate[state2class[old_source]]
280 new_dest = class2newstate[state2class[old_dest]]
281 transitions.add((new_source, guard, new_dest))
283 return SymbolicAutomaton._from_transitions(
284 new_states,
285 initial_states,
286 final_states,
287 transitions,
288 deterministic=True
289 )
291 @classmethod
292 def _from_transitions(cls, states: Set[Any],
293 initial_states: Set[Any],
294 final_states: Set[Any],
295 transitions: Set[Tuple[Any, SymbolType, Any]],
296 deterministic: Optional[bool] = None):
297 automaton = SymbolicAutomaton()
298 state_to_indices = {}
299 indices_to_state = {}
301 for s in states:
302 new_index = automaton.create_state()
303 automaton.set_initial_state(new_index, s in initial_states)
304 automaton.set_final_state(new_index, s in final_states)
305 state_to_indices[s] = new_index
306 indices_to_state[new_index] = s
308 for (source, guard, destination) in transitions:
309 source_index = state_to_indices[source]
310 dest_index = state_to_indices[destination]
311 automaton.add_transition(source_index, guard, dest_index)
313 automaton._deterministic = deterministic
314 return automaton
316 def to_graphviz(self, title: Optional[str] = None) -> graphviz.Digraph:
317 """Convert to graphviz.Digraph object."""
318 g = graphviz.Digraph(format="svg")
319 g.node("fake", style="invisible")
320 for state in self.states:
321 if state in self.initial_states:
322 if state in self.final_states:
323 g.node(str(state), root="true", shape="doublecircle")
324 else:
325 g.node(str(state), root="true")
326 elif state in self.final_states:
327 g.node(str(state), shape="doublecircle")
328 else:
329 g.node(str(state))
331 for i in self.initial_states:
332 g.edge("fake", str(i), style="bold")
333 for start in self._transition_function:
334 for end, guard in self._transition_function[start].items():
335 g.edge(str(start), str(end), label=str(guard))
337 if title is not None:
338 g.attr(label=title)
339 g.attr(fontsize="20")
341 return g