From fb43cae006e056228584aa46643d0b25d3b1a0cc Mon Sep 17 00:00:00 2001 From: zwergziege Date: Thu, 9 Nov 2023 13:52:32 +0100 Subject: [PATCH 1/7] Simplified AutomataSUL file --- aalpy/SULs/AutomataSUL.py | 168 +++----------------------------------- 1 file changed, 13 insertions(+), 155 deletions(-) diff --git a/aalpy/SULs/AutomataSUL.py b/aalpy/SULs/AutomataSUL.py index 1982a56f..3ee6389e 100644 --- a/aalpy/SULs/AutomataSUL.py +++ b/aalpy/SULs/AutomataSUL.py @@ -1,166 +1,24 @@ -from aalpy.base import SUL -from aalpy.automata import Dfa, MealyMachine, MooreMachine, Onfsm, Mdp, StochasticMealyMachine, MarkovChain +from aalpy.automata import MooreMachine, Dfa, MarkovChain, Mdp +from aalpy.base import SUL, Automaton - -class DfaSUL(SUL): - """ - System under learning for DFAs. - """ - - def __init__(self, dfa: Dfa): - super().__init__() - self.dfa = dfa - - def pre(self): - """ - Resets the dfa to the initial state. - """ - self.dfa.reset_to_initial() - - def post(self): - pass - - def step(self, letter): - """ - If the letter is empty/None check is preform to see if the empty string is accepted by the DFA. - - Args: - - letter: single input or None representing the empty string - - Returns: - - output of the dfa.step method (whether the next state is accepted or not) - - """ - return self.dfa.step(letter) - - -class MdpSUL(SUL): - def __init__(self, mdp: Mdp): +class AutomatonSUL(SUL): + def __init__(self, automaton : Automaton): super().__init__() - self.mdp = mdp - - def query(self, word: tuple) -> list: - initial_output = self.pre() - out = [initial_output] - for letter in word: - out.append(self.step(letter)) - self.post() - return out + self.automaton : Automaton = automaton def pre(self): - self.mdp.reset_to_initial() - return self.mdp.current_state.output - - def post(self): - pass - - def step(self, letter): - return self.mdp.step(letter) - - -class McSUL(SUL): - def __init__(self, mdp: MarkovChain): - super().__init__() - self.mc = mdp - - def query(self, word: tuple) -> list: - initial_output = self.pre() - out = [initial_output] - for letter in word: - out.append(self.step(letter)) - self.post() - return out - - def pre(self): - self.mc.reset_to_initial() - return self.mc.current_state.output - - def post(self): - pass + self.automaton.reset_to_initial() def step(self, letter=None): - return self.mc.step() - - -class MealySUL(SUL): - """ - System under learning for Mealy machines. - """ - - def __init__(self, mm: MealyMachine): - super().__init__() - self.mm = mm - - def pre(self): - """ """ - self.mm.reset_to_initial() - - def post(self): - """ """ - pass - - def step(self, letter): - """ - Args: - - letter: single non-Null input - - Returns: - - output of the mealy.step method (output based on the input and the current state) - - """ - return self.mm.step(letter) - - -class MooreSUL(SUL): - """ - System under learning for Mealy machines. - """ - - def __init__(self, moore_machine: MooreMachine): - super().__init__() - self.mm = moore_machine - - def pre(self): - """ """ - self.mm.reset_to_initial() + return self.automaton.step(letter) def post(self): - """ """ pass - def step(self, letter): - return self.mm.step(letter) - - -class OnfsmSUL(SUL): - def __init__(self, mdp: Onfsm): - super().__init__() - self.onfsm = mdp - - def pre(self): - self.onfsm.reset_to_initial() - - def post(self): - pass - - def step(self, letter): - return self.onfsm.step(letter) - - -class StochasticMealySUL(SUL): - def __init__(self, smm: StochasticMealyMachine): - super().__init__() - self.smm = smm - - def pre(self): - self.smm.reset_to_initial() - - def post(self): - pass + def query(self, word: tuple) -> list: + output = super().query(word) + if isinstance(self.automaton, (MooreMachine, Dfa, MarkovChain, Mdp)): + output.insert(0, self.automaton.initial_state.output) + return output - def step(self, letter): - return self.smm.step(letter) +MealySUL = OnfsmSUL = StochasticMealySUL = DfaSUL = MooreSUL = MdpSUL = McSUL = AutomatonSUL \ No newline at end of file From 9756abd63c142a5cebc695e1e282b4139eff75ad Mon Sep 17 00:00:00 2001 From: zwergziege Date: Fri, 10 Nov 2023 11:56:25 +0100 Subject: [PATCH 2/7] Purge aliases as much as possible --- Benchmarking/Benchmark_ErrorStop.py | 6 +- .../CompleteStochasticBenchmarking.py | 6 +- Benchmarking/StochasticAlgComparison.py | 6 +- Benchmarking/StochasticBenchmarkingWPrism.py | 4 +- Benchmarking/StopWithErorrRate.py | 4 +- Benchmarking/benchmark.py | 6 +- Benchmarking/benchmark_alphabet_increase.py | 8 +- Benchmarking/benchmark_size_increase.py | 8 +- Benchmarking/compare_lstar_and_kv.py | 6 +- .../evaluate_l_star_configurations.py | 4 +- Benchmarking/passive_mdp_vs_smm.py | 4 +- .../Benchmark_ErrorStop.py | 6 +- .../CompleteStochasticBenchmarking.py | 6 +- .../StochasticBenchmarkingWPrism.py | 4 +- .../passive_mdp_vs_smm.py | 4 +- .../stochastic_benchmark_random_automata.py | 4 +- .../stochastic_benchmarking/strategy_comp.py | 4 +- DotModels/Bluetooth/convert_to_stochastic.py | 4 +- Examples.py | 82 +++++++++---------- README.md | 4 +- aalpy/SULs/__init__.py | 2 +- aalpy/utils/ModelChecking.py | 9 +- tests/oracles/test_baseOracle.py | 6 +- tests/test_deterministic.py | 23 ++---- tests/test_non_deterministic.py | 4 +- tests/test_stochastic.py | 4 +- tests/test_wmethod_oracle.py | 10 +-- 27 files changed, 113 insertions(+), 125 deletions(-) diff --git a/Benchmarking/Benchmark_ErrorStop.py b/Benchmarking/Benchmark_ErrorStop.py index eafbe2e3..6ea15148 100644 --- a/Benchmarking/Benchmark_ErrorStop.py +++ b/Benchmarking/Benchmark_ErrorStop.py @@ -3,7 +3,7 @@ import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar from aalpy.oracles.RandomWordEqOracle import UnseenOutputRandomWordEqOracle from aalpy.utils import load_automaton_from_file, get_properties_file, get_correct_prop_values @@ -83,7 +83,7 @@ original_mdp = load_automaton_from_file(path_to_dir + file, automaton_type='mdp') input_alphabet = original_mdp.get_input_alphabet() - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = UnseenOutputRandomWordEqOracle(input_alphabet, mdp_sul, num_walks=150, min_walk_len=5, max_walk_len=15, reset_after_cex=True) @@ -96,7 +96,7 @@ del mdp_sul del eq_oracle random.seed(seeds[seed]) - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = UnseenOutputRandomWordEqOracle(input_alphabet, mdp_sul, num_walks=150, min_walk_len=5, max_walk_len=15, reset_after_cex=True) diff --git a/Benchmarking/CompleteStochasticBenchmarking.py b/Benchmarking/CompleteStochasticBenchmarking.py index f424424e..78bda3f9 100644 --- a/Benchmarking/CompleteStochasticBenchmarking.py +++ b/Benchmarking/CompleteStochasticBenchmarking.py @@ -3,7 +3,7 @@ import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar from aalpy.oracles.RandomWordEqOracle import RandomWordEqOracle from aalpy.utils import load_automaton_from_file, get_properties_file, get_correct_prop_values @@ -59,7 +59,7 @@ original_mdp = model_dict[exp_name] input_alphabet = original_mdp.get_input_alphabet() - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = RandomWordEqOracle(input_alphabet, mdp_sul, num_walks=150, min_walk_len=5, max_walk_len=16, reset_after_cex=True) @@ -72,7 +72,7 @@ del mdp_sul del eq_oracle random.seed(seeds[seed]) - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = RandomWordEqOracle(input_alphabet, mdp_sul, num_walks=150, min_walk_len=5, max_walk_len=15, reset_after_cex=True) diff --git a/Benchmarking/StochasticAlgComparison.py b/Benchmarking/StochasticAlgComparison.py index 410b1951..0fe728b6 100644 --- a/Benchmarking/StochasticAlgComparison.py +++ b/Benchmarking/StochasticAlgComparison.py @@ -4,7 +4,7 @@ import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar, run_Alergia from aalpy.oracles.RandomWordEqOracle import RandomWordEqOracle from aalpy.utils import load_automaton_from_file, get_properties_file, get_correct_prop_values @@ -32,7 +32,7 @@ original_mdp = model_dict[exp_name] input_alphabet = original_mdp.get_input_alphabet() - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = RandomWordEqOracle(input_alphabet, mdp_sul, num_walks=500, min_walk_len=5, max_walk_len=16, reset_after_cex=True) @@ -46,7 +46,7 @@ del mdp_sul del eq_oracle - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = RandomWordEqOracle(input_alphabet, mdp_sul, num_walks=150, min_walk_len=5, max_walk_len=15, reset_after_cex=True) diff --git a/Benchmarking/StochasticBenchmarkingWPrism.py b/Benchmarking/StochasticBenchmarkingWPrism.py index a3116008..e7f1f5c6 100644 --- a/Benchmarking/StochasticBenchmarkingWPrism.py +++ b/Benchmarking/StochasticBenchmarkingWPrism.py @@ -2,7 +2,7 @@ import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar from aalpy.oracles.RandomWalkEqOracle import RandomWalkEqOracle from aalpy.utils import load_automaton_from_file, get_correct_prop_values, get_properties_file @@ -65,7 +65,7 @@ original_mdp = load_automaton_from_file(path_to_dir + file, automaton_type='mdp') input_alphabet = original_mdp.get_input_alphabet() - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = RandomWalkEqOracle(input_alphabet, mdp_sul, num_steps=n_resample * (1 / 0.25), reset_after_cex=True, reset_prob=0.25) diff --git a/Benchmarking/StopWithErorrRate.py b/Benchmarking/StopWithErorrRate.py index 1cab31fd..f6027c89 100644 --- a/Benchmarking/StopWithErorrRate.py +++ b/Benchmarking/StopWithErorrRate.py @@ -6,7 +6,7 @@ import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar, run_Alergia from aalpy.oracles.RandomWordEqOracle import RandomWordEqOracle from aalpy.utils import load_automaton_from_file, get_properties_file, get_correct_prop_values, model_check_properties @@ -43,7 +43,7 @@ # original_mdp = model_dict[exp_name] # input_alphabet = original_mdp.get_input_alphabet() # -# mdp_sul = MdpSUL(original_mdp) +# mdp_sul = AutomatonSUL(original_mdp) # # eq_oracle = RandomWordEqOracle(input_alphabet, mdp_sul, num_walks=500, min_walk_len=5, # max_walk_len=15, reset_after_cex=True) diff --git a/Benchmarking/benchmark.py b/Benchmarking/benchmark.py index 87d1b4e8..a66b7a67 100644 --- a/Benchmarking/benchmark.py +++ b/Benchmarking/benchmark.py @@ -1,7 +1,7 @@ import os from statistics import mean -from aalpy.SULs import DfaSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_Lstar from aalpy.oracles import StatePrefixEqOracle from aalpy.utils import load_automaton_from_file @@ -13,7 +13,7 @@ run_times = [] # change on which folder to perform experiments -exp, sul = dfa_2000_states_10_inputs, DfaSUL +exp = dfa_2000_states_10_inputs benchmarks = os.listdir(exp) benchmarks = benchmarks[:10] @@ -28,7 +28,7 @@ automaton = load_automaton_from_file(f'{exp}/{b}', automaton_type='dfa') input_al = automaton.get_input_alphabet() - sul_dfa = sul(automaton) + sul_dfa = AutomatonSUL(automaton) state_origin_eq_oracle = StatePrefixEqOracle(input_al, sul_dfa, walks_per_state=5, walk_len=25) diff --git a/Benchmarking/benchmark_alphabet_increase.py b/Benchmarking/benchmark_alphabet_increase.py index a790c928..41994fb2 100644 --- a/Benchmarking/benchmark_alphabet_increase.py +++ b/Benchmarking/benchmark_alphabet_increase.py @@ -1,7 +1,7 @@ from statistics import mean import csv -from aalpy.SULs import DfaSUL, MealySUL, MooreSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_Lstar from aalpy.oracles import RandomWalkEqOracle from aalpy.utils import generate_random_dfa, generate_random_mealy_machine, generate_random_moore_machine @@ -28,7 +28,7 @@ alphabet = list(range(alph_size)) dfa = generate_random_dfa(num_states, alphabet=alphabet, num_accepting_states=num_states // 2) - sul = DfaSUL(dfa) + sul = AutomatonSUL(dfa) # eq_oracle = StatePrefixEqOracle(alphabet, sul, walks_per_state=5, walk_len=40) eq_oracle = RandomWalkEqOracle(alphabet, sul, num_steps=10000, reset_prob=0.09) @@ -42,7 +42,7 @@ del eq_oracle mealy = generate_random_mealy_machine(num_states, input_alphabet=alphabet, output_alphabet=alphabet) - sul_mealy = MealySUL(mealy) + sul_mealy = AutomatonSUL(mealy) # eq_oracle = StatePrefixEqOracle(alphabet, sul_mealy, walks_per_state=5, walk_len=40) eq_oracle = RandomWalkEqOracle(alphabet, sul_mealy, num_steps=10000, reset_prob=0.09) @@ -58,7 +58,7 @@ del eq_oracle moore = generate_random_moore_machine(num_states, input_alphabet=alphabet, output_alphabet=alphabet) - moore_sul = MooreSUL(moore) + moore_sul = AutomatonSUL(moore) # eq_oracle = StatePrefixEqOracle(alphabet, moore_sul, walks_per_state=5, walk_len=40) eq_oracle = RandomWalkEqOracle(alphabet, moore_sul, num_steps=10000, reset_prob=0.09) diff --git a/Benchmarking/benchmark_size_increase.py b/Benchmarking/benchmark_size_increase.py index a51bf733..f103039d 100644 --- a/Benchmarking/benchmark_size_increase.py +++ b/Benchmarking/benchmark_size_increase.py @@ -1,7 +1,7 @@ from statistics import mean import csv -from aalpy.SULs import DfaSUL, MealySUL, MooreSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_Lstar from aalpy.oracles import RandomWalkEqOracle from aalpy.utils import generate_random_dfa, generate_random_mealy_machine, generate_random_moore_machine @@ -35,7 +35,7 @@ for _ in range(repeat): dfa = generate_random_dfa(num_states, alphabet=alphabet, num_accepting_states=num_states // 2) - sul = DfaSUL(dfa) + sul = AutomatonSUL(dfa) # eq_oracle = StatePrefixEqOracle(alphabet, sul, walks_per_state=5, walk_len=40) eq_oracle = RandomWalkEqOracle(alphabet, sul, num_steps=9000, reset_prob=0.09) @@ -51,7 +51,7 @@ del dfa mealy = generate_random_mealy_machine(num_states, input_alphabet=alphabet, output_alphabet=alphabet) - sul_mealy = MealySUL(mealy) + sul_mealy = AutomatonSUL(mealy) # eq_oracle = StatePrefixEqOracle(alphabet, sul_mealy, walks_per_state=5, walk_len=40) eq_oracle = RandomWalkEqOracle(alphabet, sul_mealy, num_steps=9000, reset_prob=0.09) @@ -68,7 +68,7 @@ del eq_oracle moore = generate_random_moore_machine(num_states, input_alphabet=alphabet, output_alphabet=alphabet) - moore_sul = MooreSUL(moore) + moore_sul = AutomatonSUL(moore) # eq_oracle = StatePrefixEqOracle(alphabet, moore_sul, walks_per_state=5, walk_len=40) eq_oracle = RandomWalkEqOracle(alphabet, moore_sul, num_steps=9000, reset_prob=0.09) diff --git a/Benchmarking/compare_lstar_and_kv.py b/Benchmarking/compare_lstar_and_kv.py index 8b3082c0..687aa790 100644 --- a/Benchmarking/compare_lstar_and_kv.py +++ b/Benchmarking/compare_lstar_and_kv.py @@ -1,4 +1,4 @@ -from aalpy.SULs import DfaSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_Lstar, run_KV from aalpy.oracles import RandomWordEqOracle from aalpy.utils import generate_random_deterministic_automata @@ -23,14 +23,14 @@ print(f'Type: {model_type}, size: {size}, # inputs: {i}, # accepting: {size//8}') # Lstar - sul = DfaSUL(random_model) + sul = AutomatonSUL(random_model) eq_oracle = RandomWordEqOracle(input_al, sul, num_walks=5000, min_walk_len=10, max_walk_len=40) l_star_model, l_star_info = run_Lstar(input_al, sul, eq_oracle, model_type, print_level=0, return_data=True) l_star_steps, l_star_queries = l_star_info['steps_learning'], l_star_info['queries_learning'] # KV - sul = DfaSUL(random_model) + sul = AutomatonSUL(random_model) eq_oracle = RandomWordEqOracle(input_al, sul, num_walks=5000, min_walk_len=10, max_walk_len=40) kv_model, kv_info = run_KV(input_al, sul, eq_oracle, model_type, print_level=0, return_data=True) diff --git a/Benchmarking/evaluate_l_star_configurations.py b/Benchmarking/evaluate_l_star_configurations.py index 8e311795..ad4a26fe 100644 --- a/Benchmarking/evaluate_l_star_configurations.py +++ b/Benchmarking/evaluate_l_star_configurations.py @@ -3,7 +3,7 @@ from random import seed from statistics import mean -from aalpy.SULs import DfaSUL, MealySUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_Lstar from aalpy.oracles import StatePrefixEqOracle, RandomWMethodEqOracle, RandomWalkEqOracle, RandomWordEqOracle from aalpy.utils import generate_random_deterministic_automata @@ -40,7 +40,7 @@ tc += 1 print(round(tc / num_exp * 100, 2)) # seed(tc) - sul = MealySUL(test_model) + sul = AutomatonSUL(test_model) eq_oracle = RandomWordEqOracle(input_al, sul, num_walks=5000, min_walk_len=10, max_walk_len=40) model, info = run_Lstar(input_al, sul, eq_oracle, 'dfa', closing_strategy=closing_strategy, diff --git a/Benchmarking/passive_mdp_vs_smm.py b/Benchmarking/passive_mdp_vs_smm.py index 3eacd173..c77ee136 100644 --- a/Benchmarking/passive_mdp_vs_smm.py +++ b/Benchmarking/passive_mdp_vs_smm.py @@ -1,7 +1,7 @@ import random import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.automata.StochasticMealyMachine import smm_to_mdp_conversion from aalpy.learning_algs import run_Alergia from aalpy.utils import load_automaton_from_file, get_correct_prop_values, get_properties_file @@ -45,7 +45,7 @@ def deleteSampleFile(path="alergiaSamples.txt"): original_mdp = load_automaton_from_file(path_to_dir + file, automaton_type='mdp') input_alphabet = original_mdp.get_input_alphabet() - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) for _ in range(1): diff --git a/Benchmarking/stochastic_benchmarking/Benchmark_ErrorStop.py b/Benchmarking/stochastic_benchmarking/Benchmark_ErrorStop.py index a206f677..336e9e0e 100644 --- a/Benchmarking/stochastic_benchmarking/Benchmark_ErrorStop.py +++ b/Benchmarking/stochastic_benchmarking/Benchmark_ErrorStop.py @@ -3,7 +3,7 @@ import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar from aalpy.oracles.RandomWordEqOracle import RandomWordEqOracle from aalpy.utils import load_automaton_from_file, get_properties_file, get_correct_prop_values @@ -83,7 +83,7 @@ original_mdp = load_automaton_from_file(path_to_dir + file, automaton_type='mdp') input_alphabet = original_mdp.get_input_alphabet() - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = RandomWordEqOracle(input_alphabet, mdp_sul, num_walks=150, min_walk_len=5, max_walk_len=15, reset_after_cex=True) @@ -96,7 +96,7 @@ del mdp_sul del eq_oracle random.seed(seeds[seed]) - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = RandomWordEqOracle(input_alphabet, mdp_sul, num_walks=150, min_walk_len=5, max_walk_len=15, reset_after_cex=True) diff --git a/Benchmarking/stochastic_benchmarking/CompleteStochasticBenchmarking.py b/Benchmarking/stochastic_benchmarking/CompleteStochasticBenchmarking.py index 83be9c09..2231b84b 100644 --- a/Benchmarking/stochastic_benchmarking/CompleteStochasticBenchmarking.py +++ b/Benchmarking/stochastic_benchmarking/CompleteStochasticBenchmarking.py @@ -3,7 +3,7 @@ import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar from aalpy.oracles.RandomWordEqOracle import RandomWordEqOracle from aalpy.utils import load_automaton_from_file, get_properties_file, get_correct_prop_values @@ -60,7 +60,7 @@ original_mdp = model_dict[exp_name] input_alphabet = original_mdp.get_input_alphabet() - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = RandomWordEqOracle(input_alphabet, mdp_sul, num_walks=150, min_walk_len=5, max_walk_len=16, reset_after_cex=True) @@ -73,7 +73,7 @@ del mdp_sul del eq_oracle random.seed(seeds[seed]) - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = RandomWordEqOracle(input_alphabet, mdp_sul, num_walks=150, min_walk_len=5, max_walk_len=15, reset_after_cex=True) diff --git a/Benchmarking/stochastic_benchmarking/StochasticBenchmarkingWPrism.py b/Benchmarking/stochastic_benchmarking/StochasticBenchmarkingWPrism.py index 650caa12..23f1f5a8 100644 --- a/Benchmarking/stochastic_benchmarking/StochasticBenchmarkingWPrism.py +++ b/Benchmarking/stochastic_benchmarking/StochasticBenchmarkingWPrism.py @@ -2,7 +2,7 @@ import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar from aalpy.oracles.RandomWalkEqOracle import RandomWalkEqOracle from aalpy.utils import load_automaton_from_file, get_correct_prop_values, get_properties_file @@ -65,7 +65,7 @@ original_mdp = load_automaton_from_file(path_to_dir + file, automaton_type='mdp') input_alphabet = original_mdp.get_input_alphabet() - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) eq_oracle = RandomWalkEqOracle(input_alphabet, mdp_sul, num_steps=n_resample * (1 / 0.25), reset_after_cex=True, reset_prob=0.25) diff --git a/Benchmarking/stochastic_benchmarking/passive_mdp_vs_smm.py b/Benchmarking/stochastic_benchmarking/passive_mdp_vs_smm.py index 2bad4295..79771ccf 100644 --- a/Benchmarking/stochastic_benchmarking/passive_mdp_vs_smm.py +++ b/Benchmarking/stochastic_benchmarking/passive_mdp_vs_smm.py @@ -1,7 +1,7 @@ import random import os import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.automata.StochasticMealyMachine import smm_to_mdp_conversion from aalpy.learning_algs import run_Alergia, run_JAlergia from aalpy.utils import load_automaton_from_file, get_correct_prop_values, get_properties_file, visualize_automaton @@ -45,7 +45,7 @@ def deleteSampleFile(path="alergiaSamples.txt"): original_mdp = load_automaton_from_file(path_to_dir + file, automaton_type='mdp') input_alphabet = original_mdp.get_input_alphabet() - mdp_sul = MdpSUL(original_mdp) + mdp_sul = AutomatonSUL(original_mdp) for _ in range(1): diff --git a/Benchmarking/stochastic_benchmarking/stochastic_benchmark_random_automata.py b/Benchmarking/stochastic_benchmarking/stochastic_benchmark_random_automata.py index b6ad6cfc..72e8e93b 100644 --- a/Benchmarking/stochastic_benchmarking/stochastic_benchmark_random_automata.py +++ b/Benchmarking/stochastic_benchmarking/stochastic_benchmark_random_automata.py @@ -1,6 +1,6 @@ from itertools import product -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar from aalpy.oracles import RandomWordEqOracle from aalpy.utils import generate_random_mdp, generate_random_smm @@ -14,7 +14,7 @@ def learn(mdp, type): input_al = mdp.get_input_alphabet() - sul = MdpSUL(mdp) + sul = AutomatonSUL(mdp) eq_oracle = RandomWordEqOracle(input_al, sul, num_walks=1000, min_walk_len=4, max_walk_len=20) return run_stochastic_Lstar(input_al, sul, eq_oracle, automaton_type=type, cex_processing=None, print_level=0, return_data=True) diff --git a/Benchmarking/stochastic_benchmarking/strategy_comp.py b/Benchmarking/stochastic_benchmarking/strategy_comp.py index c4893163..3eff1c5b 100644 --- a/Benchmarking/stochastic_benchmarking/strategy_comp.py +++ b/Benchmarking/stochastic_benchmarking/strategy_comp.py @@ -4,7 +4,7 @@ import aalpy.paths -from aalpy.SULs import MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar from aalpy.oracles import RandomWordEqOracle @@ -22,7 +22,7 @@ def learn(strategy): input_al = mdp.get_input_alphabet() - sul = MdpSUL(mdp) + sul = AutomatonSUL(mdp) eq_oracle = RandomWordEqOracle(input_al, sul, num_walks=1000, min_walk_len=4, max_walk_len=20) model, data = run_stochastic_Lstar(input_al, sul, eq_oracle, automaton_type='smm', strategy=strategy, cex_processing=None, print_level=0, return_data=True) diff --git a/DotModels/Bluetooth/convert_to_stochastic.py b/DotModels/Bluetooth/convert_to_stochastic.py index 8558b647..8728c0f2 100644 --- a/DotModels/Bluetooth/convert_to_stochastic.py +++ b/DotModels/Bluetooth/convert_to_stochastic.py @@ -1,6 +1,6 @@ import random -from aalpy.SULs import MdpSUL, StochasticMealySUL +from aalpy.SULs import AutomatonSUL from aalpy.base import SUL from aalpy.automata import Mdp, MdpState, StochasticMealyState, StochasticMealyMachine from aalpy.learning_algs import run_Lstar, run_stochastic_Lstar @@ -113,7 +113,7 @@ def to_smm(): # exit() # mdp.make_input_complete('self_loop') # mdp_sul = StochasticMealySUL(mdp) -mdp_sul = MdpSUL(mdp.to_mdp()) +mdp_sul = AutomatonSUL(mdp.to_mdp()) eq_oracle = RandomWordEqOracle(alphabet, model_sul, num_walks=10000, min_walk_len=10, max_walk_len=100) stochastic_model = run_stochastic_Lstar(alphabet, mdp_sul, eq_oracle, automaton_type='mdp') diff --git a/Examples.py b/Examples.py index 59f1971b..c49ed0f0 100644 --- a/Examples.py +++ b/Examples.py @@ -1,6 +1,6 @@ def random_deterministic_model_example(): from aalpy.utils import generate_random_deterministic_automata - from aalpy.SULs import MealySUL + from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWMethodEqOracle from aalpy.learning_algs import run_KV @@ -10,7 +10,7 @@ def random_deterministic_model_example(): random_model = generate_random_deterministic_automata(automaton_type=model_type, num_states=100, input_alphabet_size=3, output_alphabet_size=4) - sul = MealySUL(random_model) + sul = AutomatonSUL(random_model) input_alphabet = random_model.get_input_alphabet() # select any of the oracles @@ -26,7 +26,7 @@ def angluin_seminal_example(): Example automaton from Angluin's seminal paper. :return: learned DFA """ - from aalpy.SULs import DfaSUL + from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWalkEqOracle from aalpy.learning_algs import run_Lstar from aalpy.utils import get_Angluin_dfa @@ -35,7 +35,7 @@ def angluin_seminal_example(): alphabet = dfa.get_input_alphabet() - sul = DfaSUL(dfa) + sul = AutomatonSUL(dfa) eq_oracle = RandomWalkEqOracle(alphabet, sul, 500) learned_dfa = run_Lstar(alphabet, sul, eq_oracle, automaton_type='dfa', @@ -142,7 +142,7 @@ def step(self, letter): def random_deterministic_example_with_provided_sequences(): from random import choice, randint - from aalpy.SULs import MealySUL + from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_Lstar from aalpy.utils import generate_random_deterministic_automata @@ -151,7 +151,7 @@ def random_deterministic_example_with_provided_sequences(): input_alphabet = random_mealy.get_input_alphabet() - sul_mealy = MealySUL(random_mealy) + sul_mealy = AutomatonSUL(random_mealy) # samples obtained form somewhere else samples = [] @@ -223,7 +223,7 @@ def random_onfsm_example(num_states, input_size, output_size, n_sampling): observed :return: learned ONFSM """ - from aalpy.SULs import OnfsmSUL + from aalpy.SULs import AutomatonSUL from aalpy.utils import generate_random_ONFSM from aalpy.oracles import RandomWalkEqOracle, RandomWordEqOracle from aalpy.learning_algs import run_non_det_Lstar @@ -231,7 +231,7 @@ def random_onfsm_example(num_states, input_size, output_size, n_sampling): onfsm = generate_random_ONFSM(num_states=num_states, num_inputs=input_size, num_outputs=output_size) alphabet = onfsm.get_input_alphabet() - sul = OnfsmSUL(onfsm) + sul = AutomatonSUL(onfsm) eq_oracle = RandomWalkEqOracle(alphabet, sul, num_steps=500, reset_prob=0.15, reset_after_cex=True) eq_oracle = RandomWordEqOracle(alphabet, sul, num_walks=500, min_walk_len=8, max_walk_len=20) @@ -251,14 +251,14 @@ def random_mdp_example(num_states, input_len, num_outputs, n_c=20, n_resample=10 :param max_rounds: maximum number of learning rounds :return: learned MDP """ - from aalpy.SULs import MdpSUL + from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWalkEqOracle from aalpy.learning_algs import run_stochastic_Lstar from aalpy.utils import generate_random_mdp mdp = generate_random_mdp(num_states, input_len, num_outputs) input_alphabet = mdp.get_input_alphabet() - sul = MdpSUL(mdp) + sul = AutomatonSUL(mdp) eq_oracle = RandomWalkEqOracle(input_alphabet, sul=sul, num_steps=5000, reset_prob=0.11, reset_after_cex=False) @@ -342,7 +342,7 @@ def onfsm_mealy_paper_example(): :return: learned ONFSM """ - from aalpy.SULs import OnfsmSUL + from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWordEqOracle from aalpy.learning_algs import run_non_det_Lstar from aalpy.utils import get_benchmark_ONFSM @@ -350,7 +350,7 @@ def onfsm_mealy_paper_example(): onfsm = get_benchmark_ONFSM() alphabet = onfsm.get_input_alphabet() - sul = OnfsmSUL(onfsm) + sul = AutomatonSUL(onfsm) eq_oracle = RandomWordEqOracle(alphabet, sul, num_walks=500, min_walk_len=5, max_walk_len=12) learned_onfsm = run_non_det_Lstar(alphabet, sul, eq_oracle, n_sampling=10, print_level=2) @@ -372,7 +372,7 @@ def multi_client_mqtt_example(): from aalpy.base import SUL from aalpy.oracles import RandomWalkEqOracle from aalpy.learning_algs import run_abstracted_ONFSM_Lstar - from aalpy.SULs import MealySUL + from aalpy.SULs import AutomatonSUL from aalpy.utils import load_automaton_from_file class Multi_Client_MQTT_Mapper(SUL): @@ -381,7 +381,7 @@ def __init__(self): five_clients_mqtt_mealy = load_automaton_from_file('DotModels/five_clients_mqtt_abstracted_onfsm.dot', automaton_type='mealy') - self.five_client_mqtt = MealySUL(five_clients_mqtt_mealy) + self.five_client_mqtt = AutomatonSUL(five_clients_mqtt_mealy) self.connected_clients = set() self.subscribed_clients = set() @@ -469,7 +469,7 @@ def abstracted_onfsm_example(): :return: learned abstracted ONFSM """ - from aalpy.SULs import OnfsmSUL + from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWordEqOracle from aalpy.learning_algs import run_abstracted_ONFSM_Lstar from aalpy.utils import get_ONFSM @@ -478,7 +478,7 @@ def abstracted_onfsm_example(): alphabet = onfsm.get_input_alphabet() - sul = OnfsmSUL(onfsm) + sul = AutomatonSUL(onfsm) eq_oracle = RandomWordEqOracle(alphabet, sul, num_walks=500, min_walk_len=4, max_walk_len=8, reset_after_cex=True) abstraction_mapping = {0: 0, 'O': 0} @@ -496,14 +496,14 @@ def faulty_coffee_machine_mdp_example(automaton_type='mdp'): :automaton_type either mdp or smm :return learned MDP """ - from aalpy.SULs import MdpSUL + from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWalkEqOracle from aalpy.learning_algs import run_stochastic_Lstar from aalpy.utils import get_faulty_coffee_machine_MDP mdp = get_faulty_coffee_machine_MDP() input_alphabet = mdp.get_input_alphabet() - sul = MdpSUL(mdp) + sul = AutomatonSUL(mdp) eq_oracle = RandomWalkEqOracle(input_alphabet, sul=sul, num_steps=500, reset_prob=0.11, reset_after_cex=False) @@ -521,14 +521,14 @@ def weird_coffee_machine_mdp_example(): Learning faulty coffee machine that can be found in Chapter 5 and Chapter 7 of Martin's Tappler PhD thesis. :return learned MDP """ - from aalpy.SULs import MdpSUL + from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWordEqOracle from aalpy.learning_algs import run_stochastic_Lstar from aalpy.utils import get_weird_coffee_machine_MDP mdp = get_weird_coffee_machine_MDP() input_alphabet = mdp.get_input_alphabet() - sul = MdpSUL(mdp) + sul = AutomatonSUL(mdp) eq_oracle = RandomWordEqOracle(input_alphabet, sul=sul, num_walks=2000, min_walk_len=4, max_walk_len=10, reset_after_cex=True) @@ -559,7 +559,7 @@ def benchmark_stochastic_example(example, automaton_type='smm', n_c=20, n_resamp :return: learned SMM """ - from aalpy.SULs import MdpSUL + from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWalkEqOracle, RandomWordEqOracle from aalpy.learning_algs import run_stochastic_Lstar from aalpy.utils import load_automaton_from_file @@ -568,7 +568,7 @@ def benchmark_stochastic_example(example, automaton_type='smm', n_c=20, n_resamp mdp = load_automaton_from_file(f'./DotModels/MDPs/{example}.dot', automaton_type='mdp') input_alphabet = mdp.get_input_alphabet() - sul = MdpSUL(mdp) + sul = AutomatonSUL(mdp) eq_oracle = RandomWordEqOracle(input_alphabet, sul, num_walks=100, min_walk_len=5, max_walk_len=15, reset_after_cex=True) eq_oracle = RandomWalkEqOracle(input_alphabet, sul=sul, num_steps=2000, reset_prob=0.25, @@ -592,17 +592,13 @@ def custom_stochastic_example(stochastic_machine, learning_type='smm', min_round :param max_rounds: maximum number of learning rounds :return: learned model """ - from aalpy.SULs import MdpSUL, StochasticMealySUL - from aalpy.automata import Mdp + from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWordEqOracle from aalpy.learning_algs import run_stochastic_Lstar input_al = stochastic_machine.get_input_alphabet() - if isinstance(stochastic_machine, Mdp): - sul = MdpSUL(stochastic_machine) - else: - sul = StochasticMealySUL(stochastic_machine) + sul = AutomatonSUL(stochastic_machine) eq_oracle = RandomWordEqOracle(alphabet=input_al, sul=sul, num_walks=1000, min_walk_len=10, max_walk_len=30, reset_after_cex=True) @@ -643,13 +639,13 @@ def learn_stochastic_system_and_do_model_checking(example, automaton_type='smm', def alergia_mdp_example(): - from aalpy.SULs import MdpSUL + from aalpy.SULs import AutomatonSUL from random import randint, choice from aalpy.learning_algs import run_Alergia from aalpy.utils import generate_random_mdp mdp = generate_random_mdp(5, 2, 3) - sul = MdpSUL(mdp) + sul = AutomatonSUL(mdp) inputs = mdp.get_input_alphabet() data = [] @@ -671,13 +667,13 @@ def alergia_mdp_example(): def alergia_smm_example(): - from aalpy.SULs import StochasticMealySUL + from aalpy.SULs import AutomatonSUL from random import randint, choice from aalpy.learning_algs import run_Alergia from aalpy.utils import generate_random_smm smm = generate_random_smm(5, 2, 2) - sul = StochasticMealySUL(smm) + sul = AutomatonSUL(smm) inputs = smm.get_input_alphabet() data = [] @@ -701,7 +697,7 @@ def alergia_smm_example(): def alergia_mc_example(): from os import remove - from aalpy.SULs import McSUL + from aalpy.SULs import AutomatonSUL from random import randint from aalpy.learning_algs import run_Alergia from aalpy.utils import generate_random_markov_chain @@ -710,7 +706,7 @@ def alergia_mc_example(): mc = generate_random_markov_chain(10) mc.visualize('Original') - sul = McSUL(mc) + sul = AutomatonSUL(mc) # note that this example shows writing to file just to show how tokenizer is used... # this step can ofc be skipped and lists passed to alergia @@ -762,7 +758,7 @@ def jAlergiaExample(): def active_alergia_example(example='first_grid'): from random import choice, randint - from aalpy.SULs import MdpSUL + from aalpy.SULs import AutomatonSUL from aalpy.utils import load_automaton_from_file from aalpy.learning_algs import run_active_Alergia from aalpy.learning_algs.stochastic_passive.ActiveAleriga import RandomWordSampler @@ -770,7 +766,7 @@ def active_alergia_example(example='first_grid'): mdp = load_automaton_from_file(f'./DotModels/MDPs/{example}.dot', automaton_type='mdp') input_alphabet = mdp.get_input_alphabet() - sul = MdpSUL(mdp) + sul = AutomatonSUL(mdp) data = [] for _ in range(50000): @@ -805,7 +801,7 @@ def rpni_example(): def rpni_check_model_example(): import random - from aalpy.SULs import MooreSUL + from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_RPNI from aalpy.oracles import StatePrefixEqOracle from aalpy.utils import generate_random_moore_machine, generate_random_dfa @@ -826,7 +822,7 @@ def rpni_check_model_example(): rpni_model = run_RPNI(data, automaton_type='moore', print_info=True) rpni_model.make_input_complete('sink_state') - sul = MooreSUL(model) + sul = AutomatonSUL(model) eq_oracle_2 = StatePrefixEqOracle(input_al, sul, walks_per_state=100) cex = eq_oracle_2.find_cex(rpni_model) if cex is None: @@ -869,7 +865,7 @@ def random_active_rpni_example(): from aalpy.learning_algs.deterministic_passive.active_RPNI import RandomWordSampler from aalpy.utils import generate_random_deterministic_automata from aalpy.utils.HelperFunctions import all_prefixes - from aalpy.SULs import MealySUL + from aalpy.SULs import AutomatonSUL model = generate_random_deterministic_automata('mealy', num_states=50, input_alphabet_size=3, output_alphabet_size=5) @@ -886,7 +882,7 @@ def random_active_rpni_example(): data.append((prefix, output)) sampler = RandomWordSampler(500, 5, 25) - sul = MealySUL(model) + sul = AutomatonSUL(model) active_rpni_model = run_active_RPNI(data, sul, sampler=sampler, n_iter=5, automaton_type='mealy', print_info=True) @@ -898,7 +894,7 @@ def compare_stochastic_and_non_deterministic_learning(example='first_grid'): aalpy.paths.path_to_prism = "C:/Program Files/prism-4.6/bin/prism.bat" aalpy.paths.path_to_properties = "Benchmarking/prism_eval_props/" - from aalpy.SULs import MdpSUL, OnfsmSUL + from aalpy.SULs import AutomatonSUL from aalpy.automata import StochasticMealyMachine from aalpy.automata.StochasticMealyMachine import smm_to_mdp_conversion from aalpy.learning_algs import run_stochastic_Lstar, run_non_det_Lstar @@ -911,7 +907,7 @@ def compare_stochastic_and_non_deterministic_learning(example='first_grid'): # Stochastic Learning print("Stochastic Learning") - sul = MdpSUL(mdp) + sul = AutomatonSUL(mdp) eq_oracle = RandomWordEqOracle(input_alphabet, sul, num_walks=100, min_walk_len=5, max_walk_len=15, reset_after_cex=True) stochastic_learned_model = run_stochastic_Lstar(input_alphabet=input_alphabet, eq_oracle=eq_oracle, sul=sul, @@ -919,7 +915,7 @@ def compare_stochastic_and_non_deterministic_learning(example='first_grid'): # Non Deterministic Learning print("Non-deterministic Learning") - sul = OnfsmSUL(mdp) + sul = AutomatonSUL(mdp) eq_oracle = RandomWordEqOracle(input_alphabet, sul, num_walks=100, min_walk_len=5, max_walk_len=15, reset_after_cex=True) non_det_model = run_non_det_Lstar(alphabet=input_alphabet, eq_oracle=eq_oracle, sul=sul, n_sampling=5, diff --git a/README.md b/README.md index 89731720..8e5fe422 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,7 @@ The following snippet demonstrates a short example in which an automaton is eith ```python from aalpy.utils import load_automaton_from_file, save_automaton_to_file, visualize_automaton, generate_random_dfa from aalpy.automata import Dfa -from aalpy.SULs import DfaSUL +from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWalkEqOracle from aalpy.learning_algs import run_Lstar, run_KV @@ -116,7 +116,7 @@ alphabet = random_dfa.get_input_alphabet() # loaded or randomly generated automata are considered as BLACK-BOX that is queried # learning algorithm has no knowledge about its structure # create a SUL instance for the automaton/system under learning -sul = DfaSUL(random_dfa) +sul = AutomatonSUL(random_dfa) # define the equivalence oracle eq_oracle = RandomWalkEqOracle(alphabet, sul, num_steps=5000, reset_prob=0.09) diff --git a/aalpy/SULs/__init__.py b/aalpy/SULs/__init__.py index 03fc2df7..b87b0cad 100644 --- a/aalpy/SULs/__init__.py +++ b/aalpy/SULs/__init__.py @@ -1,4 +1,4 @@ -from .AutomataSUL import DfaSUL, MealySUL, MooreSUL, MdpSUL, OnfsmSUL, StochasticMealySUL, McSUL +from .AutomataSUL import DfaSUL, MealySUL, MooreSUL, MdpSUL, OnfsmSUL, StochasticMealySUL, McSUL, AutomatonSUL from .PyMethodSUL import FunctionDecorator, PyClassSUL from .RegexSUL import RegexSUL from .TomitaSUL import TomitaSUL \ No newline at end of file diff --git a/aalpy/utils/ModelChecking.py b/aalpy/utils/ModelChecking.py index b40fd906..5e1fb9bd 100644 --- a/aalpy/utils/ModelChecking.py +++ b/aalpy/utils/ModelChecking.py @@ -7,7 +7,7 @@ from typing import Tuple, Union import aalpy.paths -from aalpy.SULs import MealySUL, DfaSUL, MooreSUL +from aalpy.SULs import AutomatonSUL from aalpy.automata import Mdp, StochasticMealyMachine, MealyMachine, Dfa, MooreMachine, MooreState, MealyState, \ DfaState from aalpy.base import DeterministicAutomaton, SUL, AutomatonState @@ -300,14 +300,13 @@ def compare_automata(aut_1: DeterministicAutomaton, aut_2: DeterministicAutomato # from aalpy.oracles import RandomWMethodEqOracle - type_map = {MooreMachine: MooreSUL, Dfa: DfaSUL, MealyMachine: MealySUL} assert set(aut_1.get_input_alphabet()) == set(aut_2.get_input_alphabet()) input_al = aut_1.get_input_alphabet() # larger automaton is used as hypothesis, as then test-cases will contain prefixes leading to states # not in smaller automaton base_automaton, test_automaton = (aut_1, aut_2) if aut_1.size < aut_2.size else (aut_2, aut_1) - base_sul = type_map[type(base_automaton)](base_automaton) + base_sul = AutomatonSUL(base_automaton) # compute prefixes for all states of the test automaton (needed for advanced eq. oracle) for state in test_automaton.states: @@ -378,10 +377,8 @@ def generate_test_cases(automaton: DeterministicAutomaton, oracle): """ from copy import deepcopy - type_map = {MooreMachine: MooreSUL, Dfa: DfaSUL, MealyMachine: MealySUL} - automaton_copy = deepcopy(automaton) - base_sul = type_map[type(automaton_copy)](automaton_copy) + base_sul = AutomatonSUL(automaton_copy) wrapped_sul = TestCaseWrapperSUL(base_sul) oracle.sul = wrapped_sul diff --git a/tests/oracles/test_baseOracle.py b/tests/oracles/test_baseOracle.py index 56e1087a..beeaae45 100644 --- a/tests/oracles/test_baseOracle.py +++ b/tests/oracles/test_baseOracle.py @@ -1,6 +1,6 @@ import unittest -from aalpy.SULs import DfaSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_Lstar from aalpy.oracles import WMethodEqOracle from aalpy.utils import generate_random_dfa @@ -27,8 +27,8 @@ def generate_dfa_suls(self, number_of_states=10, alphabet_size=10, num_accepting dfa = generate_random_dfa(number_of_states, alphabet, num_accepting_states) - learning_sul = DfaSUL(dfa) - validation_sul = DfaSUL(dfa) + learning_sul = AutomatonSUL(dfa) + validation_sul = AutomatonSUL(dfa) return learning_sul, validation_sul, alphabet diff --git a/tests/test_deterministic.py b/tests/test_deterministic.py index eb3e7a29..adcd579d 100644 --- a/tests/test_deterministic.py +++ b/tests/test_deterministic.py @@ -1,7 +1,7 @@ import random import unittest -from aalpy.SULs import DfaSUL, MealySUL, MooreSUL +from aalpy.SULs import AutomatonSUL from aalpy.automata import Dfa, MealyMachine, MooreMachine from aalpy.learning_algs import run_Lstar from aalpy.learning_algs.deterministic_passive.rpni_helper_functions import createPTA @@ -16,11 +16,6 @@ MealyMachine: load_automaton_from_file('../DotModels/Angluin_Mealy.dot', automaton_type='mealy'), MooreMachine: load_automaton_from_file('../DotModels/Angluin_Moore.dot', automaton_type='moore')} -suls = {Dfa: DfaSUL, - MealyMachine: MealySUL, - MooreMachine: MooreSUL} - - class DeterministicTest(unittest.TestCase): def prove_equivalence(self, learned_automaton): @@ -33,7 +28,7 @@ def prove_equivalence(self, learned_automaton): return False alphabet = learned_automaton.get_input_alphabet() - sul = suls[learned_automaton.__class__](correct_automaton) + sul = AutomatonSUL(correct_automaton) # + 2 for good measure self.eq_oracle = WMethodEqOracle(alphabet, sul, max_number_of_states=len(correct_automaton.states) + 2) @@ -55,7 +50,7 @@ def test_closing_strategies(self): for automata in automata_type: for closing in closing_strategies: - sul = DfaSUL(dfa) + sul = AutomatonSUL(dfa) eq_oracle = RandomWalkEqOracle(alphabet, sul, 1000) learned_dfa = run_Lstar(alphabet, sul, eq_oracle, automaton_type=automata, closing_strategy=closing, @@ -78,7 +73,7 @@ def test_suffix_closedness(self): for automata in automata_type: for s_closed in suffix_closedness: - sul = DfaSUL(angluin_example) + sul = AutomatonSUL(angluin_example) eq_oracle = RandomWalkEqOracle(alphabet, sul, 500) learned_dfa = run_Lstar(alphabet, sul, eq_oracle, automaton_type=automata, @@ -102,7 +97,7 @@ def test_cex_processing(self): for automata in automata_type: for cex in cex_processing: - sul = DfaSUL(angluin_example) + sul = AutomatonSUL(angluin_example) eq_oracle = RandomWalkEqOracle(alphabet, sul, 500) learned_dfa = run_Lstar(alphabet, sul, eq_oracle, automaton_type=automata, @@ -122,7 +117,7 @@ def test_eq_oracles(self): automata_type = ['dfa', 'mealy', 'moore'] for automata in automata_type: - sul = DfaSUL(angluin_example) + sul = AutomatonSUL(angluin_example) random_walk_eq_oracle = RandomWalkEqOracle(alphabet, sul, 5000, reset_after_cex=True) state_origin_eq_oracle = StatePrefixEqOracle(alphabet, sul, walks_per_state=10, walk_len=50) @@ -140,7 +135,7 @@ def test_eq_oracles(self): state_origin_eq_oracle] for oracle in oracles: - sul = DfaSUL(angluin_example) + sul = AutomatonSUL(angluin_example) oracle.sul = sul learned_model = run_Lstar(alphabet, sul, oracle, automaton_type=automata, @@ -170,7 +165,7 @@ def test_all_configuration_combinations(self): for cex in cex_processing: for suffix in suffix_closedness: for cache in caching: - sul = DfaSUL(angluin_example) + sul = AutomatonSUL(angluin_example) random_walk_eq_oracle = RandomWalkEqOracle(alphabet, sul, 5000, reset_after_cex=True) state_origin_eq_oracle = StatePrefixEqOracle(alphabet, sul, walks_per_state=10, walk_len=50) @@ -193,7 +188,7 @@ def test_all_configuration_combinations(self): oracles.remove(cache_based_eq_oracle) for oracle in oracles: - sul = DfaSUL(angluin_example) + sul = AutomatonSUL(angluin_example) oracle.sul = sul learned_model = run_Lstar(alphabet, sul, oracle, automaton_type=automata, diff --git a/tests/test_non_deterministic.py b/tests/test_non_deterministic.py index ed6df833..0b0cb0ac 100644 --- a/tests/test_non_deterministic.py +++ b/tests/test_non_deterministic.py @@ -5,7 +5,7 @@ class NonDeterministicTest(unittest.TestCase): def test_non_det(self): - from aalpy.SULs import OnfsmSUL + from aalpy.SULs import AutomatonSUL from aalpy.oracles import RandomWordEqOracle, RandomWalkEqOracle from aalpy.learning_algs import run_non_det_Lstar from aalpy.utils import get_benchmark_ONFSM @@ -14,7 +14,7 @@ def test_non_det(self): alphabet = onfsm.get_input_alphabet() for _ in range(100): - sul = OnfsmSUL(onfsm) + sul = AutomatonSUL(onfsm) oracle = RandomWordEqOracle(alphabet, sul, num_walks=500, min_walk_len=2, max_walk_len=5) diff --git a/tests/test_stochastic.py b/tests/test_stochastic.py index 1f02fe29..99b90b1b 100644 --- a/tests/test_stochastic.py +++ b/tests/test_stochastic.py @@ -1,7 +1,7 @@ import unittest import aalpy.paths -from aalpy.SULs import StochasticMealySUL, MdpSUL +from aalpy.SULs import AutomatonSUL from aalpy.learning_algs import run_stochastic_Lstar from aalpy.oracles import RandomWalkEqOracle from aalpy.utils import load_automaton_from_file @@ -39,7 +39,7 @@ def test_learning_based_on_accuracy_based_stopping(self): for cex in cex_processing: for sample_cex in samples_cex_strategy: - sul = StochasticMealySUL(mdp) if aut_type == 'smm' else MdpSUL(mdp) + sul = AutomatonSUL(mdp) eq_oracle = RandomWalkEqOracle(input_alphabet, sul=sul, num_steps=200, reset_prob=0.25, diff --git a/tests/test_wmethod_oracle.py b/tests/test_wmethod_oracle.py index 5a719218..83d14288 100644 --- a/tests/test_wmethod_oracle.py +++ b/tests/test_wmethod_oracle.py @@ -4,7 +4,7 @@ from aalpy.automata import MooreMachine, MooreState from aalpy.learning_algs import run_Lstar from aalpy.oracles.WMethodEqOracle import WMethodEqOracle - from aalpy.SULs import MooreSUL + from aalpy.SULs import AutomatonSUL from aalpy.utils import visualize_automaton except ImportError: import sys @@ -18,7 +18,7 @@ from aalpy.automata import MooreMachine, MooreState from aalpy.learning_algs import run_Lstar from aalpy.oracles.WMethodEqOracle import WMethodEqOracle - from aalpy.SULs import MooreSUL + from aalpy.SULs import AutomatonSUL from aalpy.utils import visualize_automaton @@ -83,7 +83,7 @@ def test_wmethod_oracle(self): assert len(hyp.states) == 2 alphabet = real.get_input_alphabet() oracle = WMethodEqOracle( - alphabet, MooreSUL(real), len(real.states) + 1, shuffle_test_set=False + alphabet, AutomatonSUL(real), len(real.states) + 1, shuffle_test_set=False ) cex = oracle.find_cex(hyp) assert cex is not None, "Expected a counterexample, but got None" @@ -99,9 +99,9 @@ def test_wmethod_oracle_with_lstar(self): assert len(hyp.states) == 2 alphabet = real.get_input_alphabet() oracle = WMethodEqOracle( - alphabet, MooreSUL(real), len(real.states) + 1, shuffle_test_set=False + alphabet, AutomatonSUL(real), len(real.states) + 1, shuffle_test_set=False ) - lstar_hyp = run_Lstar(alphabet, MooreSUL(real), oracle, "moore") + lstar_hyp = run_Lstar(alphabet, AutomatonSUL(real), oracle, "moore") # print(lstar_hyp) # visualize_automaton(lstar_hyp) assert ( From 74ab0978750d3355e23528b364b880338888481b Mon Sep 17 00:00:00 2001 From: zwergziege Date: Fri, 10 Nov 2023 12:13:50 +0100 Subject: [PATCH 3/7] restore original behavior for different SULs --- aalpy/SULs/AutomataSUL.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aalpy/SULs/AutomataSUL.py b/aalpy/SULs/AutomataSUL.py index 3ee6389e..81207367 100644 --- a/aalpy/SULs/AutomataSUL.py +++ b/aalpy/SULs/AutomataSUL.py @@ -17,7 +17,7 @@ def post(self): def query(self, word: tuple) -> list: output = super().query(word) - if isinstance(self.automaton, (MooreMachine, Dfa, MarkovChain, Mdp)): + if isinstance(self.automaton, (MarkovChain, Mdp)): output.insert(0, self.automaton.initial_state.output) return output From 9710ab79514311fa045fcaad923a81b7f8de1261 Mon Sep 17 00:00:00 2001 From: Edi Muskardin Date: Fri, 10 Nov 2023 13:48:52 +0100 Subject: [PATCH 4/7] update AutomataSUL.py --- aalpy/SULs/AutomataSUL.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/aalpy/SULs/AutomataSUL.py b/aalpy/SULs/AutomataSUL.py index 81207367..947d3dad 100644 --- a/aalpy/SULs/AutomataSUL.py +++ b/aalpy/SULs/AutomataSUL.py @@ -1,13 +1,16 @@ -from aalpy.automata import MooreMachine, Dfa, MarkovChain, Mdp from aalpy.base import SUL, Automaton +from aalpy.automata import MarkovChain, Mdp + class AutomatonSUL(SUL): - def __init__(self, automaton : Automaton): + def __init__(self, automaton: Automaton): super().__init__() - self.automaton : Automaton = automaton + self.automaton: Automaton = automaton def pre(self): self.automaton.reset_to_initial() + if isinstance(self.automaton, (MarkovChain, Mdp)): + return self.automaton.initial_state.output def step(self, letter=None): return self.automaton.step(letter) @@ -21,4 +24,5 @@ def query(self, word: tuple) -> list: output.insert(0, self.automaton.initial_state.output) return output -MealySUL = OnfsmSUL = StochasticMealySUL = DfaSUL = MooreSUL = MdpSUL = McSUL = AutomatonSUL \ No newline at end of file + +MealySUL = OnfsmSUL = StochasticMealySUL = DfaSUL = MooreSUL = MdpSUL = McSUL = AutomatonSUL From b8c6f1469cbc5206bf4c0bfce9bbf6ad925c9097 Mon Sep 17 00:00:00 2001 From: Edi Muskardin Date: Fri, 10 Nov 2023 14:06:50 +0100 Subject: [PATCH 5/7] make AutomataSUL.py consistent with MdpSUL and McSUL --- Examples.py | 1 + aalpy/SULs/AutomataSUL.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Examples.py b/Examples.py index c49ed0f0..e3321576 100644 --- a/Examples.py +++ b/Examples.py @@ -259,6 +259,7 @@ def random_mdp_example(num_states, input_len, num_outputs, n_c=20, n_resample=10 mdp = generate_random_mdp(num_states, input_len, num_outputs) input_alphabet = mdp.get_input_alphabet() sul = AutomatonSUL(mdp) + eq_oracle = RandomWalkEqOracle(input_alphabet, sul=sul, num_steps=5000, reset_prob=0.11, reset_after_cex=False) diff --git a/aalpy/SULs/AutomataSUL.py b/aalpy/SULs/AutomataSUL.py index 947d3dad..3a1ff764 100644 --- a/aalpy/SULs/AutomataSUL.py +++ b/aalpy/SULs/AutomataSUL.py @@ -6,10 +6,11 @@ class AutomatonSUL(SUL): def __init__(self, automaton: Automaton): super().__init__() self.automaton: Automaton = automaton + self.include_initial_output = isinstance(self.automaton, (MarkovChain, Mdp)) def pre(self): self.automaton.reset_to_initial() - if isinstance(self.automaton, (MarkovChain, Mdp)): + if self.include_initial_output: return self.automaton.initial_state.output def step(self, letter=None): @@ -19,10 +20,12 @@ def post(self): pass def query(self, word: tuple) -> list: - output = super().query(word) - if isinstance(self.automaton, (MarkovChain, Mdp)): - output.insert(0, self.automaton.initial_state.output) - return output + initial_output = self.pre() + out = [self.step(i) for i in word] + if initial_output: + out.insert(0, initial_output) + self.post() + return out MealySUL = OnfsmSUL = StochasticMealySUL = DfaSUL = MooreSUL = MdpSUL = McSUL = AutomatonSUL From 55cc688b5652f1beae5e32f4aea67cc399fc02aa Mon Sep 17 00:00:00 2001 From: Edi Muskardin Date: Wed, 13 Dec 2023 13:55:51 +0100 Subject: [PATCH 6/7] remove output from pre() for MC and MDPs... --- Examples.py | 8 ++++++-- aalpy/SULs/AutomataSUL.py | 32 ++------------------------------ aalpy/SULs/__init__.py | 4 ++-- aalpy/automata/MarkovChain.py | 11 ++++++----- tests/test_charSet.py | 33 ++++++++++++++++----------------- tests/test_deterministic.py | 18 +++--------------- 6 files changed, 35 insertions(+), 71 deletions(-) diff --git a/Examples.py b/Examples.py index d113964e..19ce8662 100644 --- a/Examples.py +++ b/Examples.py @@ -646,6 +646,7 @@ def alergia_mdp_example(): from aalpy.utils import generate_random_mdp mdp = generate_random_mdp(5, 2, 3) + initial_output = mdp.initial_state.output sul = AutomatonSUL(mdp) inputs = mdp.get_input_alphabet() @@ -653,7 +654,8 @@ def alergia_mdp_example(): for _ in range(100000): str_len = randint(5, 12) # add the initial output - seq = [sul.pre()] + sul.pre() + seq = [initial_output] for _ in range(str_len): i = choice(inputs) o = sul.step(i) @@ -705,6 +707,7 @@ def alergia_mc_example(): from aalpy.utils import CharacterTokenizer mc = generate_random_markov_chain(10) + initial_output = mc.initial_state.output mc.visualize('Original') sul = AutomatonSUL(mc) @@ -713,8 +716,9 @@ def alergia_mc_example(): # this step can ofc be skipped and lists passed to alergia data = [] for _ in range(20000): + sul.pre() str_len = randint(4, 12) - seq = [f'{sul.pre()}'] + seq = [f'{initial_output}'] for _ in range(str_len): o = sul.step() seq.append(f'{o}') diff --git a/aalpy/SULs/AutomataSUL.py b/aalpy/SULs/AutomataSUL.py index 68c33778..0337b158 100644 --- a/aalpy/SULs/AutomataSUL.py +++ b/aalpy/SULs/AutomataSUL.py @@ -1,19 +1,14 @@ -from aalpy.base import SUL, Automaton -from aalpy.automata import MarkovChain, Mdp +from aalpy.base import Automaton from aalpy.base import SUL -from aalpy.automata import Dfa, MealyMachine, MooreMachine, Onfsm, Mdp, StochasticMealyMachine, MarkovChain, Sevpa class AutomatonSUL(SUL): def __init__(self, automaton: Automaton): super().__init__() self.automaton: Automaton = automaton - self.include_initial_output = isinstance(self.automaton, (MarkovChain, Mdp)) def pre(self): self.automaton.reset_to_initial() - if self.include_initial_output: - return self.automaton.initial_state.output def step(self, letter=None): return self.automaton.step(letter) @@ -21,28 +16,5 @@ def step(self, letter=None): def post(self): pass - def query(self, word: tuple) -> list: - initial_output = self.pre() - out = [self.step(i) for i in word] - if initial_output: - out.insert(0, initial_output) - self.post() - return out - -MealySUL = OnfsmSUL = StochasticMealySUL = DfaSUL = MooreSUL = MdpSUL = McSUL = AutomatonSUL - - -class SevpaSUL(SUL): - def __init__(self, sevpa: Sevpa): - super().__init__() - self.sevpa = sevpa - - def pre(self): - self.sevpa.reset_to_initial() - - def post(self): - pass - - def step(self, letter): - return self.sevpa.step(letter) +MealySUL = OnfsmSUL = StochasticMealySUL = DfaSUL = MooreSUL = MdpSUL = McSUL = SevpaSUL = AutomatonSUL diff --git a/aalpy/SULs/__init__.py b/aalpy/SULs/__init__.py index 03fc2df7..406c0853 100644 --- a/aalpy/SULs/__init__.py +++ b/aalpy/SULs/__init__.py @@ -1,4 +1,4 @@ -from .AutomataSUL import DfaSUL, MealySUL, MooreSUL, MdpSUL, OnfsmSUL, StochasticMealySUL, McSUL +from .AutomataSUL import * from .PyMethodSUL import FunctionDecorator, PyClassSUL from .RegexSUL import RegexSUL -from .TomitaSUL import TomitaSUL \ No newline at end of file +from .TomitaSUL import TomitaSUL diff --git a/aalpy/automata/MarkovChain.py b/aalpy/automata/MarkovChain.py index ef94a9f2..1280edd2 100644 --- a/aalpy/automata/MarkovChain.py +++ b/aalpy/automata/MarkovChain.py @@ -8,9 +8,10 @@ class McState(AutomatonState, Generic[OutputType]): def __init__(self, state_id, output): super().__init__(state_id) - self.output : OutputType = output + self.output: OutputType = output # transitions is a list of tuples (Node(output), probability) - self.transitions : List[Tuple[McState, float]] = list() + self.transitions: List[Tuple[McState, float]] = list() + class MarkovChain(Automaton[McState[OutputType]]): """Markov Decision Process.""" @@ -62,8 +63,8 @@ def step_to(self, input): return None @staticmethod - def from_state_setup(state_setup : dict, **kwargs): - raise NotImplementedError() # TODO implement + def from_state_setup(state_setup: dict, **kwargs): + raise NotImplementedError() # TODO implement def to_state_setup(self): - raise NotImplementedError() # TODO implement \ No newline at end of file + raise NotImplementedError() # TODO implement diff --git a/tests/test_charSet.py b/tests/test_charSet.py index fc5f617f..1b8c1cf6 100644 --- a/tests/test_charSet.py +++ b/tests/test_charSet.py @@ -8,23 +8,24 @@ class TestCharSet(unittest.TestCase): def get_test_automata(self): return {"angluin_dfa": get_Angluin_dfa(), - "angluin_mealy": load_automaton_from_file('../DotModels/Angluin_Mealy.dot', automaton_type='mealy'), - "angluin_moore": load_automaton_from_file('../DotModels/Angluin_Moore.dot', automaton_type='moore'), - "mqtt": load_automaton_from_file('../DotModels/MQTT/emqtt__two_client_will_retain.dot', - automaton_type='mealy'), - "openssl": load_automaton_from_file('../DotModels/TLS/OpenSSL_1.0.2_server_regular.dot', - automaton_type='mealy'), - "tcp_server": load_automaton_from_file('../DotModels/TCP/TCP_Linux_Server.dot', - automaton_type='mealy')} + "angluin_mealy": load_automaton_from_file('../DotModels/Angluin_Mealy.dot', automaton_type='mealy'), + "angluin_moore": load_automaton_from_file('../DotModels/Angluin_Moore.dot', automaton_type='moore'), + "mqtt": load_automaton_from_file('../DotModels/MQTT/emqtt__two_client_will_retain.dot', + automaton_type='mealy'), + "openssl": load_automaton_from_file('../DotModels/TLS/OpenSSL_1.0.2_server_regular.dot', + automaton_type='mealy'), + "tcp_server": load_automaton_from_file('../DotModels/TCP/TCP_Linux_Server.dot', + automaton_type='mealy')} def test_can_differentiate(self): automata = self.get_test_automata() - for init_with_alphabet in [True,False]: - for (online_suffix_closure,split_all_blocks) in [(False, False),(False,True),(True,False),(True,True)]: + for init_with_alphabet in [True, False]: + for (online_suffix_closure, split_all_blocks) in [(False, False), (False, True), (True, False), + (True, True)]: for test_aut_name in automata: print(f"Testing with {test_aut_name}") test_aut = automata[test_aut_name] - char_set_init = list(map(lambda input: tuple([input]),test_aut.get_input_alphabet())) \ + char_set_init = list(map(lambda input: tuple([input]), test_aut.get_input_alphabet())) \ if init_with_alphabet else None if "dfa" in test_aut_name or "moore" in test_aut_name: char_set_init = [] if char_set_init is None else char_set_init @@ -37,7 +38,7 @@ def test_can_differentiate(self): for s in test_aut.states: responses_from_s = [] for c in char_set: - responses_from_s.append(tuple(test_aut.compute_output_seq(s,c))) + responses_from_s.append(tuple(test_aut.compute_output_seq(s, c))) all_responses.add(tuple(responses_from_s)) # every state must have a unique response to the whole characterization set @@ -45,13 +46,13 @@ def test_can_differentiate(self): def test_suffix_closed(self): automata = self.get_test_automata() - for init_with_alphabet in [True,False]: + for init_with_alphabet in [True, False]: online_suffix_closure = True - for split_all_blocks in [True,False]: + for split_all_blocks in [True, False]: for test_aut_name in automata: print(f"Testing with {test_aut_name}") test_aut = automata[test_aut_name] - char_set_init = list(map(lambda input: tuple([input]),test_aut.get_input_alphabet())) \ + char_set_init = list(map(lambda input: tuple([input]), test_aut.get_input_alphabet())) \ if init_with_alphabet else None if "dfa" in test_aut_name or "moore" in test_aut_name: char_set_init = [] if char_set_init is None else char_set_init @@ -65,5 +66,3 @@ def test_suffix_closed(self): if suffix not in char_set: print(suffix) assert suffix in char_set - - diff --git a/tests/test_deterministic.py b/tests/test_deterministic.py index adcd579d..4ace6d01 100644 --- a/tests/test_deterministic.py +++ b/tests/test_deterministic.py @@ -1,21 +1,19 @@ -import random import unittest from aalpy.SULs import AutomatonSUL from aalpy.automata import Dfa, MealyMachine, MooreMachine from aalpy.learning_algs import run_Lstar -from aalpy.learning_algs.deterministic_passive.rpni_helper_functions import createPTA from aalpy.oracles import WMethodEqOracle, RandomWalkEqOracle, StatePrefixEqOracle, TransitionFocusOracle, \ RandomWMethodEqOracle, BreadthFirstExplorationEqOracle, RandomWordEqOracle, CacheBasedEqOracle, \ KWayStateCoverageEqOracle -from aalpy.utils import get_Angluin_dfa, load_automaton_from_file, save_automaton_to_file +from aalpy.utils import get_Angluin_dfa, load_automaton_from_file from aalpy.utils.ModelChecking import bisimilar -from aalpy.utils.AutomatonGenerators import generate_random_deterministic_automata correct_automata = {Dfa: get_Angluin_dfa(), MealyMachine: load_automaton_from_file('../DotModels/Angluin_Mealy.dot', automaton_type='mealy'), MooreMachine: load_automaton_from_file('../DotModels/Angluin_Moore.dot', automaton_type='moore')} + class DeterministicTest(unittest.TestCase): def prove_equivalence(self, learned_automaton): @@ -27,17 +25,7 @@ def prove_equivalence(self, learned_automaton): print(len(learned_automaton.states), len(correct_automaton.states)) return False - alphabet = learned_automaton.get_input_alphabet() - sul = AutomatonSUL(correct_automaton) - - # + 2 for good measure - self.eq_oracle = WMethodEqOracle(alphabet, sul, max_number_of_states=len(correct_automaton.states) + 2) - - cex = self.eq_oracle.find_cex(learned_automaton) - - if cex: - return False - return True + return bisimilar(correct_automaton, learned_automaton) def test_closing_strategies(self): From 50bf4ef6dc30f88bbe4d5f1714324e7e138e6392 Mon Sep 17 00:00:00 2001 From: Edi Muskardin Date: Wed, 13 Dec 2023 15:13:06 +0100 Subject: [PATCH 7/7] update arithmetic expression example as eval is weird and allows 1 ++++ 1 --- Examples.py | 21 +++++++++++++++------ docs/arithmeticSevpa.PNG | Bin 49515 -> 25238 bytes 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/Examples.py b/Examples.py index 19ce8662..ac90781f 100644 --- a/Examples.py +++ b/Examples.py @@ -971,6 +971,7 @@ def arithmetic_expression_sevpa_learning(): from aalpy.oracles import RandomWordEqOracle from aalpy.learning_algs import run_KV import warnings + import ast warnings.filterwarnings("ignore") class ArithmeticSUL(SUL): @@ -986,22 +987,30 @@ def post(self): def step(self, letter): if letter: - self.string_under_test += ' ' + letter + self.string_under_test += ' ' + letter if len(self.string_under_test) > 0 else letter try: - eval(self.string_under_test) - return True - except (SyntaxError, TypeError): + # Parse the expression using ast + parsed_expr = ast.parse(self.string_under_test, mode='eval') + # Check if the parsed expression is a valid arithmetic expression + is_valid = all(isinstance(node, (ast.Expression, ast.BinOp, ast.UnaryOp, ast.Num, ast.Name, ast.Load)) + or isinstance(node, ast.operator) or isinstance(node, ast.expr_context) + or (isinstance(node, ast.BinOp) and isinstance(node.op, ast.operator)) + for node in ast.walk(parsed_expr)) + return is_valid + + except SyntaxError: return False sul = ArithmeticSUL() alphabet = SevpaAlphabet(internal_alphabet=['1', '+'], call_alphabet=['('], return_alphabet=[')']) - eq_oracle = RandomWordEqOracle(alphabet.get_merged_alphabet(), sul, min_walk_len=5, - max_walk_len=20, num_walks=20000) + eq_oracle = RandomWordEqOracle(alphabet.get_merged_alphabet(), sul, min_walk_len=2, + max_walk_len=10, num_walks=2000) learned_model = run_KV(alphabet, sul, eq_oracle, automaton_type='vpa') + learned_model.visualize() diff --git a/docs/arithmeticSevpa.PNG b/docs/arithmeticSevpa.PNG index bb1ef6984a69adf09f25f8b2985602bd2da8ad66..2e86fdd0953e03b12cddb8b4a5c6aeb6c1229ed4 100644 GIT binary patch literal 25238 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 vw^C!0nygbQ!l>#IQV}6>BEXn literal 49515 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%0AOk7iu} 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 HM)3auNiS@P