Skip to content

Commit 4ab6300

Browse files
authored
Merge pull request #75 from DES-Lab/gsm-dev
Merge GSM to master
2 parents 4ae9d6f + 3231b52 commit 4ab6300

File tree

15 files changed

+1478
-155
lines changed

15 files changed

+1478
-155
lines changed

Examples.py

Lines changed: 147 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,15 +1140,12 @@ def passive_vpa_learning_arithmetics():
11401140

11411141
def passive_vpa_learning_on_all_benchmark_models():
11421142
from aalpy.learning_algs import run_PAPNI
1143-
from aalpy.utils.BenchmarkVpaModels import get_all_VPAs
1143+
from aalpy.utils.BenchmarkVpaModels import vpa_L1, vpa_L12, vpa_for_odd_parentheses
11441144
from aalpy.utils import generate_input_output_data_from_vpa, convert_i_o_traces_for_RPNI
11451145

1146-
for gt in get_all_VPAs():
1147-
1146+
for gt in [vpa_L1(), vpa_L12(), vpa_for_odd_parentheses()]:
11481147
vpa_alphabet = gt.input_alphabet
1149-
data = generate_input_output_data_from_vpa(gt, num_sequances=2000, min_seq_len=1, max_seq_len=16)
1150-
1151-
data = convert_i_o_traces_for_RPNI(data)
1148+
data = generate_input_output_data_from_vpa(gt, num_sequances=2000, max_seq_len=16)
11521149

11531150
papni = run_PAPNI(data, vpa_alphabet, algorithm='gsm', print_info=True)
11541151

@@ -1160,3 +1157,147 @@ def passive_vpa_learning_on_all_benchmark_models():
11601157
assert False, 'Papni Learned Model not consistent with data.'
11611158

11621159
print('PAPNI model conforms to data.')
1160+
1161+
1162+
def gsm_rpni():
1163+
from aalpy import load_automaton_from_file
1164+
from aalpy.utils.Sampling import get_io_traces, sample_with_length_limits
1165+
from aalpy.learning_algs.general_passive.GeneralizedStateMerging import run_GSM
1166+
1167+
automaton = load_automaton_from_file("DotModels/car_alarm.dot", "moore")
1168+
input_traces = sample_with_length_limits(automaton.get_input_alphabet(), 100, 20, 30)
1169+
traces = get_io_traces(automaton, input_traces)
1170+
1171+
learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="deterministic")
1172+
learned_model.visualize()
1173+
1174+
1175+
def gsm_edsm():
1176+
from typing import Dict
1177+
from aalpy import load_automaton_from_file
1178+
from aalpy.utils.Sampling import get_io_traces, sample_with_length_limits
1179+
from aalpy.learning_algs.general_passive.GeneralizedStateMerging import run_GSM
1180+
from aalpy.learning_algs.general_passive.ScoreFunctionsGSM import ScoreCalculation
1181+
from aalpy.learning_algs.general_passive.Node import Node
1182+
1183+
automaton = load_automaton_from_file("DotModels/car_alarm.dot", "moore")
1184+
input_traces = sample_with_length_limits(automaton.get_input_alphabet(), 100, 20, 30)
1185+
traces = get_io_traces(automaton, input_traces)
1186+
1187+
def EDSM_score(part: Dict[Node, Node]):
1188+
nr_partitions = len(set(part.values()))
1189+
nr_merged = len(part)
1190+
return nr_merged - nr_partitions
1191+
1192+
score = ScoreCalculation(score_function=EDSM_score)
1193+
learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="deterministic", score_calc=score)
1194+
learned_model.visualize()
1195+
1196+
1197+
def gsm_likelihood_ratio():
1198+
from typing import Dict
1199+
from scipy.stats import chi2
1200+
from aalpy.learning_algs.general_passive.GeneralizedStateMerging import run_GSM
1201+
from aalpy.learning_algs.general_passive.ScoreFunctionsGSM import ScoreFunction, differential_info, ScoreCalculation
1202+
from aalpy.learning_algs.general_passive.Node import Node
1203+
from aalpy.utils.Sampling import get_io_traces, sample_with_length_limits
1204+
from aalpy import load_automaton_from_file
1205+
1206+
automaton = load_automaton_from_file("DotModels/MDPs/faulty_car_alarm.dot", "mdp")
1207+
input_traces = sample_with_length_limits(automaton.get_input_alphabet(), 2000, 20, 30)
1208+
traces = get_io_traces(automaton, input_traces)
1209+
1210+
def likelihood_ratio_score(alpha=0.05) -> ScoreFunction:
1211+
if not 0 < alpha <= 1:
1212+
raise ValueError(f"Confidence {alpha} not between 0 and 1")
1213+
1214+
def score_fun(part: Dict[Node, Node]):
1215+
llh_diff, param_diff = differential_info(part)
1216+
if param_diff == 0:
1217+
# This should cover the corner case when the partition merges only states with no outgoing transitions.
1218+
return -1 # Let them be very bad merges.
1219+
score = 1 - chi2.cdf(2 * llh_diff, param_diff)
1220+
if score < alpha:
1221+
return False
1222+
return score
1223+
1224+
return score_fun
1225+
1226+
score = ScoreCalculation(score_function=likelihood_ratio_score())
1227+
learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="stochastic", score_calc=score)
1228+
learned_model.visualize()
1229+
1230+
1231+
def gsm_IOAlergia_EDSM():
1232+
from aalpy.learning_algs.general_passive.GeneralizedStateMerging import run_GSM
1233+
from aalpy.learning_algs.general_passive.ScoreFunctionsGSM import hoeffding_compatibility, ScoreCalculation
1234+
from aalpy.learning_algs.general_passive.Node import Node
1235+
from aalpy.utils.Sampling import get_io_traces, sample_with_length_limits
1236+
from aalpy import load_automaton_from_file
1237+
1238+
automaton = load_automaton_from_file("DotModels/MDPs/faulty_car_alarm.dot", "mdp")
1239+
input_traces = sample_with_length_limits(automaton.get_input_alphabet(), 2000, 20, 30)
1240+
traces = get_io_traces(automaton, input_traces)
1241+
1242+
class IOAlergiaWithEDSM(ScoreCalculation):
1243+
def __init__(self, epsilon):
1244+
super().__init__()
1245+
self.ioa_compatibility = hoeffding_compatibility(epsilon)
1246+
self.evidence = 0
1247+
1248+
def reset(self):
1249+
self.evidence = 0
1250+
1251+
def local_compatibility(self, a: Node, b: Node):
1252+
self.evidence += 1
1253+
return self.ioa_compatibility(a, b)
1254+
1255+
def score_function(self, part: dict[Node, Node]):
1256+
return self.evidence
1257+
1258+
epsilon = 0.05
1259+
scores = {
1260+
"IOA": ScoreCalculation(hoeffding_compatibility(epsilon)),
1261+
"IOA+EDSM": IOAlergiaWithEDSM(epsilon),
1262+
}
1263+
for name, score in scores.items():
1264+
learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="stochastic", score_calc=score,
1265+
compatibility_on_pta=True, compatibility_on_futures=True)
1266+
learned_model.visualize(name)
1267+
1268+
1269+
def gsm_IOAlergia_domain_knowldege():
1270+
from aalpy.learning_algs.general_passive.GeneralizedStateMerging import run_GSM
1271+
from aalpy.learning_algs.general_passive.ScoreFunctionsGSM import hoeffding_compatibility, ScoreCalculation
1272+
from aalpy.learning_algs.general_passive.Node import Node
1273+
from aalpy.utils.Sampling import get_io_traces, sample_with_length_limits
1274+
from aalpy import load_automaton_from_file
1275+
1276+
automaton = load_automaton_from_file("DotModels/MDPs/faulty_car_alarm.dot", "mdp")
1277+
input_traces = sample_with_length_limits(automaton.get_input_alphabet(), 2000, 20, 30)
1278+
traces = get_io_traces(automaton, input_traces)
1279+
1280+
ioa_compat = hoeffding_compatibility(0.05)
1281+
1282+
def get_parity(node: Node):
1283+
pref = node.get_prefix()
1284+
return [sum(in_s == key for in_s, out_s in pref) % 2 for key in ["l", "d"]]
1285+
1286+
# The car has 4 physical states arising from the combination of locked/unlocked and open/closed.
1287+
# Each input toggles a transition between these four states. While the car alarm system has richer behavior than that,
1288+
# it still needs to discern the physical states. Thus, in every sane implementation of a car alarm system, every state
1289+
# is associated with exactly one physical state. This additional assumption can be enforced by checking the parity of
1290+
# all input symbols during merging.
1291+
def ioa_compat_domain_knowledge(a: Node, b: Node):
1292+
parity = get_parity(a) == get_parity(b)
1293+
ioa = ioa_compat(a, b)
1294+
return parity and ioa
1295+
1296+
scores = {
1297+
"IOA": ScoreCalculation(ioa_compat),
1298+
"IOA+DK": ScoreCalculation(ioa_compat_domain_knowledge),
1299+
}
1300+
for name, score in scores.items():
1301+
learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="stochastic", score_calc=score,
1302+
compatibility_on_pta=True, compatibility_on_futures=True)
1303+
learned_model.visualize(name)

aalpy/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
MealyState,
1010
MooreMachine,
1111
MooreState,
12+
NDMooreMachine,
13+
NDMooreState,
1214
Onfsm,
1315
OnfsmState,
1416
Sevpa,
@@ -37,6 +39,7 @@
3739
run_non_det_Lstar,
3840
run_RPNI,
3941
run_stochastic_Lstar,
42+
run_GSM,
4043
run_PAPNI
4144
)
4245
from .oracles import (
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import random
2+
from collections import defaultdict
3+
from typing import List, Dict, Generic
4+
5+
from aalpy.base import AutomatonState, Automaton
6+
from aalpy.base.Automaton import OutputType, InputType
7+
8+
9+
class NDMooreState(AutomatonState, Generic[InputType, OutputType]):
10+
"""
11+
Single state of a non-deterministic Moore machine. Each state has an output value.
12+
"""
13+
14+
def __init__(self, state_id, output=None):
15+
super().__init__(state_id)
16+
self.transitions: Dict[InputType, List['NDMooreState']] = defaultdict(lambda: list())
17+
self.output: OutputType = output
18+
19+
20+
class NDMooreMachine(Automaton[NDMooreState[InputType, OutputType]]):
21+
22+
def to_state_setup(self):
23+
state_setup = dict()
24+
25+
def set_dict_entry(state: NDMooreState):
26+
state_setup[state.state_id] = (state.output,
27+
{in_sym: [target.state_id for target in trans] for in_sym, trans in
28+
state.transitions.items()})
29+
30+
set_dict_entry(self.initial_state)
31+
for state in self.states:
32+
if state is self.initial_state:
33+
continue
34+
set_dict_entry(state)
35+
36+
@staticmethod
37+
def from_state_setup(state_setup: dict, **kwargs) -> 'NDMooreMachine':
38+
states_map = {key: NDMooreState(key, output=value[0]) for key, value in state_setup.items()}
39+
40+
for key, values in state_setup.items():
41+
source = states_map[key]
42+
for i, transitions in values[1].items():
43+
for node in transitions:
44+
source.transitions[i].append(states_map[node])
45+
46+
initial_state = states_map[list(state_setup.keys())[0]]
47+
return NDMooreMachine(initial_state, list(states_map.values()))
48+
49+
def __init__(self, initial_state: AutomatonState, states: list):
50+
super().__init__(initial_state, states)
51+
52+
def step(self, letter):
53+
"""
54+
In Moore machines outputs depend on the current state.
55+
56+
Args:
57+
58+
letter: single input that is looked up in the transition function leading to a new state
59+
60+
Returns:
61+
62+
the output of the reached state
63+
64+
"""
65+
options = self.current_state.transitions[letter]
66+
self.current_state = random.choice(options)
67+
return self.current_state.output

aalpy/automata/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
from .Onfsm import Onfsm, OnfsmState
66
from .StochasticMealyMachine import StochasticMealyMachine, StochasticMealyState
77
from .MarkovChain import MarkovChain, McState
8+
from .NonDeterministicMooreMachine import NDMooreMachine, NDMooreState
89
from .Sevpa import Sevpa, SevpaState, SevpaAlphabet, SevpaTransition
910
from .Vpa import Vpa, VpaAlphabet, VpaState, VpaTransition

aalpy/learning_algs/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
from .stochastic_passive.ActiveAleriga import run_active_Alergia
1111
from .deterministic_passive.RPNI import run_RPNI, run_PAPNI
1212
from .deterministic_passive.active_RPNI import run_active_RPNI
13+
from .general_passive.GeneralizedStateMerging import run_GSM
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import time
2+
from bisect import insort
3+
from aalpy.learning_algs.deterministic_passive.rpni_helper_functions import to_automaton, createPTA, \
4+
check_sequence, extract_unique_sequences
5+
6+
7+
class ClassicRPNI:
8+
def __init__(self, data, automaton_type, print_info=True):
9+
self.data = data
10+
self.automaton_type = automaton_type
11+
self.print_info = print_info
12+
13+
pta_construction_start = time.time()
14+
self.root_node = createPTA(data, automaton_type)
15+
self.test_data = extract_unique_sequences(self.root_node)
16+
17+
if self.print_info:
18+
print(f'PTA Construction Time: {round(time.time() - pta_construction_start, 2)}')
19+
20+
def run_rpni(self):
21+
start_time = time.time()
22+
23+
red = [self.root_node]
24+
blue = list(red[0].children.values())
25+
while blue:
26+
lex_min_blue = min(list(blue))
27+
merged = False
28+
29+
for red_state in red:
30+
if not self._compatible_states(red_state, lex_min_blue):
31+
continue
32+
merge_candidate = self._merge(red_state, lex_min_blue, copy_nodes=True)
33+
if self._compatible(merge_candidate):
34+
self._merge(red_state, lex_min_blue)
35+
merged = True
36+
break
37+
38+
if not merged:
39+
insort(red, lex_min_blue)
40+
if self.print_info:
41+
print(f'\rCurrent automaton size: {len(red)}', end="")
42+
43+
blue.clear()
44+
for r in red:
45+
for c in r.children.values():
46+
if c not in red:
47+
blue.append(c)
48+
49+
if self.print_info:
50+
print(f'\nRPNI Learning Time: {round(time.time() - start_time, 2)}')
51+
print(f'RPNI Learned {len(red)} state automaton.')
52+
53+
assert sorted(red, key=lambda x: len(x.prefix)) == red
54+
return to_automaton(red, self.automaton_type)
55+
56+
def _compatible(self, root_node):
57+
"""
58+
Check if current model is compatible with the data.
59+
"""
60+
for sequence in self.test_data:
61+
if not check_sequence(root_node, sequence, automaton_type=self.automaton_type):
62+
return False
63+
return True
64+
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+
80+
def _merge(self, red_node, lex_min_blue, copy_nodes=False):
81+
"""
82+
Merge two states and return the root node of resulting model.
83+
"""
84+
root_node = self.root_node.copy() if copy_nodes else self.root_node
85+
lex_min_blue = lex_min_blue.copy() if copy_nodes else lex_min_blue
86+
87+
red_node_in_tree = root_node
88+
for p in red_node.prefix:
89+
red_node_in_tree = red_node_in_tree.children[p]
90+
91+
to_update = root_node
92+
for p in lex_min_blue.prefix[:-1]:
93+
to_update = to_update.children[p]
94+
95+
to_update.children[lex_min_blue.prefix[-1]] = red_node_in_tree
96+
97+
if self.automaton_type != 'mealy':
98+
self._fold(red_node_in_tree, lex_min_blue)
99+
else:
100+
self._fold_mealy(red_node_in_tree, lex_min_blue)
101+
102+
return root_node
103+
104+
def _fold(self, red_node, blue_node):
105+
# Change the output of red only to concrete output, ignore None
106+
red_node.output = blue_node.output if blue_node.output is not None else red_node.output
107+
108+
for i in blue_node.children.keys():
109+
if i in red_node.children.keys():
110+
self._fold(red_node.children[i], blue_node.children[i])
111+
else:
112+
red_node.children[i] = blue_node.children[i]
113+
114+
def _fold_mealy(self, red_node, blue_node):
115+
blue_io_map = {i: o for i, o in blue_node.children.keys()}
116+
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])
127+
else:
128+
red_node.children[io] = blue_node.children[io]
129+

0 commit comments

Comments
 (0)