Skip to content

Commit 0a95a23

Browse files
author
Edi Muškardin
authored
Merge pull request #93 from taburg/rpni-mealy-fix
Fix regression in classic RPNI with Mealy machines
2 parents 1b16477 + 6b1aec1 commit 0a95a23

File tree

6 files changed

+129
-34
lines changed

6 files changed

+129
-34
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
digraph simpleABCDfa {
2+
s0 [label="s0"];
3+
s1 [label="s1", shape=doublecircle];
4+
s0 -> s1 [label="a"];
5+
s0 -> s0 [label="b"];
6+
s0 -> s0 [label="c"];
7+
s1 -> s1 [label="a"];
8+
s1 -> s0 [label="b"];
9+
s1 -> s0 [label="c"];
10+
__start0 [shape=none, label=""];
11+
__start0 -> s0 [label=""];
12+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
digraph simpleABCmealy{
2+
s0 [label="s0"];
3+
s0 -> s0 [label="a/1"];
4+
s0 -> s0 [label="b/2"];
5+
s0 -> s0 [label="c/3"];
6+
__start0 [shape=none, label=""];
7+
__start0 -> s0 [label=""];
8+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
digraph simpleABCmoore{
2+
s0 [label="s0|0", shape=record, style=rounded];
3+
s1 [label="s1|1", shape=record, style=rounded];
4+
s2 [label="s2|2", shape=record, style=rounded];
5+
s3 [label="s3|3", shape=record, style=rounded];
6+
s0 -> s1 [label="a"];
7+
s0 -> s2 [label="b"];
8+
s0 -> s3 [label="c"];
9+
s1 -> s1 [label="a"];
10+
s1 -> s2 [label="b"];
11+
s1 -> s3 [label="c"];
12+
s2 -> s1 [label="a"];
13+
s2 -> s2 [label="b"];
14+
s2 -> s3 [label="c"];
15+
s3 -> s1 [label="a"];
16+
s3 -> s2 [label="b"];
17+
s3 -> s3 [label="c"];
18+
__start0 [shape=none, label=""];
19+
__start0 -> s0 [label=""];
20+
}
21+

aalpy/learning_algs/deterministic_passive/ClassicRPNI.py

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def __init__(self, data, automaton_type, print_info=True):
1212

1313
pta_construction_start = time.time()
1414
self.root_node = createPTA(data, automaton_type)
15-
self.test_data = extract_unique_sequences(self.root_node)
15+
self.test_data = extract_unique_sequences(self.root_node, automaton_type)
1616

1717
if self.print_info:
1818
print(f'PTA Construction Time: {round(time.time() - pta_construction_start, 2)}')
@@ -27,7 +27,7 @@ def run_rpni(self):
2727
merged = False
2828

2929
for red_state in red:
30-
if not self._compatible_states(red_state, lex_min_blue):
30+
if not red_state.compatible_outputs(lex_min_blue):
3131
continue
3232
merge_candidate = self._merge(red_state, lex_min_blue, copy_nodes=True)
3333
if self._compatible(merge_candidate):
@@ -62,21 +62,6 @@ def _compatible(self, root_node):
6262
return False
6363
return True
6464

65-
def _compatible_states(self, red_node, blue_node):
66-
"""
67-
Only allow merging of states that have same output(s).
68-
"""
69-
if self.automaton_type != 'mealy':
70-
# None is compatible with everything
71-
return red_node.output == blue_node.output or red_node.output is None or blue_node.output is None
72-
else:
73-
red_io = {i: o for i, o in red_node.children.keys()}
74-
blue_io = {i: o for i, o in blue_node.children.keys()}
75-
for common_i in set(red_io.keys()).intersection(blue_io.keys()):
76-
if red_io[common_i] != blue_io[common_i]:
77-
return False
78-
return True
79-
8065
def _merge(self, red_node, lex_min_blue, copy_nodes=False):
8166
"""
8267
Merge two states and return the root node of resulting model.
@@ -112,18 +97,12 @@ def _fold(self, red_node, blue_node):
11297
red_node.children[i] = blue_node.children[i]
11398

11499
def _fold_mealy(self, red_node, blue_node):
115-
blue_io_map = {i: o for i, o in blue_node.children.keys()}
100+
for i, o in blue_node.output.items():
101+
red_node.output[i] = o
116102

117-
updated_keys = {}
118-
for io, val in red_node.children.items():
119-
o = blue_io_map[io[0]] if io[0] in blue_io_map.keys() else io[1]
120-
updated_keys[(io[0], o)] = val
121-
122-
red_node.children = updated_keys
123-
124-
for io in blue_node.children.keys():
125-
if io in red_node.children.keys():
126-
self._fold_mealy(red_node.children[io], blue_node.children[io])
103+
for i in blue_node.children.keys():
104+
if i in red_node.children.keys():
105+
self._fold_mealy(red_node.children[i], blue_node.children[i])
127106
else:
128-
red_node.children[io] = blue_node.children[io]
107+
red_node.children[i] = blue_node.children[i]
129108

aalpy/learning_algs/deterministic_passive/rpni_helper_functions.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ def __hash__(self):
3434
return id(self) # TODO This is a hack
3535

3636
def compatible_outputs(self, other):
37+
"""
38+
Only allow merging of states that have same output(s).
39+
"""
3740
# None is compatible with everything
3841
if self.type != 'mealy':
3942
return self.output == other.output or self.output is None or other.output is None
@@ -59,10 +62,9 @@ def check_sequence(root_node, seq, automaton_type):
5962
curr_node = root_node
6063
for i, o in seq:
6164
if automaton_type == 'mealy':
62-
input_outputs = {i: o for i, o in curr_node.children.keys()}
63-
if i[0] not in input_outputs.keys() or o is not None and input_outputs[i[0]] != o:
65+
if i not in curr_node.output or o is not None and curr_node.output[i] != o:
6466
return False
65-
curr_node = curr_node.children[(i[0], input_outputs[i[0]])]
67+
curr_node = curr_node.children[i]
6668
else:
6769
# For dfa and moore, check if outputs are the same, iff output in test data is concrete (not None)
6870
curr_node = curr_node.children[i]
@@ -98,7 +100,7 @@ def createPTA(data, automaton_type):
98100
return root_node
99101

100102

101-
def extract_unique_sequences(root_node):
103+
def extract_unique_sequences(root_node, automaton_type):
102104
def get_leaf_nodes(root):
103105
leaves = []
104106

@@ -119,7 +121,10 @@ def _get_leaf_nodes(node):
119121
curr_node = root_node
120122
for i in node.prefix:
121123
curr_node = curr_node.children[i]
122-
seq.append((i, curr_node.output))
124+
if automaton_type == 'mealy':
125+
seq.append((i, curr_node.output.get(i)))
126+
else:
127+
seq.append((i, curr_node.output))
123128
paths.append(seq)
124129

125130
return paths
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import unittest
2+
from itertools import product
3+
4+
import aalpy
5+
from aalpy.automata import Dfa, MooreMachine, MealyMachine
6+
from aalpy.learning_algs import run_RPNI
7+
from aalpy.utils import load_automaton_from_file
8+
# from aalpy.utils.ModelChecking import bisimilar
9+
from aalpy.utils.ModelChecking import compare_automata
10+
11+
correct_automata = {Dfa: load_automaton_from_file('../DotModels/SimpleABC/simple_abc_dfa.dot', automaton_type='dfa'),
12+
MooreMachine: load_automaton_from_file('../DotModels/SimpleABC/simple_abc_moore.dot', automaton_type='moore'),
13+
MealyMachine: load_automaton_from_file('../DotModels/SimpleABC/simple_abc_mealy.dot', automaton_type='mealy')}
14+
15+
16+
class DeterministicPassiveTest(unittest.TestCase):
17+
18+
def prove_equivalence(self, learned_automaton):
19+
20+
correct_automaton = correct_automata[learned_automaton.__class__]
21+
22+
# only work if correct automaton is already minimal
23+
if len(learned_automaton.states) != len(correct_automaton.states):
24+
print(len(learned_automaton.states), len(correct_automaton.states))
25+
return False
26+
27+
return correct_automaton == learned_automaton # bisimilar
28+
29+
def generate_data(self, ground_truth, depth=5):
30+
data = []
31+
if isinstance(ground_truth, aalpy.automata.Dfa) or isinstance(ground_truth, aalpy.automata.MooreMachine):
32+
data.append(((), ground_truth.initial_state.output))
33+
34+
alphabet = ground_truth.get_input_alphabet()
35+
for level in range(1, depth + 1):
36+
for seq in product(alphabet, repeat=level):
37+
ground_truth.reset_to_initial()
38+
outputs = ground_truth.execute_sequence(ground_truth.initial_state, seq)
39+
data.append((seq, outputs[-1]))
40+
41+
return data
42+
43+
# TODO incomplete: skips input completeness options
44+
def test_all_configuration_combinations(self):
45+
46+
47+
automata_type = {Dfa: 'dfa', MooreMachine: 'moore', MealyMachine: 'mealy'}
48+
algorithms = ['gsm', 'classic']
49+
50+
for automata in correct_automata:
51+
correct_automaton = correct_automata[automata]
52+
alphabet = correct_automaton.get_input_alphabet()
53+
data = self.generate_data(correct_automaton, depth=3)
54+
for algorithm in algorithms:
55+
learned_model = run_RPNI(data,
56+
automaton_type=automata_type[automata],
57+
algorithm=algorithm,
58+
print_info=False)
59+
60+
is_eq = self.prove_equivalence(learned_model)
61+
if not is_eq:
62+
print("Learned:")
63+
print(learned_model)
64+
print(algorithm, automata_type[automata])
65+
cex = compare_automata(learned_model, correct_automaton)
66+
print(cex)
67+
assert False
68+
69+
assert True
70+

0 commit comments

Comments
 (0)