Hide keyboard shortcuts

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. 

4 

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 

16 

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 

22 

23from pythomata._internal_utils import greatest_fixpoint 

24from pythomata.core import FiniteAutomaton, SymbolType 

25from pythomata.utils import iter_powerset 

26 

27PropInt = Dict[Union[str, Symbol], bool] 

28 

29 

30class SymbolicAutomaton(FiniteAutomaton[int, PropInt]): 

31 """A symbolic automaton.""" 

32 

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 

39 

40 self._transition_function = {} # type: Dict[int, Dict[int, BooleanFunction]] 

41 self._deterministic = None # type: Optional[bool] 

42 

43 @property 

44 def states(self) -> Set[int]: 

45 """Get the states.""" 

46 return self._states 

47 

48 @property 

49 def final_states(self) -> Set[int]: 

50 """Get the final states.""" 

51 return self._final_states 

52 

53 @property 

54 def initial_states(self) -> Set[int]: 

55 """Get the initial states.""" 

56 return self._initial_states 

57 

58 @property 

59 def is_deterministic(self) -> Optional[bool]: 

60 """Check if the automaton is deterministic. 

61 

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 

65 

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 

80 

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 

87 

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

92 

93 self._transition_function.pop(state, None) 

94 for s in self._transition_function: 

95 self._transition_function[s].pop(state, None) 

96 

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 

108 

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 

120 

121 def add_transition(self, state1: int, guard: Union[BooleanFunction, str], state2: int) -> None: 

122 """ 

123 Add a transition. 

124 

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) 

142 

143 self._deterministic = None 

144 

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 

154 

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

170 

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) 

175 

176 def is_complete(self) -> bool: 

177 """ 

178 Check whether the automaton is complete. 

179 

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 

185 

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 

191 

192 return True 

193 

194 def determinize(self) -> 'SymbolicAutomaton': 

195 """Do determinize.""" 

196 if self._deterministic: 

197 return self 

198 

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

205 

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) 

210 

211 # given ... (as before) 

212 # get the target 

213 def gettarget(x): 

214 return map(operator.itemgetter(2), x) 

215 

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) 

236 

237 return self._from_transitions(visited, {frozen_initial_states}, set(final_macro_states), moves, 

238 deterministic=True) 

239 

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 ) 

247 

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 

258 

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 

266 

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

270 

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

276 

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

282 

283 return SymbolicAutomaton._from_transitions( 

284 new_states, 

285 initial_states, 

286 final_states, 

287 transitions, 

288 deterministic=True 

289 ) 

290 

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 = {} 

300 

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 

307 

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) 

312 

313 automaton._deterministic = deterministic 

314 return automaton 

315 

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

330 

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

336 

337 if title is not None: 

338 g.attr(label=title) 

339 g.attr(fontsize="20") 

340 

341 return g