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
@@ -0,0 +1,367 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rbs_inline: enabled
|
3
|
+
|
4
|
+
module Lernen
|
5
|
+
module Algorithm
|
6
|
+
module LSharp
|
7
|
+
# LSharpLearner is an implementation of L# algorithm.
|
8
|
+
#
|
9
|
+
# L# is introduced by [Vaandrager et al. (2022) "A New Approach for Active
|
10
|
+
# Automata Learning Based on Apartness"](https://link.springer.com/chapter/10.1007/978-3-030-99524-9_12).
|
11
|
+
#
|
12
|
+
# @rbs generic In -- Type for input alphabet
|
13
|
+
# @rbs generic Out -- Type for output values
|
14
|
+
class LSharpLearner < Learner #[In, Out]
|
15
|
+
# @rbs @alphabet: Array[In]
|
16
|
+
# @rbs @sul: System::SUL[In, Out]
|
17
|
+
# @rbs @oracle: Equiv::Oracle[In, Out]
|
18
|
+
# @rbs @automaton_type: :dfa | :mealy | :moore
|
19
|
+
# @rbs @tree: ObservationTree[In, Out]
|
20
|
+
# @rbs @witness_cache: Hash[[Array[In], Array[In]], Array[In]]
|
21
|
+
# @rbs @basis: Array[Array[In]]
|
22
|
+
# @rbs @frontier: Hash[Array[In], Array[Array[In]]]
|
23
|
+
# @rbs @incomplete_basis: Array[Array[In]]
|
24
|
+
|
25
|
+
#: (
|
26
|
+
# Array[In] alphabet,
|
27
|
+
# System::SUL[In, Out] sul,
|
28
|
+
# automaton_type: :dfa | :mealy | :moore,
|
29
|
+
# ) -> void
|
30
|
+
def initialize(alphabet, sul, automaton_type:)
|
31
|
+
super()
|
32
|
+
|
33
|
+
@alphabet = alphabet.dup
|
34
|
+
@sul = sul
|
35
|
+
@automaton_type = automaton_type
|
36
|
+
|
37
|
+
@tree = ObservationTree.new(sul, automaton_type:)
|
38
|
+
@witness_cache = {}
|
39
|
+
|
40
|
+
@basis = []
|
41
|
+
@basis_set = Set.new
|
42
|
+
@frontier = {}
|
43
|
+
|
44
|
+
@incomplete_basis = []
|
45
|
+
|
46
|
+
add_basis([])
|
47
|
+
end
|
48
|
+
|
49
|
+
# @rbs override
|
50
|
+
def build_hypothesis
|
51
|
+
loop do
|
52
|
+
next if promotion || completion || identification
|
53
|
+
|
54
|
+
hypothesis, state_to_prefix = build_hypothesis_internal
|
55
|
+
cex = check_consistency(hypothesis, state_to_prefix)
|
56
|
+
if cex
|
57
|
+
process_cex(cex, hypothesis, state_to_prefix)
|
58
|
+
update_frontier
|
59
|
+
next
|
60
|
+
end
|
61
|
+
|
62
|
+
return hypothesis, state_to_prefix
|
63
|
+
end
|
64
|
+
|
65
|
+
raise "BUG: unreachable"
|
66
|
+
end
|
67
|
+
|
68
|
+
# @rbs override
|
69
|
+
def refine_hypothesis(cex, hypothesis, state_to_prefix)
|
70
|
+
@tree.query(cex)
|
71
|
+
|
72
|
+
node = @tree.root
|
73
|
+
state = hypothesis.initial_conf
|
74
|
+
cex.size.times do |n|
|
75
|
+
input = cex[n]
|
76
|
+
node = node.branch[input]
|
77
|
+
|
78
|
+
_, state = hypothesis.step(state, input)
|
79
|
+
state_node = @tree[state_to_prefix[state]]
|
80
|
+
raise "BUG: A node for the basis prefix must exist" unless state_node
|
81
|
+
|
82
|
+
if check_apartness(state_node, node)
|
83
|
+
cex = cex[0..n]
|
84
|
+
break
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
process_cex(cex, hypothesis, state_to_prefix)
|
89
|
+
end
|
90
|
+
|
91
|
+
# @rbs override
|
92
|
+
def add_alphabet(input)
|
93
|
+
@alphabet << input
|
94
|
+
@incomplete_basis = @basis.dup
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# Checks apartness on the current observation tree between the given two nodes.
|
100
|
+
# It returns the witness suffix if they are apart. If it is not, it returns `nil`.
|
101
|
+
#
|
102
|
+
#: (
|
103
|
+
# ObservationTree::Node[In, Out] node1,
|
104
|
+
# ObservationTree::Node[In, Out] node2
|
105
|
+
# ) -> (Array[In] | nil)
|
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.branch.each do |input, next_node1|
|
120
|
+
next_node2 = node2.branch[input]
|
121
|
+
next unless next_node2
|
122
|
+
return suffix + [input] if next_node1.output != next_node2.output # steep:ignore
|
123
|
+
queue << [suffix + [input], next_node1, next_node2]
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
nil
|
128
|
+
end
|
129
|
+
|
130
|
+
# Computes the witness suffix of apartness between the given two basis prefixes.
|
131
|
+
#
|
132
|
+
#: (
|
133
|
+
# Array[In] prefix1,
|
134
|
+
# Array[In] prefix2
|
135
|
+
# ) -> Array[In]
|
136
|
+
def compute_witness(prefix1, prefix2)
|
137
|
+
cached = @witness_cache[[prefix1, prefix2]]
|
138
|
+
return cached if cached
|
139
|
+
|
140
|
+
node1 = @tree[prefix1]
|
141
|
+
node2 = @tree[prefix2]
|
142
|
+
raise "BUG: Nodes for the basis prefixes must exist" unless node1 && node2
|
143
|
+
|
144
|
+
witness = check_apartness(node1, node2)
|
145
|
+
raise "BUG: A witness prefix for two basis prefixes must exist" unless witness
|
146
|
+
|
147
|
+
@witness_cache[[prefix1, prefix2]] = witness
|
148
|
+
|
149
|
+
witness
|
150
|
+
end
|
151
|
+
|
152
|
+
#: (Array[In] prefix) -> void
|
153
|
+
def add_basis(prefix)
|
154
|
+
@basis << prefix
|
155
|
+
@incomplete_basis << prefix
|
156
|
+
prefix_node = @tree[prefix]
|
157
|
+
raise "BUG: A node for the basis prefix must exist" unless prefix_node
|
158
|
+
|
159
|
+
@frontier.each do |border, eq_prefixes|
|
160
|
+
border_node = @tree[border]
|
161
|
+
raise "BUG: A node for the frontier prefix must exist" unless border_node
|
162
|
+
|
163
|
+
eq_prefixes << prefix unless check_apartness(prefix_node, border_node)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
#: (Array[In] border) -> void
|
168
|
+
def add_frontier(border)
|
169
|
+
border_node = @tree[border]
|
170
|
+
raise "BUG: A node for the frontier prefix must exist" unless border_node
|
171
|
+
|
172
|
+
@frontier[border] = @basis.filter do |prefix|
|
173
|
+
prefix_node = @tree[prefix]
|
174
|
+
raise "BUG: A node for the basis prefix must exist" unless prefix_node
|
175
|
+
|
176
|
+
check_apartness(prefix_node, border_node).nil?
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
#: () -> void
|
181
|
+
def update_frontier
|
182
|
+
@frontier.each do |border, eq_prefixes|
|
183
|
+
border_node = @tree[border]
|
184
|
+
raise "BUG: A node for the frontier prefix must exist" unless border_node
|
185
|
+
|
186
|
+
eq_prefixes.filter! do |prefix|
|
187
|
+
prefix_node = @tree[prefix]
|
188
|
+
raise "BUG: A node for the basis prefix must exist" unless prefix_node
|
189
|
+
|
190
|
+
check_apartness(prefix_node, border_node).nil?
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
#: () -> [Automaton::TransitionSystem[Integer, In, Out], Hash[Integer, Array[In]]]
|
196
|
+
def build_hypothesis_internal
|
197
|
+
transitions = {}
|
198
|
+
prefix_to_state = @basis.each_with_index.to_h
|
199
|
+
|
200
|
+
@basis.each do |prefix|
|
201
|
+
state = prefix_to_state[prefix]
|
202
|
+
node = @tree[prefix]
|
203
|
+
raise "BUG: A node for the basis prefix must exist" unless node
|
204
|
+
|
205
|
+
@alphabet.each do |input|
|
206
|
+
next_node = node.branch[input]
|
207
|
+
next_prefix = prefix + [input]
|
208
|
+
next_state =
|
209
|
+
(
|
210
|
+
if @frontier.include?(next_prefix)
|
211
|
+
prefix_to_state[@frontier[next_prefix].first]
|
212
|
+
else
|
213
|
+
prefix_to_state[next_prefix]
|
214
|
+
end
|
215
|
+
)
|
216
|
+
|
217
|
+
case @automaton_type
|
218
|
+
in :dfa | :moore
|
219
|
+
transitions[[state, input]] = next_state
|
220
|
+
in :mealy
|
221
|
+
transitions[[state, input]] = [next_node.output, next_state]
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
state_to_prefix = prefix_to_state.to_h { |q, i| [i, q] }
|
227
|
+
automaton =
|
228
|
+
case @automaton_type
|
229
|
+
in :dfa
|
230
|
+
accept_states =
|
231
|
+
state_to_prefix
|
232
|
+
.to_a
|
233
|
+
.filter { |(_, prefix)| @tree.observed_query(prefix).last }
|
234
|
+
.to_set { |(state, _)| state }
|
235
|
+
Automaton::DFA.new(0, accept_states, transitions)
|
236
|
+
in :moore
|
237
|
+
outputs = state_to_prefix.transform_values { |state| @tree.observed_query(state).last }
|
238
|
+
Automaton::Moore.new(0, outputs, transitions)
|
239
|
+
in :mealy
|
240
|
+
Automaton::Mealy.new(0, transitions)
|
241
|
+
end
|
242
|
+
|
243
|
+
[automaton, state_to_prefix]
|
244
|
+
end
|
245
|
+
|
246
|
+
#: (
|
247
|
+
# Automaton::TransitionSystem[Integer, In, Out] hypothesis,
|
248
|
+
# Hash[Integer, Array[In]] state_to_prefix
|
249
|
+
# ) -> (Array[In] | nil)
|
250
|
+
def check_consistency(hypothesis, state_to_prefix)
|
251
|
+
queue = []
|
252
|
+
queue << [[], hypothesis.initial_conf, @tree.root]
|
253
|
+
|
254
|
+
until queue.empty?
|
255
|
+
prefix, state, node = queue.shift
|
256
|
+
state_prefix = state_to_prefix[state]
|
257
|
+
next unless state_prefix
|
258
|
+
|
259
|
+
state_node = @tree[state_prefix]
|
260
|
+
raise "BUG: A node for the basis prefix must exist" unless state_node
|
261
|
+
return prefix if check_apartness(node, state_node)
|
262
|
+
|
263
|
+
node.branch.each do |input, next_node|
|
264
|
+
_, next_state = hypothesis.step(state, input)
|
265
|
+
queue << [prefix + [input], next_state, next_node]
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
nil
|
270
|
+
end
|
271
|
+
|
272
|
+
#: () -> bool
|
273
|
+
def promotion
|
274
|
+
@frontier.each do |new_prefix, eq_prefixes|
|
275
|
+
next unless eq_prefixes.empty?
|
276
|
+
|
277
|
+
@frontier.delete(new_prefix)
|
278
|
+
add_basis(new_prefix)
|
279
|
+
|
280
|
+
return true
|
281
|
+
end
|
282
|
+
|
283
|
+
false
|
284
|
+
end
|
285
|
+
|
286
|
+
#: () -> bool
|
287
|
+
def completion
|
288
|
+
updated = false
|
289
|
+
|
290
|
+
while (prefix = @incomplete_basis.pop)
|
291
|
+
prefix_node = @tree[prefix]
|
292
|
+
raise "BUG: A node for the basis prefix must exist" unless prefix_node
|
293
|
+
|
294
|
+
@alphabet.each do |input|
|
295
|
+
border = prefix + [input]
|
296
|
+
next if prefix_node.branch[input] && (@basis.include?(border) || @frontier.include?(border))
|
297
|
+
|
298
|
+
@tree.query(border)
|
299
|
+
add_frontier(border)
|
300
|
+
|
301
|
+
updated = true
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
updated
|
306
|
+
end
|
307
|
+
|
308
|
+
#: () -> bool
|
309
|
+
def identification
|
310
|
+
@frontier.each do |border, eq_prefixes|
|
311
|
+
next unless eq_prefixes.size >= 2
|
312
|
+
|
313
|
+
prefix1 = eq_prefixes[0]
|
314
|
+
prefix2 = eq_prefixes[1]
|
315
|
+
witness = compute_witness(prefix1, prefix2)
|
316
|
+
@tree.query(border + witness)
|
317
|
+
update_frontier
|
318
|
+
|
319
|
+
return true
|
320
|
+
end
|
321
|
+
|
322
|
+
false
|
323
|
+
end
|
324
|
+
|
325
|
+
#: (
|
326
|
+
# Array[In] cex,
|
327
|
+
# Automaton::TransitionSystem[Integer, In, Out] hypothesis,
|
328
|
+
# Hash[Integer, Array[In]] state_to_prefix,
|
329
|
+
# ) -> void
|
330
|
+
def process_cex(cex, hypothesis, state_to_prefix)
|
331
|
+
border = @frontier.keys.find { cex[0..._1.size] == _1 }
|
332
|
+
raise ArgumentError, "A border must exist" unless border
|
333
|
+
|
334
|
+
while border.size < cex.size
|
335
|
+
_, state = hypothesis.run(cex)
|
336
|
+
state_node = @tree[state_to_prefix[state]]
|
337
|
+
node = @tree[cex]
|
338
|
+
raise "BUG: Nodes for the basis prefix and `cex` must exist" unless state_node && node
|
339
|
+
|
340
|
+
witness = check_apartness(state_node, node)
|
341
|
+
raise ArgumentError, "A witness prefix for `cex` and its hypothesis state prefix must exist" unless witness
|
342
|
+
|
343
|
+
mid = border.size + ((cex.size - border.size) / 2)
|
344
|
+
cex1 = cex[0...mid]
|
345
|
+
cex2 = cex[mid...]
|
346
|
+
|
347
|
+
_, state1 = hypothesis.run(cex1) # steep:ignore
|
348
|
+
state1_prefix = state_to_prefix[state1]
|
349
|
+
@tree.query(state1_prefix + cex2 + witness) # steep:ignore
|
350
|
+
|
351
|
+
state1_node = @tree[state1_prefix]
|
352
|
+
node1 = @tree[cex1] # steep:ignore
|
353
|
+
raise "BUG: Nodes for the basis prefix and the prefix of `cex` must exist" unless state1_node && node1
|
354
|
+
|
355
|
+
if check_apartness(state1_node, node1)
|
356
|
+
cex = cex1
|
357
|
+
else
|
358
|
+
cex = state1_prefix + cex2 # steep:ignore
|
359
|
+
border = @frontier.keys.find { cex[0..._1.size] == _1 }
|
360
|
+
raise ArgumentError, "A border must exist" unless border
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
367
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rbs_inline: enabled
|
3
|
+
|
4
|
+
module Lernen
|
5
|
+
module Algorithm
|
6
|
+
module LSharp
|
7
|
+
# ObservationTree is an implementation of observation tree data structure.
|
8
|
+
#
|
9
|
+
# This data structure is used for L# algorithm.
|
10
|
+
#
|
11
|
+
# @rbs generic In -- Type for input alphabet
|
12
|
+
# @rbs generic Out -- Type for output values
|
13
|
+
class ObservationTree
|
14
|
+
# Node is a node of an observation tree.
|
15
|
+
#
|
16
|
+
# `output` can take `nil` for the root node on learning Mealy machine.
|
17
|
+
#
|
18
|
+
# @rbs skip
|
19
|
+
Node =
|
20
|
+
Data.define(:output, :branch) do
|
21
|
+
def to_s
|
22
|
+
"#{output&.inspect}(#{branch.map { |c, n| "#{c.inspect} -> #{n}" }.join(", ")})" # steep:ignore
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @rbs!
|
27
|
+
# class Node[In, Out] < Data
|
28
|
+
# attr_reader output: Out | nil
|
29
|
+
# attr_reader branch: Hash[In, Node[In, Out]]
|
30
|
+
# def self.[]: [In, Out] (
|
31
|
+
# Out output,
|
32
|
+
# Hash[In, Node[In, Out]] branch
|
33
|
+
# ) -> Node[In, Out]
|
34
|
+
# end
|
35
|
+
|
36
|
+
# @rbs @sul: System::SUL[In, Out]
|
37
|
+
# @rbs @automaton_type: :dfa | :mealy | :moore
|
38
|
+
# @rbs @root: Node[In, Out]
|
39
|
+
|
40
|
+
#: (
|
41
|
+
# System::SUL[In, Out] sul,
|
42
|
+
# automaton_type: :dfa | :mealy | :moore
|
43
|
+
# ) -> void
|
44
|
+
def initialize(sul, automaton_type:)
|
45
|
+
@sul = sul
|
46
|
+
@automaton_type = automaton_type
|
47
|
+
|
48
|
+
case automaton_type
|
49
|
+
in :dfa | :moore
|
50
|
+
raise "MooreLikeSUL is required to learn DFA or Moore" unless sul.is_a?(System::MooreLikeSUL)
|
51
|
+
@root = Node[sul.query_empty, {}]
|
52
|
+
in :mealy
|
53
|
+
@root = Node[nil, {}]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
attr_reader :root #: Node[In, Out]
|
58
|
+
|
59
|
+
# Returns a node for the given word.
|
60
|
+
# When the word (or its subword) is not observed, it returns `nil` instead.
|
61
|
+
#
|
62
|
+
#: (Array[In] word) -> (Node[In, Out] | nil)
|
63
|
+
def [](word)
|
64
|
+
node = @root
|
65
|
+
word.each do |input|
|
66
|
+
return nil unless node.branch[input]
|
67
|
+
node = node.branch[input]
|
68
|
+
end
|
69
|
+
node
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns an output sequence for the given word if it is observed.
|
73
|
+
# If it is not, it returns `nil` instead.
|
74
|
+
#
|
75
|
+
#: (Array[In] word) -> (Array[Out] | nil)
|
76
|
+
def observed_query(word)
|
77
|
+
return [@root.output] if word.empty? # steep:ignore
|
78
|
+
|
79
|
+
node = @root
|
80
|
+
outputs = []
|
81
|
+
word.each do |input|
|
82
|
+
node = node.branch[input]
|
83
|
+
return nil unless node
|
84
|
+
|
85
|
+
outputs << node.output
|
86
|
+
end
|
87
|
+
|
88
|
+
outputs
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns an output sequence for the given word.
|
92
|
+
# When the word is observed, it runs actual query over `sul`.
|
93
|
+
#
|
94
|
+
#: (Array[In] word) -> Array[Out]
|
95
|
+
def query(word)
|
96
|
+
outputs = observed_query(word)
|
97
|
+
return outputs if outputs
|
98
|
+
|
99
|
+
node = @root
|
100
|
+
inprogress_word = []
|
101
|
+
outputs = []
|
102
|
+
word.each do |input|
|
103
|
+
inprogress_word << input
|
104
|
+
output = @sul.query_last(inprogress_word)
|
105
|
+
outputs << output
|
106
|
+
node.branch[input] ||= Node[output, {}] # steep:ignore
|
107
|
+
node = node.branch[input]
|
108
|
+
end
|
109
|
+
|
110
|
+
outputs
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rbs_inline: enabled
|
3
|
+
|
4
|
+
require "lernen/algorithm/lsharp/observation_tree"
|
5
|
+
require "lernen/algorithm/lsharp/lsharp_learner"
|
6
|
+
|
7
|
+
module Lernen
|
8
|
+
module Algorithm
|
9
|
+
# LSharp provides an implementation of L# algorithm.
|
10
|
+
#
|
11
|
+
# L# is introduced by [Vaandrager et al. (2022) "A New Approach for Active
|
12
|
+
# Automata Learning Based on Apartness"](https://link.springer.com/chapter/10.1007/978-3-030-99524-9_12).
|
13
|
+
module LSharp
|
14
|
+
# Runs the L# algorithm and returns an inferred automaton.
|
15
|
+
#
|
16
|
+
#: [In] (
|
17
|
+
# Array[In] alphabet,
|
18
|
+
# System::SUL[In, bool] sul,
|
19
|
+
# Equiv::Oracle[In, bool] oracle,
|
20
|
+
# automaton_type: :dfa,
|
21
|
+
# ?max_learning_rounds: Integer | nil
|
22
|
+
# ) -> Automaton::DFA[In]
|
23
|
+
#: [In, Out] (
|
24
|
+
# Array[In] alphabet,
|
25
|
+
# System::SUL[In, Out] sul,
|
26
|
+
# Equiv::Oracle[In, Out] oracle,
|
27
|
+
# automaton_type: :mealy,
|
28
|
+
# ?max_learning_rounds: Integer | nil
|
29
|
+
# ) -> Automaton::Mealy[In, Out]
|
30
|
+
#: [In, Out] (
|
31
|
+
# Array[In] alphabet,
|
32
|
+
# System::SUL[In, Out] sul,
|
33
|
+
# Equiv::Oracle[In, Out] oracle,
|
34
|
+
# automaton_type: :moore,
|
35
|
+
# ?max_learning_rounds: Integer | nil
|
36
|
+
# ) -> Automaton::Moore[In, Out]
|
37
|
+
def self.learn(alphabet, sul, oracle, automaton_type:, max_learning_rounds: nil) # steep:ignore
|
38
|
+
learner = LSharpLearner.new(alphabet, sul, automaton_type:)
|
39
|
+
learner.learn(oracle, max_learning_rounds:)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# rbs_inline: enabled
|
3
|
+
|
4
|
+
module Lernen
|
5
|
+
module Algorithm
|
6
|
+
module LStar
|
7
|
+
# LStarLearner is an implementation of Angluin's L* algorithm.
|
8
|
+
#
|
9
|
+
# Angluin's L* is introduced by [Angluin (1987) "Learning Regular Sets from
|
10
|
+
# Queries and Counterexamples"](https://dl.acm.org/doi/10.1016/0890-5401%2887%2990052-6).
|
11
|
+
#
|
12
|
+
# @rbs generic In -- Type for input alphabet
|
13
|
+
# @rbs generic Out -- Type for output values
|
14
|
+
class LStarLearner < Learner #[In, Out]
|
15
|
+
# @rbs @alphabet: Array[In]
|
16
|
+
# @rbs @oracle: Equiv::Oracle[In, Out]
|
17
|
+
# @rbs @table: ObservationTable[In, Out]
|
18
|
+
|
19
|
+
#: (
|
20
|
+
# Array[In] alphabet, System::SUL[In, Out] sul,
|
21
|
+
# automaton_type: :dfa | :moore | :mealy,
|
22
|
+
# ?cex_processing: cex_processing_method | nil
|
23
|
+
# ) -> void
|
24
|
+
def initialize(alphabet, sul, automaton_type:, cex_processing: :binary)
|
25
|
+
super()
|
26
|
+
|
27
|
+
@alphabet = alphabet.dup
|
28
|
+
|
29
|
+
@table = ObservationTable.new(@alphabet, sul, automaton_type:, cex_processing:)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @rbs override
|
33
|
+
def build_hypothesis
|
34
|
+
@table.build_hypothesis
|
35
|
+
end
|
36
|
+
|
37
|
+
# @rbs override
|
38
|
+
def refine_hypothesis(cex, hypothesis, state_to_prefix)
|
39
|
+
@table.refine_hypothesis(cex, hypothesis, state_to_prefix)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @rbs override
|
43
|
+
def add_alphabet(input)
|
44
|
+
@alphabet << input
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|