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.
Files changed (142) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +18 -0
  3. data/README.md +531 -28
  4. data/Rakefile +29 -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 -92
  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 +322 -13
  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 -61
  138. data/lib/lernen/kearns_vazirani.rb +0 -199
  139. data/lib/lernen/lsharp.rb +0 -335
  140. data/lib/lernen/lstar.rb +0 -169
  141. data/lib/lernen/oracle.rb +0 -116
  142. 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