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.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -0
  3. data/README.md +534 -48
  4. data/Rakefile +26 -7
  5. data/Steepfile +14 -0
  6. data/examples/ripper_prism.rb +63 -0
  7. data/examples/uri_parse_regexp.rb +73 -0
  8. data/lib/lernen/algorithm/cex_processor/acex.rb +43 -0
  9. data/lib/lernen/algorithm/cex_processor/prefix_transformer_acex.rb +43 -0
  10. data/lib/lernen/algorithm/cex_processor.rb +115 -0
  11. data/lib/lernen/algorithm/kearns_vazirani/discrimination_tree.rb +207 -0
  12. data/lib/lernen/algorithm/kearns_vazirani/kearns_vazirani_learner.rb +100 -0
  13. data/lib/lernen/algorithm/kearns_vazirani.rb +44 -0
  14. data/lib/lernen/algorithm/kearns_vazirani_vpa/discrimination_tree_vpa.rb +246 -0
  15. data/lib/lernen/algorithm/kearns_vazirani_vpa/kearns_vazirani_vpa_learner.rb +89 -0
  16. data/lib/lernen/algorithm/kearns_vazirani_vpa.rb +35 -0
  17. data/lib/lernen/algorithm/learner.rb +82 -0
  18. data/lib/lernen/algorithm/lsharp/lsharp_learner.rb +367 -0
  19. data/lib/lernen/algorithm/lsharp/observation_tree.rb +115 -0
  20. data/lib/lernen/algorithm/lsharp.rb +43 -0
  21. data/lib/lernen/algorithm/lstar/lstar_learner.rb +49 -0
  22. data/lib/lernen/algorithm/lstar/observation_table.rb +214 -0
  23. data/lib/lernen/algorithm/lstar.rb +49 -0
  24. data/lib/lernen/algorithm/procedural/atr_manager.rb +200 -0
  25. data/lib/lernen/algorithm/procedural/procedural_learner.rb +223 -0
  26. data/lib/lernen/algorithm/procedural/procedural_sul.rb +47 -0
  27. data/lib/lernen/algorithm/procedural/return_indices_acex.rb +58 -0
  28. data/lib/lernen/algorithm/procedural.rb +57 -0
  29. data/lib/lernen/algorithm.rb +19 -0
  30. data/lib/lernen/automaton/dfa.rb +204 -0
  31. data/lib/lernen/automaton/mealy.rb +108 -0
  32. data/lib/lernen/automaton/moore.rb +122 -0
  33. data/lib/lernen/automaton/moore_like.rb +83 -0
  34. data/lib/lernen/automaton/proc_util.rb +93 -0
  35. data/lib/lernen/automaton/spa.rb +368 -0
  36. data/lib/lernen/automaton/transition_system.rb +209 -0
  37. data/lib/lernen/automaton/vpa.rb +300 -0
  38. data/lib/lernen/automaton.rb +19 -493
  39. data/lib/lernen/equiv/combined_oracle.rb +57 -0
  40. data/lib/lernen/equiv/exhaustive_search_oracle.rb +60 -0
  41. data/lib/lernen/equiv/moore_like_simulator_oracle.rb +36 -0
  42. data/lib/lernen/equiv/oracle.rb +109 -0
  43. data/lib/lernen/equiv/random_walk_oracle.rb +69 -0
  44. data/lib/lernen/equiv/random_well_matched_word_oracle.rb +139 -0
  45. data/lib/lernen/equiv/random_word_oracle.rb +71 -0
  46. data/lib/lernen/equiv/spa_simulator_oracle.rb +39 -0
  47. data/lib/lernen/equiv/test_words_oracle.rb +42 -0
  48. data/lib/lernen/equiv/transition_system_simulator_oracle.rb +36 -0
  49. data/lib/lernen/equiv/vpa_simulator_oracle.rb +48 -0
  50. data/lib/lernen/equiv.rb +25 -0
  51. data/lib/lernen/graph.rb +215 -0
  52. data/lib/lernen/system/block_sul.rb +41 -0
  53. data/lib/lernen/system/moore_like_simulator.rb +45 -0
  54. data/lib/lernen/system/moore_like_sul.rb +33 -0
  55. data/lib/lernen/system/sul.rb +126 -0
  56. data/lib/lernen/system/transition_system_simulator.rb +40 -0
  57. data/lib/lernen/system.rb +72 -0
  58. data/lib/lernen/version.rb +2 -1
  59. data/lib/lernen.rb +284 -34
  60. data/rbs_collection.lock.yaml +16 -0
  61. data/rbs_collection.yaml +14 -0
  62. data/renovate.json +6 -0
  63. data/sig/generated/lernen/algorithm/cex_processor/acex.rbs +30 -0
  64. data/sig/generated/lernen/algorithm/cex_processor/prefix_transformer_acex.rbs +27 -0
  65. data/sig/generated/lernen/algorithm/cex_processor.rbs +59 -0
  66. data/sig/generated/lernen/algorithm/kearns_vazirani/discrimination_tree.rbs +68 -0
  67. data/sig/generated/lernen/algorithm/kearns_vazirani/kearns_vazirani_learner.rbs +51 -0
  68. data/sig/generated/lernen/algorithm/kearns_vazirani.rbs +32 -0
  69. data/sig/generated/lernen/algorithm/kearns_vazirani_vpa/discrimination_tree_vpa.rbs +73 -0
  70. data/sig/generated/lernen/algorithm/kearns_vazirani_vpa/kearns_vazirani_vpa_learner.rbs +51 -0
  71. data/sig/generated/lernen/algorithm/kearns_vazirani_vpa.rbs +20 -0
  72. data/sig/generated/lernen/algorithm/learner.rbs +53 -0
  73. data/sig/generated/lernen/algorithm/lsharp/lsharp_learner.rbs +103 -0
  74. data/sig/generated/lernen/algorithm/lsharp/observation_tree.rbs +53 -0
  75. data/sig/generated/lernen/algorithm/lsharp.rbs +38 -0
  76. data/sig/generated/lernen/algorithm/lstar/lstar_learner.rbs +38 -0
  77. data/sig/generated/lernen/algorithm/lstar/observation_table.rbs +79 -0
  78. data/sig/generated/lernen/algorithm/lstar.rbs +37 -0
  79. data/sig/generated/lernen/algorithm/procedural/atr_manager.rbs +80 -0
  80. data/sig/generated/lernen/algorithm/procedural/procedural_learner.rbs +79 -0
  81. data/sig/generated/lernen/algorithm/procedural/procedural_sul.rbs +36 -0
  82. data/sig/generated/lernen/algorithm/procedural/return_indices_acex.rbs +33 -0
  83. data/sig/generated/lernen/algorithm/procedural.rbs +27 -0
  84. data/sig/generated/lernen/algorithm.rbs +10 -0
  85. data/sig/generated/lernen/automaton/dfa.rbs +93 -0
  86. data/sig/generated/lernen/automaton/mealy.rbs +61 -0
  87. data/sig/generated/lernen/automaton/moore.rbs +69 -0
  88. data/sig/generated/lernen/automaton/moore_like.rbs +63 -0
  89. data/sig/generated/lernen/automaton/proc_util.rbs +38 -0
  90. data/sig/generated/lernen/automaton/spa.rbs +125 -0
  91. data/sig/generated/lernen/automaton/transition_system.rbs +108 -0
  92. data/sig/generated/lernen/automaton/vpa.rbs +109 -0
  93. data/sig/generated/lernen/automaton.rbs +15 -0
  94. data/sig/generated/lernen/equiv/combined_oracle.rbs +27 -0
  95. data/sig/generated/lernen/equiv/exhaustive_search_oracle.rbs +38 -0
  96. data/sig/generated/lernen/equiv/moore_like_simulator_oracle.rbs +27 -0
  97. data/sig/generated/lernen/equiv/oracle.rbs +75 -0
  98. data/sig/generated/lernen/equiv/random_walk_oracle.rbs +41 -0
  99. data/sig/generated/lernen/equiv/random_well_matched_word_oracle.rbs +70 -0
  100. data/sig/generated/lernen/equiv/random_word_oracle.rbs +45 -0
  101. data/sig/generated/lernen/equiv/spa_simulator_oracle.rbs +30 -0
  102. data/sig/generated/lernen/equiv/test_words_oracle.rbs +20 -0
  103. data/sig/generated/lernen/equiv/transition_system_simulator_oracle.rbs +27 -0
  104. data/sig/generated/lernen/equiv/vpa_simulator_oracle.rbs +33 -0
  105. data/sig/generated/lernen/equiv.rbs +11 -0
  106. data/sig/generated/lernen/graph.rbs +80 -0
  107. data/sig/generated/lernen/system/block_sul.rbs +29 -0
  108. data/sig/generated/lernen/system/moore_like_simulator.rbs +31 -0
  109. data/sig/generated/lernen/system/moore_like_sul.rbs +28 -0
  110. data/sig/generated/lernen/system/sul.rbs +87 -0
  111. data/sig/generated/lernen/system/transition_system_simulator.rbs +28 -0
  112. data/sig/generated/lernen/system.rbs +62 -0
  113. data/sig/generated/lernen/version.rbs +6 -0
  114. data/sig/generated/lernen.rbs +214 -0
  115. data/sig-test/generated/test/example_test.rbs +14 -0
  116. data/sig-test/generated/test/lernen/algorithm/kearns_vazirani_test.rbs +16 -0
  117. data/sig-test/generated/test/lernen/algorithm/kearns_vazirani_vpa_test.rbs +10 -0
  118. data/sig-test/generated/test/lernen/algorithm/lsharp_test.rbs +16 -0
  119. data/sig-test/generated/test/lernen/algorithm/lstar_test.rbs +16 -0
  120. data/sig-test/generated/test/lernen/algorithm/procedural_test.rbs +10 -0
  121. data/sig-test/generated/test/lernen/automaton/dfa_test.rbs +19 -0
  122. data/sig-test/generated/test/lernen/automaton/mealy_test.rbs +19 -0
  123. data/sig-test/generated/test/lernen/automaton/moore_test.rbs +19 -0
  124. data/sig-test/generated/test/lernen/automaton/proc_util_test.rbs +19 -0
  125. data/sig-test/generated/test/lernen/automaton/spa_test.rbs +19 -0
  126. data/sig-test/generated/test/lernen/automaton/vpa_test.rbs +19 -0
  127. data/sig-test/generated/test/lernen/equiv/exhaustive_search_oracle_test.rbs +10 -0
  128. data/sig-test/generated/test/lernen/equiv/random_walk_oracle_test.rbs +10 -0
  129. data/sig-test/generated/test/lernen/equiv/random_word_oracle_test.rbs +10 -0
  130. data/sig-test/generated/test/lernen/system/block_sul_test.rbs +16 -0
  131. data/sig-test/generated/test/lernen/system/moore_like_simulator_test.rbs +16 -0
  132. data/sig-test/generated/test/lernen/system/transition_system_simulator_test.rbs +13 -0
  133. data/sig-test/generated/test/lernen/system_test.rbs +11 -0
  134. data/sig-test/generated/test/lernen_test.rbs +13 -0
  135. metadata +131 -11
  136. data/.yardopts +0 -3
  137. data/lib/lernen/cex_processor.rb +0 -92
  138. data/lib/lernen/kearns_vazirani.rb +0 -310
  139. data/lib/lernen/lsharp.rb +0 -344
  140. data/lib/lernen/lstar.rb +0 -170
  141. data/lib/lernen/oracle.rb +0 -119
  142. data/lib/lernen/sul.rb +0 -210
@@ -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