Created
November 8, 2024 08:51
-
-
Save emuskardin/28e543f942f8b720b75ef7c57aac9199 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import random | |
from abc import ABC, abstractmethod | |
from aalpy import SUL, RandomWordEqOracle, run_Lstar, run_RPNI, bisimilar, compare_automata | |
class AbstractVendingMachine(ABC): | |
@abstractmethod | |
def add_coin(self, coin): | |
pass | |
@abstractmethod | |
def push_button(self, order): | |
pass | |
@abstractmethod | |
def reset(self): | |
pass | |
# for all intents and purposes the behaviour of this class is hidden from us | |
class VendingMachine(AbstractVendingMachine): | |
def __init__(self, max_coins=5): | |
self.coin_count = 0 | |
self.max_coins = max_coins | |
def add_coin(self, coin): | |
if self.coin_count + coin > self.max_coins: | |
return 'coin_returned' | |
self.coin_count += coin | |
return 'coin_added' | |
def push_button(self, order): | |
if order == 'coke': | |
if self.coin_count >= 2: | |
self.coin_count -= 2 | |
return 'Coke' | |
else: | |
return 'No_Action' | |
if order == 'peanuts': | |
if self.coin_count >= 3: | |
self.coin_count -= 3 | |
return 'Peanuts' | |
else: | |
return 'No_Action' | |
if order == 'water': | |
if self.coin_count >= 1: | |
self.coin_count -= 1 | |
return 'Water' | |
else: | |
return 'No_Action' | |
def reset(self): | |
self.coin_count = 0 | |
# for all intents and purposes the behaviour of this class is hidden from us | |
class VendingMachineMutant1(AbstractVendingMachine): | |
def __init__(self, max_coins=5): | |
self.coin_count = 0 | |
self.max_coins = max_coins | |
def add_coin(self, coin): | |
if self.coin_count + coin > self.max_coins: | |
return 'coin_returned' | |
self.coin_count += coin | |
return 'coin_added' | |
def push_button(self, order): | |
if order == 'coke': | |
if self.coin_count >= 2: | |
self.coin_count -= 2 | |
return 'Coke' | |
else: | |
return 'No_Action' | |
if order == 'peanuts': | |
if self.coin_count >= 3: | |
self.coin_count -= 3 | |
return 'Peanuts' | |
else: | |
return 'No_Action' | |
if order == 'water': | |
if self.coin_count >= 1: | |
return 'Water' | |
else: | |
return 'No_Action' | |
def reset(self): | |
self.coin_count = 0 | |
# for all intents and purposes the behaviour of this class is hidden from us | |
class VendingMachineMutant2(AbstractVendingMachine): | |
def __init__(self, max_coins=5): | |
self.coin_count = 0 | |
self.max_coins = max_coins | |
def add_coin(self, coin): | |
if self.coin_count + coin > self.max_coins: | |
return 'coin_returned' | |
self.coin_count += coin | |
return 'coin_added' | |
def push_button(self, order): | |
if order == 'coke': | |
if self.coin_count >= 2: | |
self.coin_count -= 2 | |
return 'Coke' | |
else: | |
return 'No_Action' | |
if order == 'peanuts': | |
if self.coin_count >= 3: | |
self.coin_count -= 1 | |
return 'Peanuts' | |
else: | |
return 'No_Action' | |
if order == 'water': | |
if self.coin_count >= 1: | |
self.coin_count -= 1 | |
return 'Water' | |
else: | |
return 'No_Action' | |
def reset(self): | |
self.coin_count = 0 | |
class VendingMachineSUL(SUL): | |
def __init__(self, vending_machine): | |
super().__init__() | |
self.vm = vending_machine | |
def pre(self): | |
self.vm.reset() | |
def post(self): | |
pass | |
def step(self, letter): | |
if letter[0] == 'coin': | |
return self.vm.add_coin(letter[1]) | |
if letter[0] == 'order': | |
return self.vm.push_button(letter[1]) | |
raise RuntimeError('Invalid input') | |
def active_learning(vending_machine, input_alphabet, visualize_model=True): | |
# define the input alphabet | |
# wrap the vending machine in SUL interface | |
sul = VendingMachineSUL(vending_machine) | |
# define the equivalence oracle used for conformance testing during learning | |
eq_oracle = RandomWordEqOracle(input_alphabet, sul, num_walks=1000, min_walk_len=3, max_walk_len=12) | |
# eq_oracle = RandomWMethodEqOracle(input_alphabet, sul, walks_per_state=10, walk_len=8) | |
# learn the mealy machine capturing the input-output behaviour of the vending machine | |
learned_model = run_Lstar(input_alphabet, sul, eq_oracle, 'mealy') | |
# visualize the model | |
if visualize_model: | |
learned_model.visualize("LearnedVendingMachine") | |
return learned_model | |
def passive_learning(vending_machine, input_alphabet, visualize_model=True): | |
# generate some data for passive automata learning | |
input_alphabet = [('coin', 1), ('coin', 2), ('order', 'coke'), ('order', 'water'), | |
('order', 'peanuts')] | |
sul = VendingMachineSUL(vending_machine) | |
# generate 200 random sequences of SUL's input-output behaviour | |
data = [] | |
for _ in range(200): | |
# generate random input sequance | |
inputs = random.choices(input_alphabet, k=random.randint(2, 10)) | |
# get an output | |
output = sul.query(inputs)[-1] | |
# add to the dataset | |
data.append((inputs, output)) | |
# print 10 data entries | |
for d in data[:10]: | |
print(d) | |
learned_model = run_RPNI(data, automaton_type='moore') | |
if learned_model: | |
learned_model.visualize("PassiveLearning") | |
return learned_model | |
def learning_based_testing(vm1, vm2, input_alphabet): | |
learned_model_1 = active_learning(vm1, input_alphabet, visualize_model=False) | |
learned_model_2 = active_learning(vm2, input_alphabet, visualize_model=False) | |
# find a shortest differance between models | |
shortest_counterexample = bisimilar(learned_model_1, learned_model_2, return_cex=True) | |
# if a shortest counterexample is found, print the differences | |
if shortest_counterexample: | |
shortest_counterexample = tuple(shortest_counterexample) | |
output_vm1 = learned_model_1.execute_sequence(learned_model_1.initial_state, shortest_counterexample) | |
output_vm2 = learned_model_2.execute_sequence(learned_model_2.initial_state, shortest_counterexample) | |
print('CEX:', shortest_counterexample) | |
print(f'Vending Machine 1: {output_vm1}') | |
print(f'Vending Machine 2: {output_vm2}') | |
# if we want more counterexamples we can use following function | |
additional_counterexamples = compare_automata(learned_model_1, learned_model_2, num_cex=10) | |
# use the same input alphabet and SUL implementation from the previous example | |
input_alphabet = [('coin', 1), ('coin', 2), ('order', 'coke'), ('order', 'water'), | |
('order', 'peanuts')] | |
print('Active learning example') | |
vending_machine = VendingMachine(max_coins=5) | |
active_learning(vending_machine, input_alphabet, visualize_model=True) | |
print('Passive learning example') | |
vending_machine = VendingMachine(max_coins=5) | |
passive_learning(vending_machine, input_alphabet, visualize_model=True) | |
print('Learning-based testing') | |
# initialize 2 different vending machines | |
vm1 = VendingMachine(max_coins=5) | |
vm2 = VendingMachineMutant2(max_coins=5) | |
learning_based_testing(vm1, vm2, input_alphabet) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment