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