Skip to content

Commit 99b2ef9

Browse files
authored
Merge pull request #82 from lkruger27/dfa-moore
Add DFA and Moore functionality for L# and adaptive L#
2 parents f167627 + 2ffe543 commit 99b2ef9

File tree

9 files changed

+510
-149
lines changed

9 files changed

+510
-149
lines changed

Examples.py

+25-2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,29 @@ def angluin_seminal_example():
4545
assert learned_dfa == dfa
4646
return learned_dfa
4747

48+
def angluin_seminal_example_lsharp():
49+
"""
50+
Example automaton from Angluin's seminal paper.
51+
:return: learned DFA
52+
"""
53+
from aalpy.SULs import AutomatonSUL
54+
from aalpy.oracles import RandomWalkEqOracle
55+
from aalpy.learning_algs import run_Lstar, run_Lsharp
56+
from aalpy.utils import get_Angluin_dfa
57+
58+
dfa = get_Angluin_dfa()
59+
60+
alphabet = dfa.get_input_alphabet()
61+
62+
sul = AutomatonSUL(dfa)
63+
eq_oracle = RandomWalkEqOracle(alphabet, sul, 500)
64+
65+
learned_dfa = run_Lsharp(alphabet, sul, eq_oracle, automaton_type='dfa',
66+
extension_rule="SepSeq", separation_rule="ADS", max_learning_rounds=50, print_level=3)
67+
68+
assert learned_dfa == dfa
69+
return learned_dfa
70+
4871

4972
def tomita_example(tomita_number=3):
5073
"""
@@ -157,7 +180,7 @@ def bluetooth_Lsharp():
157180
# Extension rule options: {"Nothing", "SepSeq", "ADS"}
158181
# Separation rule options: {"SepSeq", "ADS"}
159182
learned_mealy = run_Lsharp(input_alphabet, sul_mealy, eq_oracle, automaton_type='mealy', extension_rule=None,
160-
separation_rule="SepSeq", max_learning_rounds=50, print_level=1)
183+
separation_rule="SepSeq", max_learning_rounds=50, print_level=3)
161184

162185

163186
def bluetooth_adaptive_Lsharp():
@@ -1300,4 +1323,4 @@ def ioa_compat_domain_knowledge(a: GsmNode, b: GsmNode):
13001323
for name, score in scores.items():
13011324
learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="stochastic", score_calc=score,
13021325
compatibility_on_pta=True, compatibility_on_futures=True)
1303-
learned_model.visualize(name)
1326+
learned_model.visualize(name)

aalpy/base/SUL.py

+8-7
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ def adaptive_query(self, word, ads):
5959
6060
list of outputs, where the i-th output corresponds to the output of the system after the i-th input
6161
"""
62-
6362
self.pre()
6463

6564
outputs_received = []
@@ -75,12 +74,14 @@ def adaptive_query(self, word, ads):
7574
next_input = ads.next_input(last_output)
7675
if next_input is None:
7776
break
78-
79-
word.append(next_input)
80-
output = self.step(next_input)
81-
outputs_received.append(output)
82-
last_output = output
83-
self.num_steps += 1
77+
if next_input is tuple(): # Relevant for DFA/Moore
78+
last_output = self.step(None)
79+
else:
80+
word.append(next_input)
81+
output = self.step(next_input)
82+
outputs_received.append(output)
83+
last_output = output
84+
self.num_steps += 1
8485

8586
self.num_queries += 1
8687
self.post()

aalpy/learning_algs/adaptive/AdaptiveLSharp.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ...base.SUL import CacheSUL
77

88

9-
def run_adaptive_Lsharp(alphabet: list, sul: SUL, references: list, eq_oracle: Oracle, automaton_type='mealy',
9+
def run_adaptive_Lsharp(alphabet: list, sul: SUL, references: list, eq_oracle: Oracle, automaton_type,
1010
extension_rule=None, separation_rule="SepSeq",
1111
rebuilding=True, state_matching="Approximate",
1212
samples=None, max_learning_rounds=None,
@@ -27,7 +27,7 @@ def run_adaptive_Lsharp(alphabet: list, sul: SUL, references: list, eq_oracle: O
2727
2828
eq_oracle: equivalence oracle
2929
30-
automaton_type: currently only 'mealy' is accepted
30+
automaton_type: type of automaton to be learned. Either 'dfa', 'mealy' or 'moore'
3131
3232
extension_rule: strategy used during the extension rule. Options: "Nothing" (default), "SepSeq" and "ADS".
3333
@@ -61,7 +61,6 @@ def run_adaptive_Lsharp(alphabet: list, sul: SUL, references: list, eq_oracle: O
6161
automaton of type automaton_type (dict containing all information about learning if 'return_data' is True)
6262
6363
"""
64-
assert automaton_type == "mealy"
6564
assert extension_rule in {None, "SepSeq", "ADS"}
6665
assert separation_rule in {"SepSeq", "ADS"}
6766

@@ -80,7 +79,7 @@ def run_adaptive_Lsharp(alphabet: list, sul: SUL, references: list, eq_oracle: O
8079
for input_seq, output_seq in samples:
8180
sul.cache.add_to_cache(input_seq, output_seq)
8281

83-
ob_tree = AdaptiveObservationTree(alphabet, sul, references,
82+
ob_tree = AdaptiveObservationTree(alphabet, sul, references, automaton_type,
8483
extension_rule, separation_rule,
8584
rebuilding, state_matching)
8685
start_time = time.time()
@@ -100,7 +99,7 @@ def run_adaptive_Lsharp(alphabet: list, sul: SUL, references: list, eq_oracle: O
10099
print(f'Hypothesis {learning_rounds}: {hypothesis.size} states.')
101100
if print_level == 3:
102101
print(hypothesis)
103-
if state_matching != "None":
102+
if ob_tree.state_matching:
104103
ob_tree.state_matcher.print_match_table(ob_tree)
105104

106105
# Pose Equivalence Query

aalpy/learning_algs/adaptive/AdaptiveObservationTree.py

+71-28
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
from aalpy.learning_algs.deterministic.Apartness import Apartness
44
from aalpy.learning_algs.deterministic.ObservationTree import ObservationTree
55
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
68

79

810
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"):
1012
"""
1113
Initialize the tree with a root node and the alphabet
1214
A temporary new basis is needed for the prioritized promotion rule
1315
The rebuild states counter counts the number of states found with rebuilding excluding the root
1416
The matching states counter counts the number of states found with match refinement and match separation (NOT prioritized separation)
1517
"""
16-
super().__init__(alphabet, sul, extension_rule, separation_rule)
18+
super().__init__(alphabet, sul, automaton_type, extension_rule, separation_rule)
1719
self.references = references
1820
self.rebuild_states = 0
1921
self.matching_states = 0
@@ -28,6 +30,9 @@ def __init__(self, alphabet, sul, references, extension_rule, separation_rule, r
2830
self.prefixes_map = {}
2931
self.characterization_map = {}
3032
self.combined_model = self.get_combined_model()
33+
if not self.combined_model:
34+
self.state_matching = None
35+
return
3136

3237
# We keep track of a new basis to ensure maximal overlap between prefixes in the references and the new model
3338
self.new_basis = [self.root]
@@ -47,14 +52,22 @@ def __init__(self, alphabet, sul, references, extension_rule, separation_rule, r
4752

4853
def build_hypothesis(self):
4954
"""
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
5156
This is either done with or without matching rules
5257
"""
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)
5871

5972
def make_observation_tree_adequate_matching(self):
6073
"""
@@ -111,7 +124,7 @@ def identify_frontier_with_matching(self, frontier_state):
111124
if not match:
112125
return
113126

114-
if match[0].output_fun[inp] != 'epsilon':
127+
if inp in match[0].transitions:
115128
frontier_match = match[0].transitions[inp]
116129
identifiers = self.characterization_map[frontier_match]
117130
self.identify_frontier_with_identifiers(frontier_state, identifiers)
@@ -156,6 +169,31 @@ def match_refinement(self):
156169
self.refine_matches_basis(basis_state, matches)
157170
self.update_frontier_and_basis()
158171

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+
159197
def refine_matches_basis(self, basis_state, matches):
160198
"""
161199
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):
172210
if ref_state_two not in current_matches:
173211
continue
174212

175-
witness = self.combined_model.find_distinguishing_seq(
213+
witness = self.find_distinguishing_seq_partial(self.combined_model,
176214
ref_state_one, ref_state_two, self.alphabet)
177215
if witness is None:
178216
continue
@@ -217,7 +255,7 @@ def match_separation_frontier(self, matched_states, frontier_state, basis_candid
217255
parent_basis = frontier_state.parent
218256
inp = frontier_state.input_to_parent
219257
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):
221259
continue
222260

223261
frontier_match = match.transitions[inp]
@@ -367,11 +405,11 @@ def find_basis_frontier_pair(self, frontier_state, frontier_state_access):
367405
reference.initial_state, basis_state_access)
368406
state_two = reference.current_state
369407

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)
375413
return None
376414

377415
def insert_observation_rebuilding(self, inputs, outputs):
@@ -402,22 +440,26 @@ def apart_from_all(self, frontier_state):
402440
return True
403441

404442
# Functions related to finding the combined model
443+
405444
def add_ref_transitions_to_states(self, reference, reference_id):
406445
"""
407446
Makes a copy of the states of a reference with a unique state id and only transitions with the new input alphabet
408447
"""
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})")
410450
for ref_state in range(0, len(reference.states))]
411451
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
413458
for inp in self.alphabet:
414459
if inp in reference.get_input_alphabet():
415460
old_index = reference.states.index(
416461
reference.states[state_id].transitions[inp])
417462
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'
421463
return states
422464

423465
def compute_prefix_map(self, reference, reference_id):
@@ -433,19 +475,18 @@ def compute_characterization_map(self, reference, states):
433475
"""
434476
Computes the separating sequences of a reference model and stores them in a characterization map
435477
"""
436-
437478
for state, ref_state in zip(states, reference.states):
438479
all_sepseqs = state_characterization_set(reference, reference.get_input_alphabet(), ref_state)
439480
unique_sepseqs = list(dict.fromkeys(all_sepseqs))
440481
self.characterization_map[state] = unique_sepseqs
441482

442-
443483
def get_combined_model(self):
444484
"""
445485
Builds a combined model from the reference models
446486
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
448488
"""
489+
automaton_class = {'dfa': Dfa, 'mealy': MealyMachine, 'moore': MooreMachine}
449490
all_states = []
450491
for reference_id in range(0, len(self.references)):
451492
reference = self.references[reference_id]
@@ -460,9 +501,11 @@ def get_combined_model(self):
460501
states = self.add_ref_transitions_to_states(reference, reference_id)
461502
all_states += states
462503

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)
464505
self.compute_characterization_map(reference, states)
465506

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

Comments
 (0)