3
3
from aalpy .learning_algs .deterministic .Apartness import Apartness
4
4
from aalpy .learning_algs .deterministic .ObservationTree import ObservationTree
5
5
from aalpy .oracles .WpMethodEqOracle import state_characterization_set
6
+ from aalpy .base import Automaton , SUL
7
+ from aalpy .automata import Dfa , DfaState , MealyState , MealyMachine , MooreMachine , MooreState
6
8
7
9
8
10
class AdaptiveObservationTree (ObservationTree ):
9
- def __init__ (self , alphabet , sul , references , extension_rule , separation_rule , rebuilding = True , state_matching = "Approximate" ):
11
+ def __init__ (self , alphabet , sul , references , automaton_type , extension_rule , separation_rule , rebuilding = True , state_matching = "Approximate" ):
10
12
"""
11
13
Initialize the tree with a root node and the alphabet
12
14
A temporary new basis is needed for the prioritized promotion rule
13
15
The rebuild states counter counts the number of states found with rebuilding excluding the root
14
16
The matching states counter counts the number of states found with match refinement and match separation (NOT prioritized separation)
15
17
"""
16
- super ().__init__ (alphabet , sul , extension_rule , separation_rule )
18
+ super ().__init__ (alphabet , sul , automaton_type , extension_rule , separation_rule )
17
19
self .references = references
18
20
self .rebuild_states = 0
19
21
self .matching_states = 0
@@ -28,6 +30,9 @@ def __init__(self, alphabet, sul, references, extension_rule, separation_rule, r
28
30
self .prefixes_map = {}
29
31
self .characterization_map = {}
30
32
self .combined_model = self .get_combined_model ()
33
+ if not self .combined_model :
34
+ self .state_matching = None
35
+ return
31
36
32
37
# We keep track of a new basis to ensure maximal overlap between prefixes in the references and the new model
33
38
self .new_basis = [self .root ]
@@ -47,14 +52,22 @@ def __init__(self, alphabet, sul, references, extension_rule, separation_rule, r
47
52
48
53
def build_hypothesis (self ):
49
54
"""
50
- Builds the hypothesis which will be sent to the SUL
55
+ Builds the hypothesis which will be sent to the SUL and checks consistency
51
56
This is either done with or without matching rules
52
57
"""
53
- if self .state_matching :
54
- self .make_observation_tree_adequate_matching ()
55
- else :
56
- super ().make_observation_tree_adequate ()
57
- return self .construct_hypothesis ()
58
+ while True :
59
+ if self .state_matching :
60
+ self .make_observation_tree_adequate_matching ()
61
+ else :
62
+ super ().make_observation_tree_adequate ()
63
+ hypothesis = self .construct_hypothesis ()
64
+ counter_example = Apartness .compute_witness_in_tree_and_hypothesis_states (self , self .root , hypothesis .initial_state )
65
+
66
+ if not counter_example :
67
+ return hypothesis
68
+
69
+ cex_outputs = self .get_observation (counter_example )
70
+ self .process_counter_example (hypothesis , counter_example , cex_outputs )
58
71
59
72
def make_observation_tree_adequate_matching (self ):
60
73
"""
@@ -111,7 +124,7 @@ def identify_frontier_with_matching(self, frontier_state):
111
124
if not match :
112
125
return
113
126
114
- if match [0 ].output_fun [ inp ] != 'epsilon' :
127
+ if inp in match [0 ].transitions :
115
128
frontier_match = match [0 ].transitions [inp ]
116
129
identifiers = self .characterization_map [frontier_match ]
117
130
self .identify_frontier_with_identifiers (frontier_state , identifiers )
@@ -156,6 +169,31 @@ def match_refinement(self):
156
169
self .refine_matches_basis (basis_state , matches )
157
170
self .update_frontier_and_basis ()
158
171
172
+ def find_distinguishing_seq_partial (self , model , state1 , state2 , alphabet ):
173
+ """
174
+ A BFS to determine an input sequence that distinguishes two states in the automaton
175
+ Can handle partial models
176
+ """
177
+ visited = set ()
178
+ to_explore = [(state1 , state2 , [])]
179
+ while to_explore :
180
+ (curr_s1 , curr_s2 , prefix ) = to_explore .pop (0 )
181
+ visited .add ((curr_s1 , curr_s2 ))
182
+ for i in alphabet :
183
+ if i in curr_s1 .transitions and i in curr_s2 .transitions :
184
+ o1 = model .output_step (curr_s1 , i )
185
+ o2 = model .output_step (curr_s2 , i )
186
+ new_prefix = prefix + [i ]
187
+ if o1 != o2 :
188
+ return new_prefix
189
+ else :
190
+ next_s1 = curr_s1 .transitions [i ]
191
+ next_s2 = curr_s2 .transitions [i ]
192
+ if (next_s1 , next_s2 ) not in visited :
193
+ to_explore .append ((next_s1 , next_s2 , new_prefix ))
194
+
195
+ return None
196
+
159
197
def refine_matches_basis (self , basis_state , matches ):
160
198
"""
161
199
Loops over the matched reference states and separates them using a separating sequence
@@ -172,7 +210,7 @@ def refine_matches_basis(self, basis_state, matches):
172
210
if ref_state_two not in current_matches :
173
211
continue
174
212
175
- witness = self .combined_model . find_distinguishing_seq (
213
+ witness = self .find_distinguishing_seq_partial ( self . combined_model ,
176
214
ref_state_one , ref_state_two , self .alphabet )
177
215
if witness is None :
178
216
continue
@@ -217,7 +255,7 @@ def match_separation_frontier(self, matched_states, frontier_state, basis_candid
217
255
parent_basis = frontier_state .parent
218
256
inp = frontier_state .input_to_parent
219
257
for match in self .state_matcher .best_match [parent_basis ]:
220
- if match .transitions [inp ] in matched_states :
258
+ if ( inp in match .transitions and match . transitions [inp ] in matched_states ) or ( inp not in match . transitions ) :
221
259
continue
222
260
223
261
frontier_match = match .transitions [inp ]
@@ -367,11 +405,11 @@ def find_basis_frontier_pair(self, frontier_state, frontier_state_access):
367
405
reference .initial_state , basis_state_access )
368
406
state_two = reference .current_state
369
407
370
- sep_seq = tuple ( reference . find_distinguishing_seq (
371
- state_one , state_two , reference . get_input_alphabet ()) )
372
- if sep_seq and (self .get_successor (frontier_state_access + sep_seq ) is None or
373
- self .get_successor (basis_state_access + sep_seq ) is None ):
374
- return basis_state_access , frontier_state_access , sep_seq
408
+ sep_seq = self . find_distinguishing_seq_partial ( reference ,
409
+ state_one , state_two , self . alphabet )
410
+ if sep_seq and (self .get_successor (frontier_state_access + tuple ( sep_seq ) ) is None or
411
+ self .get_successor (basis_state_access + tuple ( sep_seq ) ) is None ):
412
+ return basis_state_access , frontier_state_access , tuple ( sep_seq )
375
413
return None
376
414
377
415
def insert_observation_rebuilding (self , inputs , outputs ):
@@ -402,22 +440,26 @@ def apart_from_all(self, frontier_state):
402
440
return True
403
441
404
442
# Functions related to finding the combined model
443
+
405
444
def add_ref_transitions_to_states (self , reference , reference_id ):
406
445
"""
407
446
Makes a copy of the states of a reference with a unique state id and only transitions with the new input alphabet
408
447
"""
409
- states = [MealyState (f"s({ reference_id } ,{ ref_state } )" )
448
+ automaton_state = {'dfa' : DfaState , 'mealy' : MealyState , 'moore' : MooreState }
449
+ states = [automaton_state [self .automaton_type ](f"s({ reference_id } ,{ ref_state } )" )
410
450
for ref_state in range (0 , len (reference .states ))]
411
451
for state_id in range (0 , len (reference .states )):
412
- states [state_id ].output_fun = reference .states [state_id ].output_fun
452
+ if self .automaton_type == 'mealy' :
453
+ states [state_id ].output_fun = reference .states [state_id ].output_fun
454
+ elif self .automaton_type == 'dfa' :
455
+ states [state_id ].is_accepting = reference .states [state_id ].is_accepting
456
+ else :
457
+ states [state_id ].output = reference .states [state_id ].output
413
458
for inp in self .alphabet :
414
459
if inp in reference .get_input_alphabet ():
415
460
old_index = reference .states .index (
416
461
reference .states [state_id ].transitions [inp ])
417
462
states [state_id ].transitions [inp ] = states [old_index ]
418
- else :
419
- states [state_id ].transitions [inp ] = states [state_id ]
420
- states [state_id ].output_fun [inp ] = 'epsilon'
421
463
return states
422
464
423
465
def compute_prefix_map (self , reference , reference_id ):
@@ -433,19 +475,18 @@ def compute_characterization_map(self, reference, states):
433
475
"""
434
476
Computes the separating sequences of a reference model and stores them in a characterization map
435
477
"""
436
-
437
478
for state , ref_state in zip (states , reference .states ):
438
479
all_sepseqs = state_characterization_set (reference , reference .get_input_alphabet (), ref_state )
439
480
unique_sepseqs = list (dict .fromkeys (all_sepseqs ))
440
481
self .characterization_map [state ] = unique_sepseqs
441
482
442
-
443
483
def get_combined_model (self ):
444
484
"""
445
485
Builds a combined model from the reference models
446
486
Compute the prefix and characterization maps used during construction of the combined model
447
- The resulting mealy machine is made input complete by adding self-loops with output 'epsilon' for all undefined inputs
487
+ The resulting mealy machine may be partial
448
488
"""
489
+ automaton_class = {'dfa' : Dfa , 'mealy' : MealyMachine , 'moore' : MooreMachine }
449
490
all_states = []
450
491
for reference_id in range (0 , len (self .references )):
451
492
reference = self .references [reference_id ]
@@ -460,9 +501,11 @@ def get_combined_model(self):
460
501
states = self .add_ref_transitions_to_states (reference , reference_id )
461
502
all_states += states
462
503
463
- self .compute_prefix_map (MealyMachine (states [0 ], states ), reference_id )
504
+ self .compute_prefix_map (automaton_class [ self . automaton_type ] (states [0 ], states ), reference_id )
464
505
self .compute_characterization_map (reference , states )
465
506
466
- mm = MealyMachine (all_states [0 ], all_states )
467
- mm .make_input_complete ()
468
- return mm
507
+ if all_states == []:
508
+ print (f"Warning: the references did not lead to any usable states, this could be due to empty models or no common inputs." )
509
+ return None
510
+ else :
511
+ return automaton_class [self .automaton_type ](all_states [0 ], all_states )
0 commit comments