Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
155 commits
Select commit Hold shift + click to select a range
f87fb7e
implemented first version of passive obs. non det. moore machine lear…
zwergziege Apr 5, 2023
ba95130
fixed bugs. baseline is working
zwergziege Apr 6, 2023
c602b8c
simplified structure of the algorithm
zwergziege Apr 6, 2023
8a1e80d
fixed a typing mistake
zwergziege Apr 7, 2023
fc0eb31
added observation count to internal node structure
zwergziege May 27, 2023
d730f73
added auxiliary method for pruning RPNI nodes based on observation count
zwergziege May 27, 2023
3c244f2
added more info to plots
zwergziege May 27, 2023
e6141e3
externalized debug stuff in non-det passive
zwergziege May 27, 2023
553917c
Added an alteration of the GSM framework code for stochastic Mealy ma…
zwergziege May 28, 2023
7dd29c9
bugfixes
zwergziege May 29, 2023
9a8cf6c
changed GSM automaton output from stochastic mealy to whatever was sp…
zwergziege May 30, 2023
ad0497c
fixed debug output
zwergziege May 30, 2023
d73b667
slight optimizations
zwergziege Jun 2, 2023
409f8dc
removed unnecessary debug info
zwergziege Jun 2, 2023
b42b8f3
changed visualization to incorporate automaton type
zwergziege Jun 2, 2023
3125c74
made Node picklable
zwergziege Jun 2, 2023
007a1d2
added possibility to pass information down the merge chain
zwergziege Jun 2, 2023
b4614c3
added an experimental merge cond for nondet
zwergziege Jun 2, 2023
ca9b450
added checks for literal types. maybe should switch to enums...
zwergziege Jun 2, 2023
cc65d84
moved transition count update to partitioning
zwergziege Jun 2, 2023
16982dc
increased debug level for visualizing models
zwergziege Jun 2, 2023
6d7121f
minor simplifications in GSM
zwergziege Jun 3, 2023
a6eaffa
Switched to shortlex order of nodes
zwergziege Jun 3, 2023
4e9f79e
removed update_count option in favor of compatibility_behavior
zwergziege Jun 3, 2023
21c84b8
improvements for visualization and automaton conversion of internal f…
zwergziege Jun 4, 2023
5d939f6
extracted comparison of futures into separate function and adapted pa…
zwergziege Jun 4, 2023
47bd3c8
improved get_all_nodes
zwergziege Jun 5, 2023
ba426a4
nicer shortlex implementation
zwergziege Jun 13, 2023
33c02fa
converted io pairs to strings for comparing prefixes
zwergziege Jun 27, 2023
ad0d216
added option to GSM for computing the score on the partition
zwergziege Jun 27, 2023
d8a0a1e
nits and todos
zwergziege Jun 27, 2023
ff95e85
extended GSM internal structure (custom plotting, safe cmp, named tuple)
zwergziege Jul 26, 2023
10951bb
added option to specify the graphviz engine for visualization of GSM …
zwergziege Jul 26, 2023
4bc9d01
Merge branch 'master' into statemerging-dev
zwergziege Sep 11, 2023
ff2a32f
Merge branch 'master' into statemerging-dev
zwergziege Sep 11, 2023
443789e
nits
zwergziege Sep 25, 2023
f49307e
added shortcut for alergia
zwergziege Sep 25, 2023
e4405d2
Merge branch 'master' into statemerging-dev
zwergziege Sep 27, 2023
9f2b6f8
removed old code
zwergziege Sep 27, 2023
54b0a7e
removed source of nondeterminism (nodes as set)
zwergziege Sep 27, 2023
c7edd58
Changed file structure for deterministic passive
zwergziege Sep 27, 2023
c57c729
renamed stochastic passive helper file.
zwergziege Sep 27, 2023
a19bcc5
purged queue from GSM
zwergziege Oct 6, 2023
858ab0f
nits
zwergziege Oct 6, 2023
134790c
turned gsm node.transitions into regular dict
zwergziege Oct 6, 2023
e40176e
merged transition and transition count in GSM
zwergziege Oct 8, 2023
38a743e
GSM: reintroduced split functionality "compare only futures" and "use…
zwergziege Oct 8, 2023
169802d
reduce use of "if in"
zwergziege Oct 8, 2023
fc60dae
fixed gsm bug in viz
zwergziege Oct 10, 2023
395f43e
added some remarks to GSM
zwergziege Oct 10, 2023
9e4ba22
moved DebugInfo for GSM up a bit
zwergziege Nov 15, 2023
29fe55b
refactor in DebugInfo for GSM
zwergziege Nov 15, 2023
bcf6c65
added runGSM function
zwergziege Nov 15, 2023
ae7e4b4
lil comment
zwergziege Dec 11, 2023
e187292
Added support for custom node order
zwergziege Dec 11, 2023
689402a
reduced safe comp code
zwergziege Dec 22, 2023
a081d5b
Preparation for score based GSM
zwergziege Dec 22, 2023
e2e40c5
switched to deques
zwergziege Dec 22, 2023
2b309e4
extracted score functions
zwergziege Dec 22, 2023
32f3206
more prep for score functions
zwergziege Dec 22, 2023
8ab1ab3
implemented support for score functions and llh-ratio, EDSM and AIC s…
zwergziege Jan 5, 2024
09f325b
slightly more consistent naming
zwergziege Jan 5, 2024
50496d3
added depth first compatibility and option to consider all blue state…
zwergziege Jan 5, 2024
b2faf99
minor tweaks
zwergziege Jan 5, 2024
4fa8d33
replaced `CompatibilityBehavior.merge` with `local_to_global_score` a…
zwergziege Jan 8, 2024
2cfe805
consolidated info_init, local_score and global_score into ScoreCalcul…
zwergziege Jan 9, 2024
d8d662c
added score for non det learning
zwergziege Jan 9, 2024
d6b4696
Backport to python 3.6 & elimination of dependencies
zwergziege Jan 9, 2024
23e7548
Merge branch 'master' into statemerging-dev
zwergziege Jan 9, 2024
102c32f
move GSM to own folder :)
zwergziege Jan 9, 2024
40cca4a
add RPNI shortcut
zwergziege Jan 9, 2024
aa8cf3c
updated implementation of NDMooreMachine
zwergziege Jan 12, 2024
b1decdb
some work on score functions
zwergziege Jan 12, 2024
d115211
improved GSM internal visualization
zwergziege Jan 12, 2024
ba2d74c
more debug info for GSM
zwergziege Jan 12, 2024
345c8be
Added general folding method for GSM (allows definition of bisimilari…
zwergziege Jan 18, 2024
657b42f
More score / compat functions
zwergziege Jan 18, 2024
67679a4
marginally more debug output
zwergziege Jan 18, 2024
fe51c49
fixed cornercase in likelighood score
zwergziege Jan 19, 2024
9b0bf14
Fix corner case in likelihood score (this time for real?)
zwergziege Jan 30, 2024
c35fa65
Made k-tail scores more intuitive and added a few comments
zwergziege Jan 30, 2024
5196be0
refactored ScoreCalculation fields / methods for clarity (compatibili…
zwergziege Jan 30, 2024
cbb568d
nits
zwergziege Jan 30, 2024
1fd3756
remove unnecessary lambdas
zwergziege Feb 5, 2024
61279de
bit of score refactoring
zwergziege Feb 5, 2024
71c3668
Refactored ScoreCombinator and ScoreWithKTail classes
zwergziege Feb 8, 2024
0ff8b35
Added score for adding sinks with arbitrary conditions
zwergziege Feb 8, 2024
afc3eeb
fixed likelihood calculation corner case
zwergziege Mar 1, 2024
1bb8a87
fixed refactoring mistake
zwergziege Mar 1, 2024
246cc22
switch print for warning
zwergziege Mar 1, 2024
8798827
added GSM to setup.py
zwergziege Mar 1, 2024
bdd0a02
some refactoring for GSM
zwergziege Mar 1, 2024
4668e84
added option for custom stopping when folding two GSM nodes + mini do…
zwergziege Mar 4, 2024
9514fa4
add some init stuff
zwergziege Mar 18, 2024
8bfc1ce
Fixed GSM log likelihood ratio test
zwergziege Mar 18, 2024
da7daf3
Merge branch 'master' into statemerging-dev
zwergziege Mar 18, 2024
fe422fe
Merge branch 'master' into statemerging-dev
zwergziege Mar 19, 2024
4537d07
Merge branch 'master' into statemerging-dev
zwergziege Mar 19, 2024
eb71a1f
Improved try_fold function with custom enum and fixed partitioning be…
zwergziege Mar 20, 2024
724d76a
minor comment
zwergziege Apr 12, 2024
45eeffd
avoid creating a copy of the full prefix for each node
zwergziege Apr 21, 2024
e34007e
Merge branch 'master' into statemerging-dev
zwergziege Apr 24, 2024
b836b03
removed full prefix altogether
zwergziege May 13, 2024
14da47a
Merge branch 'statemerging-dev-no-explicit-prefixes' into statemergin…
zwergziege May 13, 2024
79dd592
Merge branch 'master' into statemerging-dev
zwergziege Jun 7, 2024
4f74343
removed unnecessary copying for PTA construction
zwergziege Sep 4, 2024
4a457ef
fixed breakage from removing explicit prefixes
zwergziege Sep 5, 2024
8958bdc
Merge branch 'master' into statemerging-dev
zwergziege Sep 5, 2024
bcb7e32
added type hint
zwergziege Dec 17, 2024
9432f4c
Add comment for future features
zwergziege Jan 17, 2025
80af70e
Moved data and PTA construction from `GSM.__init__` to `GSM.run`. Rem…
zwergziege Jan 23, 2025
8e0dd7e
added typehint
zwergziege Jan 23, 2025
1d2bdc2
Turned GSM debug object into local variable of `GSM.run`
zwergziege Jan 23, 2025
b5400cb
added PTA preprocessing
zwergziege Jan 24, 2025
f08dc01
add and use common dict iterator utils
zwergziege Jan 24, 2025
520ba0d
rename safe transition get of `Node` for clarity
zwergziege Jan 24, 2025
ee9be9a
added EDSM as per abbadingo
zwergziege Jan 24, 2025
32c96e3
add option for postprocessing
zwergziege Jan 24, 2025
d2cd6da
removed initial determinism check
zwergziege Jan 24, 2025
666ae04
Better default values
zwergziege Jan 27, 2025
ecb5d34
Some renaming, comments and cleanup
zwergziege Jan 27, 2025
3c674f5
added option to return debug object
zwergziege Jan 27, 2025
1ad125f
purge `comaptibility_behavior` from GSM
zwergziege Jan 27, 2025
5e66996
Redefined flag for consider_min_blue_only
zwergziege Jan 27, 2025
ab55b2b
fixed silly mistake
zwergziege Jan 28, 2025
76573df
Improved instrumentation
zwergziege Jan 28, 2025
afc97f1
minor fix, cleanup, document quirk
zwergziege Jan 28, 2025
3cbcc89
keep red states sorted
zwergziege Jan 29, 2025
12a04bf
better printing in GSM
zwergziege Jan 30, 2025
ef8674e
reduce number of comparisons
zwergziege Jan 30, 2025
e3c5e27
fixed another bug related to the implicit prefix for IOAlergia and si…
zwergziege Jan 30, 2025
e2f5c22
minor changes to instrumentation and helpers
zwergziege Jan 31, 2025
4676550
shifted order of node update and log + ensure correct access pair
zwergziege Jan 31, 2025
9f6b733
Rename GSM helpers file to Node
zwergziege Feb 3, 2025
3e9fe11
remove oversight
zwergziege Feb 3, 2025
6fd86e1
rename join iterator to union iterator
zwergziege Feb 3, 2025
9223bcf
updated inits
zwergziege Feb 3, 2025
16ff7c5
Remove old util function
zwergziege Feb 3, 2025
230ca1c
Merge branch 'master' into statemerging-dev
zwergziege Feb 3, 2025
34831d7
fixed broken k-Tails
zwergziege Feb 5, 2025
0a1a3f5
some comments for IOA
zwergziege Feb 5, 2025
6ce4a0e
changed RPNI from queue to deque (faster)
zwergziege Feb 5, 2025
70f8004
Fix (?) PAPNI
zwergziege Feb 5, 2025
57d1782
cleanup import
zwergziege Feb 5, 2025
466a167
minor adjustments and formatting
emuskardin Feb 5, 2025
8a84f04
minor adjustments
emuskardin Feb 5, 2025
9c1b98e
Removed experimental algorithm
zwergziege Feb 5, 2025
a89c5b4
cleanup
zwergziege Feb 5, 2025
35d5ab1
partial revert of k-Tails fix
zwergziege Feb 5, 2025
ea928b5
added explanatory comment for ScoreWithSink
zwergziege Feb 5, 2025
1c200a2
minor adjustments to GSM
emuskardin Feb 5, 2025
4d702ce
Merge remote-tracking branch 'origin/statemerging-dev' into statemerg…
emuskardin Feb 5, 2025
fcca5be
fix missing Score
zwergziege Feb 5, 2025
79c577c
Merge branch 'master' into gsm-dev
zwergziege Feb 5, 2025
3231b52
added gsm examples
zwergziege Feb 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 147 additions & 6 deletions Examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -1140,15 +1140,12 @@ def passive_vpa_learning_arithmetics():

def passive_vpa_learning_on_all_benchmark_models():
from aalpy.learning_algs import run_PAPNI
from aalpy.utils.BenchmarkVpaModels import get_all_VPAs
from aalpy.utils.BenchmarkVpaModels import vpa_L1, vpa_L12, vpa_for_odd_parentheses
from aalpy.utils import generate_input_output_data_from_vpa, convert_i_o_traces_for_RPNI

for gt in get_all_VPAs():

for gt in [vpa_L1(), vpa_L12(), vpa_for_odd_parentheses()]:
vpa_alphabet = gt.input_alphabet
data = generate_input_output_data_from_vpa(gt, num_sequances=2000, min_seq_len=1, max_seq_len=16)

data = convert_i_o_traces_for_RPNI(data)
data = generate_input_output_data_from_vpa(gt, num_sequances=2000, max_seq_len=16)

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

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

print('PAPNI model conforms to data.')


def gsm_rpni():
from aalpy import load_automaton_from_file
from aalpy.utils.Sampling import get_io_traces, sample_with_length_limits
from aalpy.learning_algs.general_passive.GeneralizedStateMerging import run_GSM

automaton = load_automaton_from_file("DotModels/car_alarm.dot", "moore")
input_traces = sample_with_length_limits(automaton.get_input_alphabet(), 100, 20, 30)
traces = get_io_traces(automaton, input_traces)

learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="deterministic")
learned_model.visualize()


def gsm_edsm():
from typing import Dict
from aalpy import load_automaton_from_file
from aalpy.utils.Sampling import get_io_traces, sample_with_length_limits
from aalpy.learning_algs.general_passive.GeneralizedStateMerging import run_GSM
from aalpy.learning_algs.general_passive.ScoreFunctionsGSM import ScoreCalculation
from aalpy.learning_algs.general_passive.Node import Node

automaton = load_automaton_from_file("DotModels/car_alarm.dot", "moore")
input_traces = sample_with_length_limits(automaton.get_input_alphabet(), 100, 20, 30)
traces = get_io_traces(automaton, input_traces)

def EDSM_score(part: Dict[Node, Node]):
nr_partitions = len(set(part.values()))
nr_merged = len(part)
return nr_merged - nr_partitions

score = ScoreCalculation(score_function=EDSM_score)
learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="deterministic", score_calc=score)
learned_model.visualize()


def gsm_likelihood_ratio():
from typing import Dict
from scipy.stats import chi2
from aalpy.learning_algs.general_passive.GeneralizedStateMerging import run_GSM
from aalpy.learning_algs.general_passive.ScoreFunctionsGSM import ScoreFunction, differential_info, ScoreCalculation
from aalpy.learning_algs.general_passive.Node import Node
from aalpy.utils.Sampling import get_io_traces, sample_with_length_limits
from aalpy import load_automaton_from_file

automaton = load_automaton_from_file("DotModels/MDPs/faulty_car_alarm.dot", "mdp")
input_traces = sample_with_length_limits(automaton.get_input_alphabet(), 2000, 20, 30)
traces = get_io_traces(automaton, input_traces)

def likelihood_ratio_score(alpha=0.05) -> ScoreFunction:
if not 0 < alpha <= 1:
raise ValueError(f"Confidence {alpha} not between 0 and 1")

def score_fun(part: Dict[Node, Node]):
llh_diff, param_diff = differential_info(part)
if param_diff == 0:
# This should cover the corner case when the partition merges only states with no outgoing transitions.
return -1 # Let them be very bad merges.
score = 1 - chi2.cdf(2 * llh_diff, param_diff)
if score < alpha:
return False
return score

return score_fun

score = ScoreCalculation(score_function=likelihood_ratio_score())
learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="stochastic", score_calc=score)
learned_model.visualize()


def gsm_IOAlergia_EDSM():
from aalpy.learning_algs.general_passive.GeneralizedStateMerging import run_GSM
from aalpy.learning_algs.general_passive.ScoreFunctionsGSM import hoeffding_compatibility, ScoreCalculation
from aalpy.learning_algs.general_passive.Node import Node
from aalpy.utils.Sampling import get_io_traces, sample_with_length_limits
from aalpy import load_automaton_from_file

automaton = load_automaton_from_file("DotModels/MDPs/faulty_car_alarm.dot", "mdp")
input_traces = sample_with_length_limits(automaton.get_input_alphabet(), 2000, 20, 30)
traces = get_io_traces(automaton, input_traces)

class IOAlergiaWithEDSM(ScoreCalculation):
def __init__(self, epsilon):
super().__init__()
self.ioa_compatibility = hoeffding_compatibility(epsilon)
self.evidence = 0

def reset(self):
self.evidence = 0

def local_compatibility(self, a: Node, b: Node):
self.evidence += 1
return self.ioa_compatibility(a, b)

def score_function(self, part: dict[Node, Node]):
return self.evidence

epsilon = 0.05
scores = {
"IOA": ScoreCalculation(hoeffding_compatibility(epsilon)),
"IOA+EDSM": IOAlergiaWithEDSM(epsilon),
}
for name, score in scores.items():
learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="stochastic", score_calc=score,
compatibility_on_pta=True, compatibility_on_futures=True)
learned_model.visualize(name)


def gsm_IOAlergia_domain_knowldege():
from aalpy.learning_algs.general_passive.GeneralizedStateMerging import run_GSM
from aalpy.learning_algs.general_passive.ScoreFunctionsGSM import hoeffding_compatibility, ScoreCalculation
from aalpy.learning_algs.general_passive.Node import Node
from aalpy.utils.Sampling import get_io_traces, sample_with_length_limits
from aalpy import load_automaton_from_file

automaton = load_automaton_from_file("DotModels/MDPs/faulty_car_alarm.dot", "mdp")
input_traces = sample_with_length_limits(automaton.get_input_alphabet(), 2000, 20, 30)
traces = get_io_traces(automaton, input_traces)

ioa_compat = hoeffding_compatibility(0.05)

def get_parity(node: Node):
pref = node.get_prefix()
return [sum(in_s == key for in_s, out_s in pref) % 2 for key in ["l", "d"]]

# The car has 4 physical states arising from the combination of locked/unlocked and open/closed.
# Each input toggles a transition between these four states. While the car alarm system has richer behavior than that,
# it still needs to discern the physical states. Thus, in every sane implementation of a car alarm system, every state
# is associated with exactly one physical state. This additional assumption can be enforced by checking the parity of
# all input symbols during merging.
def ioa_compat_domain_knowledge(a: Node, b: Node):
parity = get_parity(a) == get_parity(b)
ioa = ioa_compat(a, b)
return parity and ioa

scores = {
"IOA": ScoreCalculation(ioa_compat),
"IOA+DK": ScoreCalculation(ioa_compat_domain_knowledge),
}
for name, score in scores.items():
learned_model = run_GSM(traces, output_behavior="moore", transition_behavior="stochastic", score_calc=score,
compatibility_on_pta=True, compatibility_on_futures=True)
learned_model.visualize(name)
3 changes: 3 additions & 0 deletions aalpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
MealyState,
MooreMachine,
MooreState,
NDMooreMachine,
NDMooreState,
Onfsm,
OnfsmState,
Sevpa,
Expand Down Expand Up @@ -37,6 +39,7 @@
run_non_det_Lstar,
run_RPNI,
run_stochastic_Lstar,
run_GSM,
run_PAPNI
)
from .oracles import (
Expand Down
67 changes: 67 additions & 0 deletions aalpy/automata/NonDeterministicMooreMachine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import random
from collections import defaultdict
from typing import List, Dict, Generic

from aalpy.base import AutomatonState, Automaton
from aalpy.base.Automaton import OutputType, InputType


class NDMooreState(AutomatonState, Generic[InputType, OutputType]):
"""
Single state of a non-deterministic Moore machine. Each state has an output value.
"""

def __init__(self, state_id, output=None):
super().__init__(state_id)
self.transitions: Dict[InputType, List['NDMooreState']] = defaultdict(lambda: list())
self.output: OutputType = output


class NDMooreMachine(Automaton[NDMooreState[InputType, OutputType]]):

def to_state_setup(self):
state_setup = dict()

def set_dict_entry(state: NDMooreState):
state_setup[state.state_id] = (state.output,
{in_sym: [target.state_id for target in trans] for in_sym, trans in
state.transitions.items()})

set_dict_entry(self.initial_state)
for state in self.states:
if state is self.initial_state:
continue
set_dict_entry(state)

@staticmethod
def from_state_setup(state_setup: dict, **kwargs) -> 'NDMooreMachine':
states_map = {key: NDMooreState(key, output=value[0]) for key, value in state_setup.items()}

for key, values in state_setup.items():
source = states_map[key]
for i, transitions in values[1].items():
for node in transitions:
source.transitions[i].append(states_map[node])

initial_state = states_map[list(state_setup.keys())[0]]
return NDMooreMachine(initial_state, list(states_map.values()))

def __init__(self, initial_state: AutomatonState, states: list):
super().__init__(initial_state, states)

def step(self, letter):
"""
In Moore machines outputs depend on the current state.

Args:

letter: single input that is looked up in the transition function leading to a new state

Returns:

the output of the reached state

"""
options = self.current_state.transitions[letter]
self.current_state = random.choice(options)
return self.current_state.output
1 change: 1 addition & 0 deletions aalpy/automata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
from .Onfsm import Onfsm, OnfsmState
from .StochasticMealyMachine import StochasticMealyMachine, StochasticMealyState
from .MarkovChain import MarkovChain, McState
from .NonDeterministicMooreMachine import NDMooreMachine, NDMooreState
from .Sevpa import Sevpa, SevpaState, SevpaAlphabet, SevpaTransition
from .Vpa import Vpa, VpaAlphabet, VpaState, VpaTransition
1 change: 1 addition & 0 deletions aalpy/learning_algs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
from .stochastic_passive.ActiveAleriga import run_active_Alergia
from .deterministic_passive.RPNI import run_RPNI, run_PAPNI
from .deterministic_passive.active_RPNI import run_active_RPNI
from .general_passive.GeneralizedStateMerging import run_GSM
129 changes: 129 additions & 0 deletions aalpy/learning_algs/deterministic_passive/ClassicRPNI.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import time
from bisect import insort
from aalpy.learning_algs.deterministic_passive.rpni_helper_functions import to_automaton, createPTA, \
check_sequence, extract_unique_sequences


class ClassicRPNI:
def __init__(self, data, automaton_type, print_info=True):
self.data = data
self.automaton_type = automaton_type
self.print_info = print_info

pta_construction_start = time.time()
self.root_node = createPTA(data, automaton_type)
self.test_data = extract_unique_sequences(self.root_node)

if self.print_info:
print(f'PTA Construction Time: {round(time.time() - pta_construction_start, 2)}')

def run_rpni(self):
start_time = time.time()

red = [self.root_node]
blue = list(red[0].children.values())
while blue:
lex_min_blue = min(list(blue))
merged = False

for red_state in red:
if not self._compatible_states(red_state, lex_min_blue):
continue
merge_candidate = self._merge(red_state, lex_min_blue, copy_nodes=True)
if self._compatible(merge_candidate):
self._merge(red_state, lex_min_blue)
merged = True
break

if not merged:
insort(red, lex_min_blue)
if self.print_info:
print(f'\rCurrent automaton size: {len(red)}', end="")

blue.clear()
for r in red:
for c in r.children.values():
if c not in red:
blue.append(c)

if self.print_info:
print(f'\nRPNI Learning Time: {round(time.time() - start_time, 2)}')
print(f'RPNI Learned {len(red)} state automaton.')

assert sorted(red, key=lambda x: len(x.prefix)) == red
return to_automaton(red, self.automaton_type)

def _compatible(self, root_node):
"""
Check if current model is compatible with the data.
"""
for sequence in self.test_data:
if not check_sequence(root_node, sequence, automaton_type=self.automaton_type):
return False
return True

def _compatible_states(self, red_node, blue_node):
"""
Only allow merging of states that have same output(s).
"""
if self.automaton_type != 'mealy':
# None is compatible with everything
return red_node.output == blue_node.output or red_node.output is None or blue_node.output is None
else:
red_io = {i: o for i, o in red_node.children.keys()}
blue_io = {i: o for i, o in blue_node.children.keys()}
for common_i in set(red_io.keys()).intersection(blue_io.keys()):
if red_io[common_i] != blue_io[common_i]:
return False
return True

def _merge(self, red_node, lex_min_blue, copy_nodes=False):
"""
Merge two states and return the root node of resulting model.
"""
root_node = self.root_node.copy() if copy_nodes else self.root_node
lex_min_blue = lex_min_blue.copy() if copy_nodes else lex_min_blue

red_node_in_tree = root_node
for p in red_node.prefix:
red_node_in_tree = red_node_in_tree.children[p]

to_update = root_node
for p in lex_min_blue.prefix[:-1]:
to_update = to_update.children[p]

to_update.children[lex_min_blue.prefix[-1]] = red_node_in_tree

if self.automaton_type != 'mealy':
self._fold(red_node_in_tree, lex_min_blue)
else:
self._fold_mealy(red_node_in_tree, lex_min_blue)

return root_node

def _fold(self, red_node, blue_node):
# Change the output of red only to concrete output, ignore None
red_node.output = blue_node.output if blue_node.output is not None else red_node.output

for i in blue_node.children.keys():
if i in red_node.children.keys():
self._fold(red_node.children[i], blue_node.children[i])
else:
red_node.children[i] = blue_node.children[i]

def _fold_mealy(self, red_node, blue_node):
blue_io_map = {i: o for i, o in blue_node.children.keys()}

updated_keys = {}
for io, val in red_node.children.items():
o = blue_io_map[io[0]] if io[0] in blue_io_map.keys() else io[1]
updated_keys[(io[0], o)] = val

red_node.children = updated_keys

for io in blue_node.children.keys():
if io in red_node.children.keys():
self._fold_mealy(red_node.children[io], blue_node.children[io])
else:
red_node.children[io] = blue_node.children[io]

Loading
Loading