lernen 0.1.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 +18 -0
- data/README.md +531 -28
- data/Rakefile +29 -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 -92
- 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 +322 -13
- 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 -61
- data/lib/lernen/kearns_vazirani.rb +0 -199
- data/lib/lernen/lsharp.rb +0 -335
- data/lib/lernen/lstar.rb +0 -169
- data/lib/lernen/oracle.rb +0 -116
- data/lib/lernen/sul.rb +0 -134
data/lib/lernen/lsharp.rb
DELETED
@@ -1,335 +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
|
-
end
|
89
|
-
|
90
|
-
# Runs the L# algoritghm and returns an inferred automaton.
|
91
|
-
def learn
|
92
|
-
@basis << []
|
93
|
-
|
94
|
-
loop do
|
95
|
-
update_frontier
|
96
|
-
|
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
|
-
prefix_node = @observation_tree[prefix]
|
144
|
-
@frontier.each do |border, eq_prefixes|
|
145
|
-
border_node = @observation_tree[border]
|
146
|
-
eq_prefixes << prefix unless check_apartness(prefix_node, border_node)
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
def add_frontier(border)
|
151
|
-
border_node = @observation_tree[border]
|
152
|
-
@frontier[border] = @basis.filter do |prefix|
|
153
|
-
prefix_node = @observation_tree[prefix]
|
154
|
-
check_apartness(prefix_node, border_node).nil?
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
def update_frontier
|
159
|
-
@frontier.each do |border, eq_prefixes|
|
160
|
-
border_node = @observation_tree[border]
|
161
|
-
@frontier[border] = eq_prefixes.filter do |prefix|
|
162
|
-
prefix_node = @observation_tree[prefix]
|
163
|
-
check_apartness(prefix_node, border_node).nil?
|
164
|
-
end
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
def construct_hypothesis
|
169
|
-
transitions = {}
|
170
|
-
prefix_to_state = @basis.each_with_index.to_h
|
171
|
-
|
172
|
-
@basis.each do |prefix|
|
173
|
-
state = prefix_to_state[prefix]
|
174
|
-
node = @observation_tree[prefix]
|
175
|
-
@alphabet.each do |input|
|
176
|
-
next_node = node.edges[input]
|
177
|
-
next_prefix = prefix + [input]
|
178
|
-
next_state =
|
179
|
-
if @frontier.include?(next_prefix)
|
180
|
-
prefix_to_state[@frontier[next_prefix].first]
|
181
|
-
else
|
182
|
-
prefix_to_state[next_prefix]
|
183
|
-
end
|
184
|
-
|
185
|
-
case @automaton_type
|
186
|
-
in :dfa | :moore
|
187
|
-
transitions[[state, input]] = next_state
|
188
|
-
in :mealy
|
189
|
-
transitions[[state, input]] = [next_node.output, next_state]
|
190
|
-
end
|
191
|
-
end
|
192
|
-
end
|
193
|
-
|
194
|
-
state_to_prefix = prefix_to_state.to_h { |q, i| [i, q] }
|
195
|
-
automaton =
|
196
|
-
case @automaton_type
|
197
|
-
in :dfa
|
198
|
-
accept_states =
|
199
|
-
state_to_prefix
|
200
|
-
.to_a
|
201
|
-
.filter { |(_, prefix)| @observation_tree.observed_query(prefix).last }
|
202
|
-
.to_set { |(state, _)| state }
|
203
|
-
DFA.new(0, accept_states, transitions)
|
204
|
-
in :moore
|
205
|
-
outputs = state_to_prefix.transform_values { |state| @observation_tree.observed_query(state).last }
|
206
|
-
Moore.new(0, outputs, transitions)
|
207
|
-
in :mealy
|
208
|
-
Mealy.new(0, transitions)
|
209
|
-
end
|
210
|
-
|
211
|
-
[automaton, state_to_prefix]
|
212
|
-
end
|
213
|
-
|
214
|
-
def check_consistency(hypothesis, state_to_prefix)
|
215
|
-
queue = []
|
216
|
-
queue << [[], hypothesis.initial_state, @observation_tree.root]
|
217
|
-
|
218
|
-
until queue.empty?
|
219
|
-
prefix, state, node = queue.shift
|
220
|
-
state_prefix = state_to_prefix[state]
|
221
|
-
next unless state_prefix
|
222
|
-
state_node = @observation_tree[state_prefix]
|
223
|
-
return prefix if check_apartness(node, state_node)
|
224
|
-
|
225
|
-
node.edges.each do |input, next_node|
|
226
|
-
_, next_state = hypothesis.step(state, input)
|
227
|
-
queue << [prefix + [input], next_state, next_node]
|
228
|
-
end
|
229
|
-
end
|
230
|
-
|
231
|
-
nil
|
232
|
-
end
|
233
|
-
|
234
|
-
def promotion
|
235
|
-
isolated_borders = @frontier.to_a.filter { |(_, eq_prefixes)| eq_prefixes.empty? }.map { |(border, _)| border }
|
236
|
-
|
237
|
-
return false if isolated_borders.empty?
|
238
|
-
|
239
|
-
new_prefix = isolated_borders.first
|
240
|
-
@frontier.delete(new_prefix)
|
241
|
-
add_basis(new_prefix)
|
242
|
-
|
243
|
-
true
|
244
|
-
end
|
245
|
-
|
246
|
-
def completion
|
247
|
-
incomplete_borders =
|
248
|
-
@basis
|
249
|
-
.flat_map { |prefix| @alphabet.map { |a| prefix + [a] } }
|
250
|
-
.filter do |border|
|
251
|
-
@observation_tree[border].nil? || (!@basis.include?(border) && !@frontier.include?(border))
|
252
|
-
end
|
253
|
-
|
254
|
-
return false if incomplete_borders.empty?
|
255
|
-
|
256
|
-
incomplete_borders.each do |border|
|
257
|
-
@observation_tree.query(border)
|
258
|
-
add_frontier(border)
|
259
|
-
end
|
260
|
-
|
261
|
-
true
|
262
|
-
end
|
263
|
-
|
264
|
-
def identification
|
265
|
-
unidentified_borders = @frontier.keys.filter { @frontier[_1].size >= 2 }
|
266
|
-
|
267
|
-
return false if unidentified_borders.empty?
|
268
|
-
|
269
|
-
border = unidentified_borders.first
|
270
|
-
prefix1, prefix2 = @frontier[border][0...2]
|
271
|
-
witness = compute_witness(prefix1, prefix2)
|
272
|
-
@observation_tree.query(border + witness)
|
273
|
-
|
274
|
-
true
|
275
|
-
end
|
276
|
-
|
277
|
-
def check_hypothesis
|
278
|
-
hypothesis, state_to_prefix = construct_hypothesis
|
279
|
-
|
280
|
-
cex = check_consistency(hypothesis, state_to_prefix)
|
281
|
-
unless cex
|
282
|
-
cex0 = @oracle.find_cex(hypothesis)
|
283
|
-
if cex0
|
284
|
-
@observation_tree.query(cex0)
|
285
|
-
node = @observation_tree.root
|
286
|
-
state = hypothesis.initial_state
|
287
|
-
cex0.size.times do |n|
|
288
|
-
input = cex0[n]
|
289
|
-
node = node.edges[input]
|
290
|
-
_, state = hypothesis.step(state, input)
|
291
|
-
state_node = @observation_tree[state_to_prefix[state]]
|
292
|
-
if check_apartness(state_node, node)
|
293
|
-
cex = cex0[0..n]
|
294
|
-
break
|
295
|
-
end
|
296
|
-
end
|
297
|
-
end
|
298
|
-
end
|
299
|
-
|
300
|
-
return hypothesis if cex.nil?
|
301
|
-
|
302
|
-
process_cex(hypothesis, state_to_prefix, cex)
|
303
|
-
|
304
|
-
nil
|
305
|
-
end
|
306
|
-
|
307
|
-
def process_cex(hypothesis, state_to_prefix, cex)
|
308
|
-
border = @frontier.keys.find { cex[0..._1.size] == _1 }
|
309
|
-
|
310
|
-
while border.size < cex.size
|
311
|
-
_, state = hypothesis.run(cex)
|
312
|
-
state_node = @observation_tree[state_to_prefix[state]]
|
313
|
-
node = @observation_tree[cex]
|
314
|
-
witness = check_apartness(state_node, node)
|
315
|
-
|
316
|
-
mid = border.size + ((cex.size - border.size) / 2)
|
317
|
-
cex1 = cex[0...mid]
|
318
|
-
cex2 = cex[mid...]
|
319
|
-
|
320
|
-
_, state1 = hypothesis.run(cex1)
|
321
|
-
state1_prefix = state_to_prefix[state1]
|
322
|
-
@observation_tree.query(state1_prefix + cex2 + witness)
|
323
|
-
|
324
|
-
state1_node = @observation_tree[state1_prefix]
|
325
|
-
node1 = @observation_tree[cex1]
|
326
|
-
if check_apartness(state1_node, node1)
|
327
|
-
cex = cex1
|
328
|
-
else
|
329
|
-
cex = state1_prefix + cex2
|
330
|
-
border = @frontier.keys.find { cex[0..._1.size] == _1 }
|
331
|
-
end
|
332
|
-
end
|
333
|
-
end
|
334
|
-
end
|
335
|
-
end
|
data/lib/lernen/lstar.rb
DELETED
@@ -1,169 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Lernen
|
4
|
-
# ObservationTable is an observation table implementation.
|
5
|
-
class ObservationTable
|
6
|
-
def initialize(alphabet, sul, automaton_type:)
|
7
|
-
@alphabet = alphabet
|
8
|
-
@sul = sul
|
9
|
-
@automaton_type = automaton_type
|
10
|
-
|
11
|
-
@prefixes = [[]]
|
12
|
-
@suffixes = []
|
13
|
-
@table = {}
|
14
|
-
|
15
|
-
case @automaton_type
|
16
|
-
in :dfa | :moore
|
17
|
-
@suffixes << []
|
18
|
-
in :mealy
|
19
|
-
@alphabet.each { |a| @suffixes << [a] }
|
20
|
-
end
|
21
|
-
|
22
|
-
update_table
|
23
|
-
end
|
24
|
-
|
25
|
-
attr_reader :prefixes, :suffixes
|
26
|
-
|
27
|
-
# Finds new prefixes to close.
|
28
|
-
def find_prefixes_to_close
|
29
|
-
prefixes_to_close = []
|
30
|
-
unclosed_rows = Set.new
|
31
|
-
|
32
|
-
prefix_rows = @prefixes.to_set { @table[_1] }
|
33
|
-
|
34
|
-
extended_prefixes = @prefixes.flat_map { |q| @alphabet.map { |a| q + [a] } }
|
35
|
-
extended_prefixes.each do |qa|
|
36
|
-
row = @table[qa]
|
37
|
-
unless prefix_rows.include?(row) || unclosed_rows.include?(row)
|
38
|
-
prefixes_to_close << qa
|
39
|
-
unclosed_rows << row
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
return if prefixes_to_close.empty?
|
44
|
-
|
45
|
-
prefixes_to_close.sort_by!(&:size).reverse!
|
46
|
-
end
|
47
|
-
|
48
|
-
# Checks consistency and returns a new suffix to add if this observation table
|
49
|
-
# is inconsistent.
|
50
|
-
def check_consistency
|
51
|
-
@prefixes.combination(2) do |(q1, q2)|
|
52
|
-
next unless @table[q1] == @table[q2]
|
53
|
-
|
54
|
-
@alphabet.each do |a|
|
55
|
-
q1a = q1 + [a]
|
56
|
-
q2a = q2 + [a]
|
57
|
-
next if @table[q1a] == @table[q2a]
|
58
|
-
|
59
|
-
@suffixes.each_with_index do |e, i|
|
60
|
-
next if @table[q1a][i] == @table[q2a][i]
|
61
|
-
|
62
|
-
return [a] + e
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
nil
|
68
|
-
end
|
69
|
-
|
70
|
-
# Update observation table entries.
|
71
|
-
def update_table
|
72
|
-
extended_prefixes = @prefixes.flat_map { |q| [q] + @alphabet.map { |a| q + [a] } }
|
73
|
-
|
74
|
-
extended_prefixes.each do |qa|
|
75
|
-
@table[qa] ||= []
|
76
|
-
next if @table[qa].size == @suffixes.size
|
77
|
-
@suffixes.each_with_index do |e, i|
|
78
|
-
next if i < @table[qa].size
|
79
|
-
inputs = qa + e
|
80
|
-
outputs = inputs.empty? ? [@sul.query_empty] : @sul.query(inputs)
|
81
|
-
@table[qa] += [outputs.last]
|
82
|
-
end
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# Constructs a hypothesis automaton from this observation table.
|
87
|
-
def to_hypothesis
|
88
|
-
state_to_prefix = @prefixes.each_with_index.to_h { |q, i| [i, q] }
|
89
|
-
row_to_state = @prefixes.each_with_index.to_h { |q, i| [@table[q], i] }
|
90
|
-
|
91
|
-
transitions = {}
|
92
|
-
@prefixes.each_with_index do |q, i|
|
93
|
-
@alphabet.each_with_index do |a, j|
|
94
|
-
case @automaton_type
|
95
|
-
in :moore | :dfa
|
96
|
-
transitions[[i, a]] = row_to_state[@table[q + [a]]]
|
97
|
-
in :mealy
|
98
|
-
transitions[[i, a]] = [@table[q][j], row_to_state[@table[q + [a]]]]
|
99
|
-
end
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
automaton =
|
104
|
-
case @automaton_type
|
105
|
-
in :dfa
|
106
|
-
accept_states = state_to_prefix.to_a.filter { |(_, q)| @table[q][0] }.to_set { |(i, _)| i }
|
107
|
-
DFA.new(0, accept_states, transitions)
|
108
|
-
in :moore
|
109
|
-
outputs = state_to_prefix.transform_values { |q| @table[q][0] }
|
110
|
-
Moore.new(0, outputs, transitions)
|
111
|
-
in :mealy
|
112
|
-
Mealy.new(0, transitions)
|
113
|
-
end
|
114
|
-
|
115
|
-
[automaton, state_to_prefix]
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
# LStar is an implementation of Angluin's L* algorithm.
|
120
|
-
module LStar
|
121
|
-
# Runs Angluin's L* algoritghm and returns an inferred automaton.
|
122
|
-
def self.learn(alphabet, sul, oracle, automaton_type:, cex_processing: :binary, max_learning_rounds: nil)
|
123
|
-
observation_table = ObservationTable.new(alphabet, sul, automaton_type:)
|
124
|
-
learning_rounds = 0
|
125
|
-
|
126
|
-
loop do
|
127
|
-
break if max_learning_rounds && learning_rounds == max_learning_rounds
|
128
|
-
learning_rounds += 1
|
129
|
-
|
130
|
-
if cex_processing.nil?
|
131
|
-
new_suffix = observation_table.check_consistency
|
132
|
-
until new_suffix.nil?
|
133
|
-
observation_table.suffixes << new_suffix
|
134
|
-
observation_table.update_table
|
135
|
-
new_suffix = observation_table.check_consistency
|
136
|
-
end
|
137
|
-
end
|
138
|
-
|
139
|
-
new_prefixes = observation_table.find_prefixes_to_close
|
140
|
-
until new_prefixes.nil?
|
141
|
-
observation_table.prefixes.push(*new_prefixes)
|
142
|
-
observation_table.update_table
|
143
|
-
new_prefixes = observation_table.find_prefixes_to_close
|
144
|
-
end
|
145
|
-
|
146
|
-
hypothesis, state_to_prefix = observation_table.to_hypothesis
|
147
|
-
cex = oracle.find_cex(hypothesis)
|
148
|
-
break if cex.nil?
|
149
|
-
|
150
|
-
if cex_processing.nil?
|
151
|
-
all_prefixes = (0..cex.size).map { |n| cex[0...n] }
|
152
|
-
all_prefixes.each do |prefix|
|
153
|
-
observation_table.prefixes << prefix unless observation_table.prefixes.include?(prefix)
|
154
|
-
end
|
155
|
-
else
|
156
|
-
old_prefix, new_input, new_suffix =
|
157
|
-
CexProcessor.process(sul, hypothesis, cex, state_to_prefix, cex_processing:)
|
158
|
-
new_prefix = old_prefix + [new_input]
|
159
|
-
observation_table.prefixes << new_prefix unless observation_table.prefixes.include?(new_prefix)
|
160
|
-
observation_table.suffixes << new_suffix unless observation_table.suffixes.include?(new_suffix)
|
161
|
-
end
|
162
|
-
observation_table.update_table
|
163
|
-
end
|
164
|
-
|
165
|
-
hypothesis, = observation_table.to_hypothesis
|
166
|
-
hypothesis
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
data/lib/lernen/oracle.rb
DELETED
@@ -1,116 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Lernen
|
4
|
-
# Oracle is an equivalence oracle.
|
5
|
-
#
|
6
|
-
# Note that this class is *abstract*. You should implement the following method:
|
7
|
-
#
|
8
|
-
# - `#find_cex(hypothesis)`
|
9
|
-
class Oracle
|
10
|
-
def initialize(alphabet, sul)
|
11
|
-
@alphabet = alphabet
|
12
|
-
@sul = sul
|
13
|
-
|
14
|
-
@num_calls = 0
|
15
|
-
@num_queries = 0
|
16
|
-
@num_steps = 0
|
17
|
-
@current_state = nil
|
18
|
-
end
|
19
|
-
|
20
|
-
# Returns statistics information as a `Hash` object.
|
21
|
-
def stats
|
22
|
-
{ num_calls: @num_calls, num_queries: @num_queries, num_steps: @num_steps }
|
23
|
-
end
|
24
|
-
|
25
|
-
# Finds a conterexample against the given `hypothesis` automaton.
|
26
|
-
# If it is found, it returns the counterexample inputs, or it returns `nil` otherwise.
|
27
|
-
#
|
28
|
-
# This is *abstract*.
|
29
|
-
def find_cex(_hypothesis)
|
30
|
-
raise TypeError, "abstract method: `step`"
|
31
|
-
end
|
32
|
-
|
33
|
-
# Resets the internal states of this oracle.
|
34
|
-
def reset_internal(hypothesis)
|
35
|
-
@current_state = hypothesis.initial_state
|
36
|
-
|
37
|
-
@sul.shutdown
|
38
|
-
@sul.setup
|
39
|
-
|
40
|
-
@num_queries += 1
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
# This equivalence oracles uses bradth-first exploration of all possible input
|
45
|
-
# combinations up to a specified depth for equivalence checking.
|
46
|
-
class BreadthFirstExplorationOracle < Oracle
|
47
|
-
def initialize(alphabet, sul, depth: 5)
|
48
|
-
super(alphabet, sul)
|
49
|
-
|
50
|
-
@depth = depth
|
51
|
-
end
|
52
|
-
|
53
|
-
# Finds a conterexample against the given `hypothesis` automaton.
|
54
|
-
def find_cex(hypothesis)
|
55
|
-
@num_calls += 1
|
56
|
-
|
57
|
-
@alphabet.product(*[@alphabet] * (@depth - 1)) do |inputs|
|
58
|
-
reset_internal(hypothesis)
|
59
|
-
|
60
|
-
inputs.each_with_index do |input, i|
|
61
|
-
@num_steps += 1
|
62
|
-
h_out, @current_state = hypothesis.step(@current_state, input)
|
63
|
-
s_out = @sul.step(input)
|
64
|
-
|
65
|
-
if h_out != s_out
|
66
|
-
@sul.shutdown
|
67
|
-
return inputs[0..i]
|
68
|
-
end
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
nil
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
# This equivalence oracles uses random-walk exploration for equivalence checking.
|
77
|
-
class RandomWalkOracle < Oracle
|
78
|
-
def initialize(alphabet, sul, step_limit: 500, reset_prob: 0.09)
|
79
|
-
super(alphabet, sul)
|
80
|
-
|
81
|
-
@step_limit = step_limit
|
82
|
-
@reset_prob = reset_prob
|
83
|
-
end
|
84
|
-
|
85
|
-
# Finds a conterexample against the given `hypothesis` automaton.
|
86
|
-
def find_cex(hypothesis)
|
87
|
-
@num_calls += 1
|
88
|
-
|
89
|
-
random_steps_done = 0
|
90
|
-
inputs = []
|
91
|
-
reset_internal(hypothesis)
|
92
|
-
|
93
|
-
while random_steps_done < @step_limit
|
94
|
-
random_steps_done += 1
|
95
|
-
|
96
|
-
if rand < @reset_prob
|
97
|
-
inputs = []
|
98
|
-
reset_internal(hypothesis)
|
99
|
-
end
|
100
|
-
|
101
|
-
inputs << @alphabet.sample
|
102
|
-
|
103
|
-
@num_steps += 1
|
104
|
-
h_out, @current_state = hypothesis.step(@current_state, inputs.last)
|
105
|
-
s_out = @sul.step(inputs.last)
|
106
|
-
|
107
|
-
if h_out != s_out
|
108
|
-
@sul.shutdown
|
109
|
-
return inputs
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
nil
|
114
|
-
end
|
115
|
-
end
|
116
|
-
end
|