lernen 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -0
- data/README.md +534 -48
- data/Rakefile +26 -7
- data/Steepfile +14 -0
- data/examples/ripper_prism.rb +63 -0
- data/examples/uri_parse_regexp.rb +73 -0
- data/lib/lernen/algorithm/cex_processor/acex.rb +43 -0
- data/lib/lernen/algorithm/cex_processor/prefix_transformer_acex.rb +43 -0
- data/lib/lernen/algorithm/cex_processor.rb +115 -0
- data/lib/lernen/algorithm/kearns_vazirani/discrimination_tree.rb +207 -0
- data/lib/lernen/algorithm/kearns_vazirani/kearns_vazirani_learner.rb +100 -0
- data/lib/lernen/algorithm/kearns_vazirani.rb +44 -0
- data/lib/lernen/algorithm/kearns_vazirani_vpa/discrimination_tree_vpa.rb +246 -0
- data/lib/lernen/algorithm/kearns_vazirani_vpa/kearns_vazirani_vpa_learner.rb +89 -0
- data/lib/lernen/algorithm/kearns_vazirani_vpa.rb +35 -0
- data/lib/lernen/algorithm/learner.rb +82 -0
- data/lib/lernen/algorithm/lsharp/lsharp_learner.rb +367 -0
- data/lib/lernen/algorithm/lsharp/observation_tree.rb +115 -0
- data/lib/lernen/algorithm/lsharp.rb +43 -0
- data/lib/lernen/algorithm/lstar/lstar_learner.rb +49 -0
- data/lib/lernen/algorithm/lstar/observation_table.rb +214 -0
- data/lib/lernen/algorithm/lstar.rb +49 -0
- data/lib/lernen/algorithm/procedural/atr_manager.rb +200 -0
- data/lib/lernen/algorithm/procedural/procedural_learner.rb +223 -0
- data/lib/lernen/algorithm/procedural/procedural_sul.rb +47 -0
- data/lib/lernen/algorithm/procedural/return_indices_acex.rb +58 -0
- data/lib/lernen/algorithm/procedural.rb +57 -0
- data/lib/lernen/algorithm.rb +19 -0
- data/lib/lernen/automaton/dfa.rb +204 -0
- data/lib/lernen/automaton/mealy.rb +108 -0
- data/lib/lernen/automaton/moore.rb +122 -0
- data/lib/lernen/automaton/moore_like.rb +83 -0
- data/lib/lernen/automaton/proc_util.rb +93 -0
- data/lib/lernen/automaton/spa.rb +368 -0
- data/lib/lernen/automaton/transition_system.rb +209 -0
- data/lib/lernen/automaton/vpa.rb +300 -0
- data/lib/lernen/automaton.rb +19 -493
- data/lib/lernen/equiv/combined_oracle.rb +57 -0
- data/lib/lernen/equiv/exhaustive_search_oracle.rb +60 -0
- data/lib/lernen/equiv/moore_like_simulator_oracle.rb +36 -0
- data/lib/lernen/equiv/oracle.rb +109 -0
- data/lib/lernen/equiv/random_walk_oracle.rb +69 -0
- data/lib/lernen/equiv/random_well_matched_word_oracle.rb +139 -0
- data/lib/lernen/equiv/random_word_oracle.rb +71 -0
- data/lib/lernen/equiv/spa_simulator_oracle.rb +39 -0
- data/lib/lernen/equiv/test_words_oracle.rb +42 -0
- data/lib/lernen/equiv/transition_system_simulator_oracle.rb +36 -0
- data/lib/lernen/equiv/vpa_simulator_oracle.rb +48 -0
- data/lib/lernen/equiv.rb +25 -0
- data/lib/lernen/graph.rb +215 -0
- data/lib/lernen/system/block_sul.rb +41 -0
- data/lib/lernen/system/moore_like_simulator.rb +45 -0
- data/lib/lernen/system/moore_like_sul.rb +33 -0
- data/lib/lernen/system/sul.rb +126 -0
- data/lib/lernen/system/transition_system_simulator.rb +40 -0
- data/lib/lernen/system.rb +72 -0
- data/lib/lernen/version.rb +2 -1
- data/lib/lernen.rb +284 -34
- data/rbs_collection.lock.yaml +16 -0
- data/rbs_collection.yaml +14 -0
- data/renovate.json +6 -0
- data/sig/generated/lernen/algorithm/cex_processor/acex.rbs +30 -0
- data/sig/generated/lernen/algorithm/cex_processor/prefix_transformer_acex.rbs +27 -0
- data/sig/generated/lernen/algorithm/cex_processor.rbs +59 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani/discrimination_tree.rbs +68 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani/kearns_vazirani_learner.rbs +51 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani.rbs +32 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani_vpa/discrimination_tree_vpa.rbs +73 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani_vpa/kearns_vazirani_vpa_learner.rbs +51 -0
- data/sig/generated/lernen/algorithm/kearns_vazirani_vpa.rbs +20 -0
- data/sig/generated/lernen/algorithm/learner.rbs +53 -0
- data/sig/generated/lernen/algorithm/lsharp/lsharp_learner.rbs +103 -0
- data/sig/generated/lernen/algorithm/lsharp/observation_tree.rbs +53 -0
- data/sig/generated/lernen/algorithm/lsharp.rbs +38 -0
- data/sig/generated/lernen/algorithm/lstar/lstar_learner.rbs +38 -0
- data/sig/generated/lernen/algorithm/lstar/observation_table.rbs +79 -0
- data/sig/generated/lernen/algorithm/lstar.rbs +37 -0
- data/sig/generated/lernen/algorithm/procedural/atr_manager.rbs +80 -0
- data/sig/generated/lernen/algorithm/procedural/procedural_learner.rbs +79 -0
- data/sig/generated/lernen/algorithm/procedural/procedural_sul.rbs +36 -0
- data/sig/generated/lernen/algorithm/procedural/return_indices_acex.rbs +33 -0
- data/sig/generated/lernen/algorithm/procedural.rbs +27 -0
- data/sig/generated/lernen/algorithm.rbs +10 -0
- data/sig/generated/lernen/automaton/dfa.rbs +93 -0
- data/sig/generated/lernen/automaton/mealy.rbs +61 -0
- data/sig/generated/lernen/automaton/moore.rbs +69 -0
- data/sig/generated/lernen/automaton/moore_like.rbs +63 -0
- data/sig/generated/lernen/automaton/proc_util.rbs +38 -0
- data/sig/generated/lernen/automaton/spa.rbs +125 -0
- data/sig/generated/lernen/automaton/transition_system.rbs +108 -0
- data/sig/generated/lernen/automaton/vpa.rbs +109 -0
- data/sig/generated/lernen/automaton.rbs +15 -0
- data/sig/generated/lernen/equiv/combined_oracle.rbs +27 -0
- data/sig/generated/lernen/equiv/exhaustive_search_oracle.rbs +38 -0
- data/sig/generated/lernen/equiv/moore_like_simulator_oracle.rbs +27 -0
- data/sig/generated/lernen/equiv/oracle.rbs +75 -0
- data/sig/generated/lernen/equiv/random_walk_oracle.rbs +41 -0
- data/sig/generated/lernen/equiv/random_well_matched_word_oracle.rbs +70 -0
- data/sig/generated/lernen/equiv/random_word_oracle.rbs +45 -0
- data/sig/generated/lernen/equiv/spa_simulator_oracle.rbs +30 -0
- data/sig/generated/lernen/equiv/test_words_oracle.rbs +20 -0
- data/sig/generated/lernen/equiv/transition_system_simulator_oracle.rbs +27 -0
- data/sig/generated/lernen/equiv/vpa_simulator_oracle.rbs +33 -0
- data/sig/generated/lernen/equiv.rbs +11 -0
- data/sig/generated/lernen/graph.rbs +80 -0
- data/sig/generated/lernen/system/block_sul.rbs +29 -0
- data/sig/generated/lernen/system/moore_like_simulator.rbs +31 -0
- data/sig/generated/lernen/system/moore_like_sul.rbs +28 -0
- data/sig/generated/lernen/system/sul.rbs +87 -0
- data/sig/generated/lernen/system/transition_system_simulator.rbs +28 -0
- data/sig/generated/lernen/system.rbs +62 -0
- data/sig/generated/lernen/version.rbs +6 -0
- data/sig/generated/lernen.rbs +214 -0
- data/sig-test/generated/test/example_test.rbs +14 -0
- data/sig-test/generated/test/lernen/algorithm/kearns_vazirani_test.rbs +16 -0
- data/sig-test/generated/test/lernen/algorithm/kearns_vazirani_vpa_test.rbs +10 -0
- data/sig-test/generated/test/lernen/algorithm/lsharp_test.rbs +16 -0
- data/sig-test/generated/test/lernen/algorithm/lstar_test.rbs +16 -0
- data/sig-test/generated/test/lernen/algorithm/procedural_test.rbs +10 -0
- data/sig-test/generated/test/lernen/automaton/dfa_test.rbs +19 -0
- data/sig-test/generated/test/lernen/automaton/mealy_test.rbs +19 -0
- data/sig-test/generated/test/lernen/automaton/moore_test.rbs +19 -0
- data/sig-test/generated/test/lernen/automaton/proc_util_test.rbs +19 -0
- data/sig-test/generated/test/lernen/automaton/spa_test.rbs +19 -0
- data/sig-test/generated/test/lernen/automaton/vpa_test.rbs +19 -0
- data/sig-test/generated/test/lernen/equiv/exhaustive_search_oracle_test.rbs +10 -0
- data/sig-test/generated/test/lernen/equiv/random_walk_oracle_test.rbs +10 -0
- data/sig-test/generated/test/lernen/equiv/random_word_oracle_test.rbs +10 -0
- data/sig-test/generated/test/lernen/system/block_sul_test.rbs +16 -0
- data/sig-test/generated/test/lernen/system/moore_like_simulator_test.rbs +16 -0
- data/sig-test/generated/test/lernen/system/transition_system_simulator_test.rbs +13 -0
- data/sig-test/generated/test/lernen/system_test.rbs +11 -0
- data/sig-test/generated/test/lernen_test.rbs +13 -0
- metadata +131 -11
- data/.yardopts +0 -3
- data/lib/lernen/cex_processor.rb +0 -92
- data/lib/lernen/kearns_vazirani.rb +0 -310
- data/lib/lernen/lsharp.rb +0 -344
- data/lib/lernen/lstar.rb +0 -170
- data/lib/lernen/oracle.rb +0 -119
- data/lib/lernen/sul.rb +0 -210
@@ -1,310 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Lernen
|
4
|
-
# ClassificationTree is a classification tree implementation.
|
5
|
-
class ClassificationTree
|
6
|
-
Node = Data.define(:suffix, :edges)
|
7
|
-
Leaf = Data.define(:prefix)
|
8
|
-
|
9
|
-
private_constant :Node, :Leaf
|
10
|
-
|
11
|
-
def initialize(alphabet, sul, cex:, automaton_type:, cex_processing:, call_alphabet:, return_alphabet:)
|
12
|
-
@alphabet = alphabet
|
13
|
-
@sul = sul
|
14
|
-
@automaton_type = automaton_type
|
15
|
-
@cex_processing = cex_processing
|
16
|
-
@call_alphabet = call_alphabet
|
17
|
-
@return_alphabet = return_alphabet
|
18
|
-
|
19
|
-
@paths = {}
|
20
|
-
|
21
|
-
case @automaton_type
|
22
|
-
in :dfa | :moore
|
23
|
-
@root = Node[[], {}]
|
24
|
-
|
25
|
-
empty_out = sul.query_empty
|
26
|
-
@root.edges[empty_out] = Leaf[[]]
|
27
|
-
@paths[[]] = [empty_out]
|
28
|
-
|
29
|
-
cex_out = sul.query(cex).last
|
30
|
-
@root.edges[cex_out] = Leaf[cex]
|
31
|
-
@paths[cex] = [cex_out]
|
32
|
-
in :mealy
|
33
|
-
prefix = cex[0...-1]
|
34
|
-
suffix = [cex.last]
|
35
|
-
@root = Node[suffix, {}]
|
36
|
-
|
37
|
-
suffix_out = sul.query(suffix).last
|
38
|
-
@root.edges[suffix_out] = Leaf[[]]
|
39
|
-
@paths[[]] = [suffix_out]
|
40
|
-
|
41
|
-
cex_out = sul.query(cex).last
|
42
|
-
@root.edges[cex_out] = Leaf[prefix]
|
43
|
-
@paths[prefix] = [cex_out]
|
44
|
-
in :vpa
|
45
|
-
@root = Node[[[], []], {}]
|
46
|
-
|
47
|
-
empty_out = sul.query_empty
|
48
|
-
@root.edges[empty_out] = Leaf[[]]
|
49
|
-
@paths[[]] = [empty_out]
|
50
|
-
|
51
|
-
cex_out = sul.query(cex).last
|
52
|
-
@root.edges[cex_out] = Leaf[cex]
|
53
|
-
@paths[cex] = [cex_out]
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
# Returns a prefix classified by `word`.
|
58
|
-
def sift(word)
|
59
|
-
node = @root
|
60
|
-
path = []
|
61
|
-
|
62
|
-
until node.is_a?(Leaf)
|
63
|
-
inputs =
|
64
|
-
case @automaton_type
|
65
|
-
in :vpa
|
66
|
-
access, suffix = node.suffix
|
67
|
-
access + word + suffix
|
68
|
-
in :dfa | :moore | :mealy
|
69
|
-
word + node.suffix
|
70
|
-
end
|
71
|
-
|
72
|
-
out = @sul.query(inputs).last
|
73
|
-
path << out
|
74
|
-
|
75
|
-
unless node.edges.include?(out)
|
76
|
-
node.edges[out] = Leaf[word]
|
77
|
-
@paths[word] = path
|
78
|
-
end
|
79
|
-
|
80
|
-
node = node.edges[out]
|
81
|
-
end
|
82
|
-
|
83
|
-
node.prefix
|
84
|
-
end
|
85
|
-
|
86
|
-
# Constructs a hypothesis automaton from this classification tree.
|
87
|
-
def to_hypothesis
|
88
|
-
transitions = {}
|
89
|
-
returns = {}
|
90
|
-
|
91
|
-
queue = []
|
92
|
-
prefix_to_state = {}
|
93
|
-
state_to_prefix = {}
|
94
|
-
|
95
|
-
queue << []
|
96
|
-
prefix_to_state[[]] = prefix_to_state.size
|
97
|
-
state_to_prefix[state_to_prefix.size] = []
|
98
|
-
|
99
|
-
until queue.empty?
|
100
|
-
prefix = queue.shift
|
101
|
-
state = prefix_to_state[prefix]
|
102
|
-
@alphabet.each do |input|
|
103
|
-
word = prefix + [input]
|
104
|
-
next_prefix = sift(word)
|
105
|
-
|
106
|
-
unless prefix_to_state.include?(next_prefix)
|
107
|
-
queue << next_prefix
|
108
|
-
prefix_to_state[next_prefix] = prefix_to_state.size
|
109
|
-
state_to_prefix[state_to_prefix.size] = next_prefix
|
110
|
-
end
|
111
|
-
|
112
|
-
next_state = prefix_to_state[next_prefix]
|
113
|
-
case @automaton_type
|
114
|
-
in :dfa | :moore | :vpa
|
115
|
-
transitions[[state, input]] = next_state
|
116
|
-
in :mealy
|
117
|
-
output = @sul.query(word).last
|
118
|
-
transitions[[state, input]] = [output, next_state]
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
next unless @automaton_type == :vpa
|
123
|
-
|
124
|
-
found_states = prefix_to_state.values
|
125
|
-
|
126
|
-
returns.each do |(return_state, return_input), return_transitions|
|
127
|
-
return_prefix = state_to_prefix[return_state]
|
128
|
-
@call_alphabet.each do |call_input|
|
129
|
-
word = prefix + [call_input] + return_prefix + [return_input]
|
130
|
-
next_prefix = sift(word)
|
131
|
-
|
132
|
-
unless prefix_to_state.include?(next_prefix)
|
133
|
-
queue << next_prefix
|
134
|
-
prefix_to_state[next_prefix] = prefix_to_state.size
|
135
|
-
state_to_prefix[state_to_prefix.size] = next_prefix
|
136
|
-
end
|
137
|
-
|
138
|
-
next_state = prefix_to_state[next_prefix]
|
139
|
-
return_transitions[[state, call_input]] = next_state
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
@return_alphabet.each do |return_input|
|
144
|
-
return_transitions = returns[[state, return_input]] = {}
|
145
|
-
found_states.each do |call_state|
|
146
|
-
call_prefix = state_to_prefix[call_state]
|
147
|
-
@call_alphabet.each do |call_input|
|
148
|
-
word = call_prefix + [call_input] + prefix + [return_input]
|
149
|
-
next_prefix = sift(word)
|
150
|
-
|
151
|
-
unless prefix_to_state.include?(next_prefix)
|
152
|
-
queue << next_prefix
|
153
|
-
prefix_to_state[next_prefix] = prefix_to_state.size
|
154
|
-
state_to_prefix[state_to_prefix.size] = next_prefix
|
155
|
-
end
|
156
|
-
|
157
|
-
next_state = prefix_to_state[next_prefix]
|
158
|
-
return_transitions[[call_state, call_input]] = next_state
|
159
|
-
end
|
160
|
-
end
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
automaton =
|
165
|
-
case @automaton_type
|
166
|
-
in :dfa
|
167
|
-
accept_states = state_to_prefix.to_a.filter { |(_, q)| @paths[q][0] }.to_set { |(i, _)| i }
|
168
|
-
DFA.new(0, accept_states, transitions)
|
169
|
-
in :moore
|
170
|
-
outputs = state_to_prefix.transform_values { |q| @paths[q][0] }
|
171
|
-
Moore.new(0, outputs, transitions)
|
172
|
-
in :mealy
|
173
|
-
Mealy.new(0, transitions)
|
174
|
-
in :vpa
|
175
|
-
accept_states = state_to_prefix.to_a.filter { |(_, q)| @paths[q][0] }.to_set { |(i, _)| i }
|
176
|
-
state_to_prefix[nil] = [@return_alphabet.first] unless @return_alphabet.empty?
|
177
|
-
state_to_prefix = VPA::StateToPrefixMapping.new(state_to_prefix)
|
178
|
-
VPA.new(0, accept_states, transitions, returns)
|
179
|
-
end
|
180
|
-
|
181
|
-
[automaton, state_to_prefix]
|
182
|
-
end
|
183
|
-
|
184
|
-
# Update this classification tree by the given `cex`.
|
185
|
-
def process_cex(hypothesis, cex, state_to_prefix)
|
186
|
-
old_prefix, new_input, new_suffix =
|
187
|
-
CexProcessor.process(@sul, hypothesis, cex, state_to_prefix, cex_processing: @cex_processing)
|
188
|
-
|
189
|
-
_, old_state = hypothesis.run(old_prefix)
|
190
|
-
_, replace_state = hypothesis.step(old_state, new_input)
|
191
|
-
|
192
|
-
case @automaton_type
|
193
|
-
in :dfa | :moore | :mealy
|
194
|
-
new_prefix = state_to_prefix[old_state] + [new_input]
|
195
|
-
new_out = @sul.query(new_prefix + new_suffix).last
|
196
|
-
|
197
|
-
replace_prefix = state_to_prefix[replace_state]
|
198
|
-
replace_out = @sul.query(replace_prefix + new_suffix).last
|
199
|
-
in :vpa
|
200
|
-
new_suffix = [state_to_prefix[VPA::Conf[hypothesis.initial_state, replace_state.stack]], new_suffix]
|
201
|
-
|
202
|
-
old_state_prefix = state_to_prefix.state_prefix(old_state.state)
|
203
|
-
if @alphabet.include?(new_input)
|
204
|
-
new_prefix = old_state_prefix + [new_input]
|
205
|
-
else
|
206
|
-
call_state, call_input = old_state.stack[-1]
|
207
|
-
call_prefix = state_to_prefix.state_prefix(call_state)
|
208
|
-
new_prefix = call_prefix + [call_input] + old_state_prefix + [new_input]
|
209
|
-
end
|
210
|
-
# new_out = @sul.query(cex).last
|
211
|
-
new_out = @sul.query(new_suffix[0] + new_prefix + new_suffix[1]).last
|
212
|
-
|
213
|
-
replace_prefix = state_to_prefix.state_prefix(replace_state.state)
|
214
|
-
replace_out = @sul.query(new_suffix[0] + replace_prefix + new_suffix[1]).last
|
215
|
-
end
|
216
|
-
|
217
|
-
replace_node_path = @paths[replace_prefix]
|
218
|
-
replace_node_parent = @root
|
219
|
-
replace_node = @root.edges[replace_node_path.first]
|
220
|
-
replace_node_path[1..].each do |out|
|
221
|
-
replace_node_parent = replace_node
|
222
|
-
replace_node = replace_node.edges[out]
|
223
|
-
end
|
224
|
-
|
225
|
-
new_node = Node[new_suffix, {}]
|
226
|
-
replace_node_parent.edges[replace_node_path.last] = new_node
|
227
|
-
|
228
|
-
new_node.edges[new_out] = Leaf[new_prefix]
|
229
|
-
@paths[new_prefix] = replace_node_path + [new_out]
|
230
|
-
|
231
|
-
new_node.edges[replace_out] = Leaf[replace_prefix]
|
232
|
-
@paths[replace_prefix] = replace_node_path + [replace_out]
|
233
|
-
end
|
234
|
-
end
|
235
|
-
|
236
|
-
# KearnsVazirani is an implementation of the Kearns-Vazirani automata learning algorithm.
|
237
|
-
module KearnsVazirani
|
238
|
-
# Runs the Kearns-Vazirani algoritghm and returns an inferred automaton.
|
239
|
-
def self.learn(
|
240
|
-
alphabet,
|
241
|
-
sul,
|
242
|
-
oracle,
|
243
|
-
automaton_type:,
|
244
|
-
cex_processing: :binary,
|
245
|
-
max_learning_rounds: nil,
|
246
|
-
call_alphabet: nil,
|
247
|
-
return_alphabet: nil
|
248
|
-
)
|
249
|
-
hypothesis = construct_first_hypothesis(alphabet, sul, automaton_type, call_alphabet:, return_alphabet:)
|
250
|
-
cex = oracle.find_cex(hypothesis)
|
251
|
-
return hypothesis if cex.nil?
|
252
|
-
|
253
|
-
classification_tree =
|
254
|
-
ClassificationTree.new(alphabet, sul, cex:, automaton_type:, cex_processing:, call_alphabet:, return_alphabet:)
|
255
|
-
learning_rounds = 0
|
256
|
-
|
257
|
-
loop do
|
258
|
-
break if max_learning_rounds && learning_rounds == max_learning_rounds
|
259
|
-
learning_rounds += 1
|
260
|
-
|
261
|
-
hypothesis, state_to_prefix = classification_tree.to_hypothesis
|
262
|
-
cex = oracle.find_cex(hypothesis)
|
263
|
-
break if cex.nil?
|
264
|
-
|
265
|
-
classification_tree.process_cex(hypothesis, cex, state_to_prefix)
|
266
|
-
end
|
267
|
-
|
268
|
-
hypothesis, = classification_tree.to_hypothesis
|
269
|
-
hypothesis
|
270
|
-
end
|
271
|
-
|
272
|
-
# Constructs the first hypothesis automaton.
|
273
|
-
def self.construct_first_hypothesis(alphabet, sul, automaton_type, call_alphabet:, return_alphabet:)
|
274
|
-
transitions = {}
|
275
|
-
alphabet.each do |input|
|
276
|
-
case automaton_type
|
277
|
-
in :dfa | :moore | :vpa
|
278
|
-
transitions[[0, input]] = 0
|
279
|
-
in :mealy
|
280
|
-
out = sul.query([input]).last
|
281
|
-
transitions[[0, input]] = [out, 0]
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
case automaton_type
|
286
|
-
in :dfa
|
287
|
-
accept_states = sul.query_empty ? Set[0] : Set.new
|
288
|
-
DFA.new(0, accept_states, transitions)
|
289
|
-
in :moore
|
290
|
-
outputs = { 0 => sul.query_empty }
|
291
|
-
Moore.new(0, outputs, transitions)
|
292
|
-
in :mealy
|
293
|
-
Mealy.new(0, transitions)
|
294
|
-
in :vpa
|
295
|
-
raise ArgumentError, "Learning 1-SEVPA needs call and return alphabet." unless call_alphabet && return_alphabet
|
296
|
-
|
297
|
-
returns = {}
|
298
|
-
return_alphabet.each do |return_input|
|
299
|
-
return_transitions = returns[[0, return_input]] = {}
|
300
|
-
call_alphabet.each { |call_input| return_transitions[[0, call_input]] = 0 }
|
301
|
-
end
|
302
|
-
|
303
|
-
accept_states = sul.query_empty ? Set[0] : Set.new
|
304
|
-
VPA.new(0, accept_states, transitions, returns)
|
305
|
-
end
|
306
|
-
end
|
307
|
-
|
308
|
-
private_class_method :construct_first_hypothesis
|
309
|
-
end
|
310
|
-
end
|
data/lib/lernen/lsharp.rb
DELETED
@@ -1,344 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Lernen
|
4
|
-
# ObservationTree is an observation tree implementation.
|
5
|
-
class ObservationTree
|
6
|
-
Node = Data.define(:output, :edges)
|
7
|
-
|
8
|
-
private_constant :Node
|
9
|
-
|
10
|
-
def initialize(sul, automaton_type:)
|
11
|
-
@sul = sul
|
12
|
-
@automaton_type = automaton_type
|
13
|
-
|
14
|
-
case automaton_type
|
15
|
-
in :dfa | :moore
|
16
|
-
@root = Node[@sul.query_empty, {}]
|
17
|
-
in :mealy
|
18
|
-
@root = Node[nil, {}]
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
attr_reader :root
|
23
|
-
|
24
|
-
# Returns a node for `inputs`.
|
25
|
-
def [](inputs)
|
26
|
-
node = @root
|
27
|
-
inputs.each do |input|
|
28
|
-
return nil unless node.edges[input]
|
29
|
-
node = node.edges[input]
|
30
|
-
end
|
31
|
-
node
|
32
|
-
end
|
33
|
-
|
34
|
-
# Returns an output sequence for `inputs` if it is observed.
|
35
|
-
# If it is not, it returns `nil` instead.
|
36
|
-
def observed_query(inputs)
|
37
|
-
return [@root.output] if inputs.empty?
|
38
|
-
|
39
|
-
node = @root
|
40
|
-
outputs = []
|
41
|
-
inputs.each do |input|
|
42
|
-
node = node.edges[input]
|
43
|
-
return nil unless node
|
44
|
-
|
45
|
-
outputs << node.output
|
46
|
-
end
|
47
|
-
|
48
|
-
outputs
|
49
|
-
end
|
50
|
-
|
51
|
-
# Returns an output sequence for `inputs`.
|
52
|
-
# If it is not, it runs actual query over `sul`.
|
53
|
-
def query(inputs)
|
54
|
-
outputs = observed_query(inputs)
|
55
|
-
return outputs unless outputs.nil?
|
56
|
-
|
57
|
-
outputs = @sul.query(inputs)
|
58
|
-
node = @root
|
59
|
-
inputs.zip(outputs) do |input, output|
|
60
|
-
node.edges[input] ||= Node[output, {}]
|
61
|
-
node = node.edges[input]
|
62
|
-
end
|
63
|
-
|
64
|
-
outputs
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
# LSharp is an implementation of the L# algorithm.
|
69
|
-
class LSharp
|
70
|
-
# Runs the L# algoritghm and returns an inferred automaton.
|
71
|
-
def self.learn(alphabet, sul, oracle, automaton_type:, max_learning_rounds: nil)
|
72
|
-
lsharp = new(alphabet, sul, oracle, automaton_type:, max_learning_rounds:)
|
73
|
-
lsharp.learn
|
74
|
-
end
|
75
|
-
|
76
|
-
def initialize(alphabet, sul, oracle, automaton_type:, max_learning_rounds: nil)
|
77
|
-
@alphabet = alphabet
|
78
|
-
@sul = sul
|
79
|
-
@oracle = oracle
|
80
|
-
@automaton_type = automaton_type
|
81
|
-
@max_learning_rounds = max_learning_rounds
|
82
|
-
|
83
|
-
@observation_tree = ObservationTree.new(sul, automaton_type:)
|
84
|
-
@witness_cache = {}
|
85
|
-
|
86
|
-
@basis = []
|
87
|
-
@frontier = {}
|
88
|
-
|
89
|
-
@incomplete_basis = []
|
90
|
-
end
|
91
|
-
|
92
|
-
# Runs the L# algoritghm and returns an inferred automaton.
|
93
|
-
def learn
|
94
|
-
add_basis([])
|
95
|
-
|
96
|
-
loop do
|
97
|
-
next if promotion || completion || identification
|
98
|
-
|
99
|
-
hypothesis = check_hypothesis
|
100
|
-
return hypothesis if hypothesis
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
private
|
105
|
-
|
106
|
-
def check_apartness(node1, node2)
|
107
|
-
case @automaton_type
|
108
|
-
in :dfa | :moore
|
109
|
-
return [] if node1.output != node2.output
|
110
|
-
in :mealy
|
111
|
-
# nop
|
112
|
-
end
|
113
|
-
|
114
|
-
queue = []
|
115
|
-
queue << [[], node1, node2]
|
116
|
-
|
117
|
-
until queue.empty?
|
118
|
-
suffix, node1, node2 = queue.shift
|
119
|
-
node1.edges.each do |input, next_node1|
|
120
|
-
next_node2 = node2.edges[input]
|
121
|
-
next unless next_node2
|
122
|
-
return suffix + [input] if next_node1.output != next_node2.output
|
123
|
-
queue << [suffix + [input], next_node1, next_node2]
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
nil
|
128
|
-
end
|
129
|
-
|
130
|
-
def compute_witness(prefix1, prefix2)
|
131
|
-
return @witness_cache[[prefix1, prefix2]] if @witness_cache[[prefix1, prefix2]]
|
132
|
-
|
133
|
-
node1 = @observation_tree[prefix1]
|
134
|
-
node2 = @observation_tree[prefix2]
|
135
|
-
witness = check_apartness(node1, node2)
|
136
|
-
@witness_cache[[prefix1, prefix2]] = witness
|
137
|
-
|
138
|
-
witness
|
139
|
-
end
|
140
|
-
|
141
|
-
def add_basis(prefix)
|
142
|
-
@basis << prefix
|
143
|
-
@incomplete_basis << prefix
|
144
|
-
prefix_node = @observation_tree[prefix]
|
145
|
-
@frontier.each do |border, eq_prefixes|
|
146
|
-
border_node = @observation_tree[border]
|
147
|
-
eq_prefixes << prefix unless check_apartness(prefix_node, border_node)
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
def add_frontier(border)
|
152
|
-
border_node = @observation_tree[border]
|
153
|
-
@frontier[border] = @basis.filter do |prefix|
|
154
|
-
prefix_node = @observation_tree[prefix]
|
155
|
-
check_apartness(prefix_node, border_node).nil?
|
156
|
-
end
|
157
|
-
end
|
158
|
-
|
159
|
-
def update_frontier
|
160
|
-
@frontier.each do |border, eq_prefixes|
|
161
|
-
border_node = @observation_tree[border]
|
162
|
-
eq_prefixes.filter! do |prefix|
|
163
|
-
prefix_node = @observation_tree[prefix]
|
164
|
-
check_apartness(prefix_node, border_node).nil?
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def construct_hypothesis
|
170
|
-
transitions = {}
|
171
|
-
prefix_to_state = @basis.each_with_index.to_h
|
172
|
-
|
173
|
-
@basis.each do |prefix|
|
174
|
-
state = prefix_to_state[prefix]
|
175
|
-
node = @observation_tree[prefix]
|
176
|
-
@alphabet.each do |input|
|
177
|
-
next_node = node.edges[input]
|
178
|
-
next_prefix = prefix + [input]
|
179
|
-
next_state =
|
180
|
-
if @frontier.include?(next_prefix)
|
181
|
-
prefix_to_state[@frontier[next_prefix].first]
|
182
|
-
else
|
183
|
-
prefix_to_state[next_prefix]
|
184
|
-
end
|
185
|
-
|
186
|
-
case @automaton_type
|
187
|
-
in :dfa | :moore
|
188
|
-
transitions[[state, input]] = next_state
|
189
|
-
in :mealy
|
190
|
-
transitions[[state, input]] = [next_node.output, next_state]
|
191
|
-
end
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
state_to_prefix = prefix_to_state.to_h { |q, i| [i, q] }
|
196
|
-
automaton =
|
197
|
-
case @automaton_type
|
198
|
-
in :dfa
|
199
|
-
accept_states =
|
200
|
-
state_to_prefix
|
201
|
-
.to_a
|
202
|
-
.filter { |(_, prefix)| @observation_tree.observed_query(prefix).last }
|
203
|
-
.to_set { |(state, _)| state }
|
204
|
-
DFA.new(0, accept_states, transitions)
|
205
|
-
in :moore
|
206
|
-
outputs = state_to_prefix.transform_values { |state| @observation_tree.observed_query(state).last }
|
207
|
-
Moore.new(0, outputs, transitions)
|
208
|
-
in :mealy
|
209
|
-
Mealy.new(0, transitions)
|
210
|
-
end
|
211
|
-
|
212
|
-
[automaton, state_to_prefix]
|
213
|
-
end
|
214
|
-
|
215
|
-
def check_consistency(hypothesis, state_to_prefix)
|
216
|
-
queue = []
|
217
|
-
queue << [[], hypothesis.initial, @observation_tree.root]
|
218
|
-
|
219
|
-
until queue.empty?
|
220
|
-
prefix, state, node = queue.shift
|
221
|
-
state_prefix = state_to_prefix[state]
|
222
|
-
next unless state_prefix
|
223
|
-
state_node = @observation_tree[state_prefix]
|
224
|
-
return prefix if check_apartness(node, state_node)
|
225
|
-
|
226
|
-
node.edges.each do |input, next_node|
|
227
|
-
_, next_state = hypothesis.step(state, input)
|
228
|
-
queue << [prefix + [input], next_state, next_node]
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
nil
|
233
|
-
end
|
234
|
-
|
235
|
-
def promotion
|
236
|
-
@frontier.each do |new_prefix, eq_prefixes|
|
237
|
-
next unless eq_prefixes.empty?
|
238
|
-
|
239
|
-
@frontier.delete(new_prefix)
|
240
|
-
add_basis(new_prefix)
|
241
|
-
|
242
|
-
return true
|
243
|
-
end
|
244
|
-
|
245
|
-
false
|
246
|
-
end
|
247
|
-
|
248
|
-
def completion
|
249
|
-
updated = false
|
250
|
-
|
251
|
-
until @incomplete_basis.empty?
|
252
|
-
prefix = @incomplete_basis.pop
|
253
|
-
prefix_tree = @observation_tree[prefix]
|
254
|
-
|
255
|
-
@alphabet.each do |input|
|
256
|
-
border = prefix + [input]
|
257
|
-
next if prefix_tree.edges[input] && (@basis.include?(border) || @frontier.include?(border))
|
258
|
-
|
259
|
-
@observation_tree.query(border)
|
260
|
-
add_frontier(border)
|
261
|
-
|
262
|
-
updated = true
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
updated
|
267
|
-
end
|
268
|
-
|
269
|
-
def identification
|
270
|
-
@frontier.each do |border, eq_prefixes|
|
271
|
-
next unless eq_prefixes.size >= 2
|
272
|
-
|
273
|
-
prefix1 = eq_prefixes[0]
|
274
|
-
prefix2 = eq_prefixes[1]
|
275
|
-
witness = compute_witness(prefix1, prefix2)
|
276
|
-
@observation_tree.query(border + witness)
|
277
|
-
update_frontier
|
278
|
-
|
279
|
-
return true
|
280
|
-
end
|
281
|
-
|
282
|
-
false
|
283
|
-
end
|
284
|
-
|
285
|
-
def check_hypothesis
|
286
|
-
hypothesis, state_to_prefix = construct_hypothesis
|
287
|
-
|
288
|
-
cex = check_consistency(hypothesis, state_to_prefix)
|
289
|
-
unless cex
|
290
|
-
cex0 = @oracle.find_cex(hypothesis)
|
291
|
-
if cex0
|
292
|
-
@observation_tree.query(cex0)
|
293
|
-
node = @observation_tree.root
|
294
|
-
state = hypothesis.initial_state
|
295
|
-
cex0.size.times do |n|
|
296
|
-
input = cex0[n]
|
297
|
-
node = node.edges[input]
|
298
|
-
_, state = hypothesis.step(state, input)
|
299
|
-
state_node = @observation_tree[state_to_prefix[state]]
|
300
|
-
if check_apartness(state_node, node)
|
301
|
-
cex = cex0[0..n]
|
302
|
-
break
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
end
|
307
|
-
|
308
|
-
return hypothesis if cex.nil?
|
309
|
-
|
310
|
-
process_cex(hypothesis, state_to_prefix, cex)
|
311
|
-
update_frontier
|
312
|
-
|
313
|
-
nil
|
314
|
-
end
|
315
|
-
|
316
|
-
def process_cex(hypothesis, state_to_prefix, cex)
|
317
|
-
border = @frontier.keys.find { cex[0..._1.size] == _1 }
|
318
|
-
|
319
|
-
while border.size < cex.size
|
320
|
-
_, state = hypothesis.run(cex)
|
321
|
-
state_node = @observation_tree[state_to_prefix[state]]
|
322
|
-
node = @observation_tree[cex]
|
323
|
-
witness = check_apartness(state_node, node)
|
324
|
-
|
325
|
-
mid = border.size + ((cex.size - border.size) / 2)
|
326
|
-
cex1 = cex[0...mid]
|
327
|
-
cex2 = cex[mid...]
|
328
|
-
|
329
|
-
_, state1 = hypothesis.run(cex1)
|
330
|
-
state1_prefix = state_to_prefix[state1]
|
331
|
-
@observation_tree.query(state1_prefix + cex2 + witness)
|
332
|
-
|
333
|
-
state1_node = @observation_tree[state1_prefix]
|
334
|
-
node1 = @observation_tree[cex1]
|
335
|
-
if check_apartness(state1_node, node1)
|
336
|
-
cex = cex1
|
337
|
-
else
|
338
|
-
cex = state1_prefix + cex2
|
339
|
-
border = @frontier.keys.find { cex[0..._1.size] == _1 }
|
340
|
-
end
|
341
|
-
end
|
342
|
-
end
|
343
|
-
end
|
344
|
-
end
|