lernen 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c925ae55bb57b20dc2ef637e751882ec493d565b7f0b7a8348c858593ef0d5f4
4
- data.tar.gz: dbfae4d012e582aeb909460eea27c30945a62e6586cd8ae7f9be48ac2c6dac6c
3
+ metadata.gz: f98907d83d61890848e0425254b717d3c080ea4a9b596fc6d015bc03b6de71fa
4
+ data.tar.gz: a6231a390522b67f4da4290ce385b043043e172d7045f13ede5398f8dff6131c
5
5
  SHA512:
6
- metadata.gz: c0621a919ee2cebdc932f9aec31aac52ca7cfc528cd8f8f24ff4519e2d39769c4a6c2c5f9eeecad1394339afbe1625a89fb3acb418aa02e452eef2b78eb2a111
7
- data.tar.gz: e88d2feb2c44e766e348c62c05ea15cc5eddcb816077e33db40071957047f31b7eccfeb2f9d8b1679c1907f6a57e116042dac009180b5cef2a4453da1c05f7d1
6
+ metadata.gz: 3d6fbeb6eb3f8e05d34d229201e02304418d182e3804ac33269d00d8c515497906ca7f815711a0efce29a1aaae673b3d80ef4d018e9b7abcb60e44c25d66f366
7
+ data.tar.gz: 8f92777c0fce29e40b1cca5158c7b7bf911eca481ad00084fe1e22b6615c51492f5edd66dfdfb1a823d5ff8a79898d50246ced7dacfb177b688ad2ca77aed56c
data/.rubocop.yml CHANGED
@@ -32,3 +32,6 @@ Metrics/ParameterLists:
32
32
 
33
33
  Metrics/PerceivedComplexity:
34
34
  Enabled: false
35
+
36
+ Style/NumericPredicate:
37
+ Enabled: false
data/README.md CHANGED
@@ -1,42 +1,59 @@
1
1
  # Lernen
2
2
 
3
- > a simple automata learning library.
3
+ > a simple automata learning library written in Ruby.
4
4
 
5
5
  ## Usage
6
6
 
7
7
  ```ruby
8
8
  require "lernen"
9
9
 
10
- alphabet = %w[0 1]
11
- sul = Lernen::SUL.from_block { |inputs| inputs.count { _1 == "1" } % 4 == 3 }
12
- oracle = Lernen::BreadthFirstExplorationOracle.new(alphabet, sul)
13
-
14
- dfa = Lernen::LStar.learn(alphabet, sul, oracle, automaton_type: :dfa)
15
- # => Lernen::DFA.new(
16
- # 0,
17
- # Set[3],
18
- # {
19
- # [0, "0"] => 0,
20
- # [0, "1"] => 1,
21
- # [1, "0"] => 1,
22
- # [1, "1"] => 2,
23
- # [2, "0"] => 2,
24
- # [2, "1"] => 3,
25
- # [3, "0"] => 3,
26
- # [3, "1"] => 0
27
- # }
28
- # )
10
+ automaton = Lernen.learn(alphabet: %w[0 1]) do |inputs|
11
+ inputs.count("0") % 4 == 3
12
+ end
13
+
14
+ puts automaton.to_mermaid
15
+ # => flowchart TD
16
+ # 0((0))
17
+ # 1((1))
18
+ # 2((2))
19
+ # 3(((3)))
20
+ #
21
+ # 0 -- 0 --> 1
22
+ # 0 -- 1 --> 0
23
+ # 1 -- 0 --> 2
24
+ # 1 -- 1 --> 1
25
+ # 2 -- 0 --> 3
26
+ # 2 -- 1 --> 2
27
+ # 3 -- 0 --> 0
28
+ # 3 -- 1 --> 3
29
+ ```
30
+
31
+ ```mermaid
32
+ flowchart TD
33
+ 0((0))
34
+ 1((1))
35
+ 2((2))
36
+ 3(((3)))
37
+
38
+ 0 -- 0 --> 1
39
+ 0 -- 1 --> 0
40
+ 1 -- 0 --> 2
41
+ 1 -- 1 --> 1
42
+ 2 -- 0 --> 3
43
+ 2 -- 1 --> 2
44
+ 3 -- 0 --> 0
45
+ 3 -- 1 --> 3
29
46
  ```
30
47
 
31
48
  ## Algorithms
32
49
 
33
50
  Learnen supports these automata learning algorithms.
34
51
 
35
- | Algorithm | Supported `automaton_type` |
36
- |:----------------:|:--------------------------:|
37
- | `LStar` | `:dfa`, `:moore`, `:mealy` |
38
- | `KearnsVazirani` | `:dfa`, `:moore`, `:mealy` |
39
- | `LSharp` | `:dfa`, `:moore`, `:mealy` |
52
+ | Algorithm | Supported `automaton_type` |
53
+ |:----------------:|:----------------------------------:|
54
+ | `LStar` | `:dfa`, `:moore`, `:mealy` |
55
+ | `KearnsVazirani` | `:dfa`, `:moore`, `:mealy`, `:vpa` |
56
+ | `LSharp` | `:dfa`, `:moore`, `:mealy` |
40
57
 
41
58
  ## License
42
59
 
data/Rakefile CHANGED
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "minitest/test_task"
5
4
  require "yard"
5
+ require "rake/testtask"
6
6
  require "rubocop/rake_task"
7
7
  require "syntax_tree/rake_tasks"
8
8
 
9
- Minitest::TestTask.create
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.verbose = true
11
+ t.pattern = "test/**/*_test.rb"
12
+ end
10
13
 
11
14
  YARD::Rake::YardocTask.new do |t|
12
15
  t.files = ["lib/**/*.rb"]
@@ -5,6 +5,8 @@ module Lernen
5
5
  #
6
6
  # Note that this class is *abstract*. You should implement the following method:
7
7
  #
8
+ # - `#type`
9
+ # - `#initial`
8
10
  # - `#step(state, input)`
9
11
  class Automaton
10
12
  # Computes a transition for the given `input` from the current `state`.
@@ -17,7 +19,7 @@ module Lernen
17
19
  # Runs this automaton with the given input string and returns an output sequence
18
20
  # and a state after running.
19
21
  def run(inputs)
20
- state = @initial_state
22
+ state = initial
21
23
  outputs = []
22
24
  inputs.each do |input|
23
25
  output, state = step(state, input)
@@ -25,6 +27,40 @@ module Lernen
25
27
  end
26
28
  [outputs, state]
27
29
  end
30
+
31
+ # Checks equivalence between `self` and `other` on the given `alphabet`.
32
+ #
33
+ # It returns `nil` if they are equivalence, or it returns a counterexample string.
34
+ def check_equivalence(alphabet, other)
35
+ raise ArgumentError, "Cannot check equivalence between different automata" unless instance_of?(other.class)
36
+
37
+ case self
38
+ when DFA
39
+ return [] unless accept_states.include?(initial_state) == other.accept_states.include?(other.initial_state)
40
+ when Moore
41
+ return [] unless outputs[initial_state] == other.outputs[other.initial_state]
42
+ end
43
+
44
+ queue = []
45
+ visited = Set.new
46
+ queue << [[], initial, other.initial]
47
+ visited << [initial, other.initial]
48
+ until queue.empty?
49
+ path, self_state, other_state = queue.shift
50
+ alphabet.each do |input|
51
+ self_output, self_next_state = step(self_state, input)
52
+ other_output, other_next_state = other.step(other_state, input)
53
+ return path + [input] if self_output != other_output
54
+ next_pair = [self_next_state, other_next_state]
55
+ unless visited.include?(next_pair)
56
+ queue << [path + [input], *next_pair]
57
+ visited << next_pair
58
+ end
59
+ end
60
+ end
61
+
62
+ nil
63
+ end
28
64
  end
29
65
 
30
66
  # DFA is a deterministic finite-state automaton.
@@ -38,6 +74,12 @@ module Lernen
38
74
  end
39
75
 
40
76
  attr_reader :initial_state, :accept_states, :transitions
77
+ alias initial initial_state
78
+
79
+ # Returns the type of this automaton.
80
+ def type
81
+ :dfa
82
+ end
41
83
 
42
84
  # Computes a transition for the given `input` from the current `state`.
43
85
  def step(state, input)
@@ -50,6 +92,75 @@ module Lernen
50
92
  def ==(other)
51
93
  initial_state == other.initial_state && accept_states == other.accept_states && transitions == other.transitions
52
94
  end
95
+
96
+ # Returns a mermaid diagram.
97
+ def to_mermaid
98
+ mmd = +""
99
+
100
+ mmd << "flowchart TD\n"
101
+
102
+ states = [initial_state] + accept_states.to_a + transitions.keys.map { |(q, _)| q } + transitions.values
103
+ states.uniq!
104
+
105
+ states.sort.each { |q| mmd << (accept_states.include?(q) ? " #{q}(((#{q})))\n" : " #{q}((#{q}))\n") }
106
+ mmd << "\n"
107
+
108
+ transitions.each { |(q1, i), q2| mmd << " #{q1} -- \"'#{i}'\" --> #{q2}\n" }
109
+
110
+ mmd.dup
111
+ end
112
+
113
+ # Returns a random DFA.
114
+ #
115
+ # The result DFA is complete, and all states in the result DFA are reachable
116
+ # to some accepting states or the sink state. However, the result DFA may be
117
+ # non-minimal.
118
+ def self.random(
119
+ alphabet:,
120
+ max_state_size:,
121
+ max_accept_state_ratio: 0.5,
122
+ min_state_size: 1,
123
+ sink_state_prob: 0.4,
124
+ random: Random
125
+ )
126
+ state_size = random.rand(min_state_size..max_state_size)
127
+ accept_state_ratio = [max_accept_state_ratio * random.rand, 0.01].max
128
+ accept_state_size = [state_size, (state_size * accept_state_ratio).ceil].min
129
+
130
+ initial_state = 0
131
+ non_accepting_states = (0...state_size).to_a
132
+ non_accepting_states.shuffle!(random:)
133
+ accept_states = non_accepting_states.pop(accept_state_size).to_set
134
+
135
+ sink_state = random.rand < sink_state_prob ? non_accepting_states.pop : nil
136
+
137
+ transitions = {}
138
+ accept_states.each_with_index do |accept_state, i|
139
+ next if accept_state == initial_state
140
+ n = i + 1 == accept_state_size ? non_accepting_states.size : random.rand(non_accepting_states.size)
141
+ state = initial_state
142
+ non_accepting_states
143
+ .pop(n)
144
+ .each do |next_state|
145
+ next if next_state == initial_state
146
+ input = alphabet.sample(random:)
147
+ transitions[[state, input]] = next_state
148
+ state = next_state
149
+ end
150
+ input = alphabet.sample(random:)
151
+ transitions[[state, input]] = accept_state
152
+ end
153
+
154
+ state_size.times do |state|
155
+ alphabet.each do |input|
156
+ next if transitions[[state, input]]
157
+ next_state = state == sink_state ? sink_state : random.rand(state_size)
158
+ transitions[[state, input]] = next_state
159
+ end
160
+ end
161
+
162
+ new(initial_state, accept_states, transitions)
163
+ end
53
164
  end
54
165
 
55
166
  # Moore is a deterministic Moore machine.
@@ -63,6 +174,12 @@ module Lernen
63
174
  end
64
175
 
65
176
  attr_reader :initial_state, :outputs, :transitions
177
+ alias initial initial_state
178
+
179
+ # Returns the type of this automaton.
180
+ def type
181
+ :moore
182
+ end
66
183
 
67
184
  # Computes a transition for the given `input` from the current `state`.
68
185
  def step(state, input)
@@ -75,6 +192,74 @@ module Lernen
75
192
  def ==(other)
76
193
  initial_state == other.initial_state && outputs == other.outputs && transitions == other.transitions
77
194
  end
195
+
196
+ # Returns a mermaid diagram.
197
+ def to_mermaid
198
+ mmd = +""
199
+
200
+ mmd << "flowchart TD\n"
201
+
202
+ states = [initial_state] + transitions.keys.map { |(q, _)| q } + transitions.values
203
+ states.uniq!
204
+
205
+ states.sort.each { |q| mmd << " #{q}((\"#{q}|'#{outputs[q]}'\"))\n" }
206
+ mmd << "\n"
207
+
208
+ transitions.each { |(q1, i), q2| mmd << " #{q1} -- \"'#{i}'\" --> #{q2}\n" }
209
+
210
+ mmd.dup
211
+ end
212
+
213
+ # Returns a random Moore machine.
214
+ def self.random(
215
+ alphabet:,
216
+ output_alphabet:,
217
+ max_state_size:,
218
+ max_arc_ratio: 0.5,
219
+ min_state_size: 1,
220
+ sink_state_prob: 0.4,
221
+ random: Random
222
+ )
223
+ state_size = random.rand(min_state_size..max_state_size)
224
+ arc_ratio = [max_arc_ratio * random.rand, 0.01].max
225
+ arc_state_size = [state_size, (state_size * arc_ratio).ceil].min
226
+
227
+ initial_state = 0
228
+ non_arc_states = (0...state_size).to_a
229
+ non_arc_states.shuffle!(random:)
230
+ arc_states = non_arc_states.pop(arc_state_size).to_set
231
+
232
+ sink_state = random.rand < sink_state_prob ? non_arc_states.pop : nil
233
+
234
+ transitions = {}
235
+ arc_states.each_with_index do |arc_state, i|
236
+ next if arc_state == initial_state
237
+ n = i + 1 == arc_state_size ? non_arc_states.size : random.rand(non_arc_states.size)
238
+ state = initial_state
239
+ non_arc_states
240
+ .pop(n)
241
+ .each do |next_state|
242
+ next if next_state == initial_state
243
+ input = alphabet.sample(random:)
244
+ transitions[[state, input]] = next_state
245
+ state = next_state
246
+ end
247
+ input = alphabet.sample(random:)
248
+ transitions[[state, input]] = arc_state
249
+ end
250
+
251
+ outputs = {}
252
+ state_size.times do |state|
253
+ outputs[state] = output_alphabet.sample(random:)
254
+ alphabet.each do |input|
255
+ next if transitions[[state, input]]
256
+ next_state = state == sink_state ? sink_state : random.rand(state_size)
257
+ transitions[[state, input]] = next_state
258
+ end
259
+ end
260
+
261
+ new(initial_state, outputs, transitions)
262
+ end
78
263
  end
79
264
 
80
265
  # Mealy is a deterministic Mealy machine.
@@ -87,6 +272,12 @@ module Lernen
87
272
  end
88
273
 
89
274
  attr_reader :initial_state, :transitions
275
+ alias initial initial_state
276
+
277
+ # Returns the type of this automaton.
278
+ def type
279
+ :mealy
280
+ end
90
281
 
91
282
  # Computes a transition for the given `input` from the current `state`.
92
283
  def step(state, input)
@@ -97,5 +288,215 @@ module Lernen
97
288
  def ==(other)
98
289
  initial_state == other.initial_state && transitions == other.transitions
99
290
  end
291
+
292
+ # Returns a mermaid diagram.
293
+ def to_mermaid
294
+ mmd = +""
295
+
296
+ mmd << "flowchart TD\n"
297
+
298
+ states = [initial_state] + transitions.keys.map { |(q, _)| q } + transitions.values.map { |(_, q)| q }
299
+ states.uniq!
300
+
301
+ states.sort.each { |q| mmd << " #{q}((#{q}))\n" }
302
+ mmd << "\n"
303
+
304
+ transitions.each { |(q1, i), (o, q2)| mmd << " #{q1} -- \"'#{i}'|'#{o}'\" --> #{q2}\n" }
305
+
306
+ mmd.dup
307
+ end
308
+
309
+ # Returns a random Mealy machine.
310
+ def self.random(
311
+ alphabet:,
312
+ output_alphabet:,
313
+ max_state_size:,
314
+ max_arc_ratio: 0.5,
315
+ min_state_size: 1,
316
+ sink_state_prob: 0.4,
317
+ random: Random
318
+ )
319
+ state_size = random.rand(min_state_size..max_state_size)
320
+ arc_ratio = [max_arc_ratio * random.rand, 0.01].max
321
+ arc_state_size = [state_size, (state_size * arc_ratio).ceil].min
322
+
323
+ initial_state = 0
324
+ non_arc_states = (0...state_size).to_a
325
+ non_arc_states.shuffle!(random:)
326
+ arc_states = non_arc_states.pop(arc_state_size).to_set
327
+
328
+ sink_state = random.rand < sink_state_prob ? non_arc_states.pop : nil
329
+
330
+ transitions = {}
331
+ arc_states.each_with_index do |arc_state, i|
332
+ next if arc_state == initial_state
333
+ n = i + 1 == arc_state_size ? non_arc_states.size : random.rand(non_arc_states.size)
334
+ state = initial_state
335
+ non_arc_states
336
+ .pop(n)
337
+ .each do |next_state|
338
+ next if next_state == initial_state
339
+ input = alphabet.sample(random:)
340
+ output = output_alphabet.sample(random:)
341
+ transitions[[state, input]] = [output, next_state]
342
+ state = next_state
343
+ end
344
+ input = alphabet.sample(random:)
345
+ output = output_alphabet.sample(random:)
346
+ transitions[[state, input]] = [output, arc_state]
347
+ end
348
+
349
+ state_size.times do |state|
350
+ alphabet.each do |input|
351
+ next if transitions[[state, input]]
352
+ output = output_alphabet.sample(random:)
353
+ next_state = state == sink_state ? sink_state : random.rand(state_size)
354
+ transitions[[state, input]] = [output, next_state]
355
+ end
356
+ end
357
+
358
+ new(initial_state, transitions)
359
+ end
360
+ end
361
+
362
+ # VPA is a 1-module single-entry visibly pushdown automaton (1-SEVPA).
363
+ class VPA < Automaton
364
+ # Conf is a configuration on VPAs.
365
+ Conf = Data.define(:state, :stack)
366
+
367
+ # StateToPrefixMapping is a mapping from states to prefix strings.
368
+ #
369
+ # It can transform a configuration to an access string.
370
+ class StateToPrefixMapping
371
+ def initialize(mapping)
372
+ @mapping = mapping
373
+ end
374
+
375
+ # Transforms a configuration to an access string.
376
+ def [](conf)
377
+ return @mapping[nil] unless conf
378
+ result = []
379
+
380
+ conf.stack.each do |state, call_input|
381
+ result.concat(@mapping[state])
382
+ result << call_input
383
+ end
384
+ result.concat(@mapping[conf.state])
385
+
386
+ result
387
+ end
388
+
389
+ # Returns a prefix string of `state`.
390
+ def state_prefix(state)
391
+ @mapping[state]
392
+ end
393
+ end
394
+
395
+ def initialize(initial_state, accept_states, transitions, returns)
396
+ super()
397
+
398
+ @initial_state = initial_state
399
+ @accept_states = accept_states
400
+ @transitions = transitions
401
+ @returns = returns
402
+ end
403
+
404
+ attr_reader :initial_state, :accept_states, :transitions, :returns
405
+
406
+ # Returns the type of this automaton.
407
+ def type
408
+ :vpa
409
+ end
410
+
411
+ # Returns the initial configuration.
412
+ def initial
413
+ Conf[initial_state, []]
414
+ end
415
+
416
+ # Computes a transition for the given `input` from the current `state`.
417
+ def step(conf, input)
418
+ next_conf = step_conf(conf, input)
419
+ output = !next_conf.nil? && accept_states.include?(next_conf.state) && next_conf.stack.empty?
420
+ [output, next_conf]
421
+ end
422
+
423
+ # Returns a mermaid diagram.
424
+ def to_mermaid(remove_error_state: true)
425
+ error_state = error_state() if remove_error_state
426
+ mmd = +""
427
+
428
+ mmd << "flowchart TD\n"
429
+
430
+ states =
431
+ [initial_state] + transitions.keys.map { |(q, _)| q } + transitions.values + returns.keys.map { |(q, _)| q } +
432
+ returns.values.flat_map { |rt| rt.flat_map { |(q1, _), q2| [q1, q2] } }
433
+ states.uniq!
434
+ states.delete(error_state)
435
+
436
+ states.sort.each { |q| mmd << (accept_states.include?(q) ? " #{q}(((#{q})))\n" : " #{q}((#{q}))\n") }
437
+ mmd << "\n"
438
+
439
+ transitions.each do |(q1, i), q2|
440
+ next if q1 == error_state || q2 == error_state
441
+ mmd << " #{q1} -- \"'#{i}'\" --> #{q2}\n"
442
+ end
443
+ mmd << "\n"
444
+
445
+ returns.each do |(q1, r), rt|
446
+ next if q1 == error_state
447
+ rt.each do |(q2, c), q3|
448
+ next if q2 == error_state || q3 == error_state
449
+ mmd << " #{q1} -- \"'#{r}'/(#{q2},'#{c}')\" --> #{q3}\n"
450
+ end
451
+ end
452
+
453
+ mmd.dup
454
+ end
455
+
456
+ # Returns an error state in this VPA.
457
+ def error_state
458
+ t =
459
+ transitions
460
+ .group_by { |(state, _), _| state }
461
+ .transform_values { _1.to_h { |(_, input), next_state| [input, next_state] } }
462
+
463
+ t.each do |state, d|
464
+ # The initial state and accepting states are not an error state.
465
+ next if state == initial_state || accept_states.include?(state)
466
+
467
+ # An error state should only have self-loops.
468
+ next unless d.all? { |_, next_state| state == next_state }
469
+ all_returns_are_self_loops =
470
+ returns.all? do |_, rt|
471
+ rt.filter { |(call_state, _), _| call_state == state }.all? { |_, next_state| state == next_state }
472
+ end
473
+ next unless all_returns_are_self_loops
474
+
475
+ return state
476
+ end
477
+
478
+ nil
479
+ end
480
+
481
+ private
482
+
483
+ def step_conf(conf, input)
484
+ # `nil` means the error state.
485
+ return nil unless conf
486
+
487
+ next_state = @transitions[[conf.state, input]]
488
+ return Conf[next_state, conf.stack] if next_state
489
+
490
+ return_transitions = @returns[[conf.state, input]]
491
+ if return_transitions
492
+ return nil if conf.stack.empty?
493
+ next_state = return_transitions[conf.stack.last]
494
+ return Conf[next_state, conf.stack[0...-1]]
495
+ end
496
+
497
+ # When there is no usual transition and no return tansition for `input`,
498
+ # then we assume that `input` is a call alphabet.
499
+ Conf[initial_state, conf.stack + [[conf.state, input]]]
500
+ end
100
501
  end
101
502
  end
@@ -10,6 +10,8 @@ module Lernen
10
10
  process_linear(sul, hypothesis, cex, state_to_prefix)
11
11
  in :binary
12
12
  process_binary(sul, hypothesis, cex, state_to_prefix)
13
+ in :exponential
14
+ process_exponential(sul, hypothesis, cex, state_to_prefix)
13
15
  end
14
16
  end
15
17
 
@@ -17,13 +19,13 @@ module Lernen
17
19
  def self.process_linear(sul, hypothesis, cex, state_to_prefix)
18
20
  expected_output = sul.query(cex).last
19
21
 
20
- current_state = hypothesis.initial_state
21
- cex.each_with_index do |a, i|
22
- _, next_state = hypothesis.step(current_state, a)
22
+ current_state = hypothesis.initial
23
+ cex.each_with_index do |input, i|
24
+ _, next_state = hypothesis.step(current_state, input)
23
25
 
24
26
  prefix = state_to_prefix[next_state]
25
27
  suffix = cex[i + 1...]
26
- return state_to_prefix[current_state], a, suffix if expected_output != sul.query(prefix + suffix).last
28
+ return cex[0...i], input, suffix if expected_output != sul.query(prefix + suffix).last
27
29
 
28
30
  current_state = next_state
29
31
  end
@@ -32,11 +34,10 @@ module Lernen
32
34
  # Processes a given `cex` by binary search.
33
35
  #
34
36
  # It is known as the Rivest-Schapire (RS) technique.
35
- def self.process_binary(sul, hypothesis, cex, state_to_prefix)
37
+ def self.process_binary(sul, hypothesis, cex, state_to_prefix, low: 0)
36
38
  expected_output = sul.query(cex).last
37
39
 
38
- low = 0
39
- high = cex.size
40
+ high = cex.size - 1
40
41
 
41
42
  while high - low > 1
42
43
  mid = (low + high) / 2
@@ -53,9 +54,39 @@ module Lernen
53
54
 
54
55
  prefix = cex[0...low]
55
56
  suffix = cex[high...]
57
+ [prefix, cex[low], suffix]
58
+ end
59
+
60
+ # Processes a given `cex` by exponential seatch.
61
+ #
62
+ # This idea is described in this paper.
63
+ #
64
+ # Isberner, Malte, and Bernhard Steffen. "An abstract framework for counterexample
65
+ # analysis in active automata learning." International Conference on Grammatical
66
+ # Inference. PMLR, 2014.
67
+ def self.process_exponential(sul, hypothesis, cex, state_to_prefix)
68
+ expected_output = sul.query(cex).last
69
+
70
+ prev_bp = 0
71
+ bp = 1
72
+
73
+ loop do
74
+ if bp >= cex.size
75
+ bp = cex.size
76
+ break
77
+ end
78
+
79
+ prefix = cex[0...bp]
80
+ suffix = cex[bp...]
81
+
82
+ _, prefix_state = hypothesis.run(prefix)
83
+ break if expected_output != sul.query(state_to_prefix[prefix_state] + suffix).last
84
+
85
+ prev_bp = bp
86
+ bp *= 2
87
+ end
56
88
 
57
- _, prefix_state = hypothesis.run(prefix)
58
- [state_to_prefix[prefix_state], cex[low], suffix]
89
+ process_binary(sul, hypothesis, cex, state_to_prefix, low: prev_bp)
59
90
  end
60
91
  end
61
92
  end
@@ -8,11 +8,13 @@ module Lernen
8
8
 
9
9
  private_constant :Node, :Leaf
10
10
 
11
- def initialize(alphabet, sul, cex:, automaton_type:, cex_processing:)
11
+ def initialize(alphabet, sul, cex:, automaton_type:, cex_processing:, call_alphabet:, return_alphabet:)
12
12
  @alphabet = alphabet
13
13
  @sul = sul
14
14
  @automaton_type = automaton_type
15
15
  @cex_processing = cex_processing
16
+ @call_alphabet = call_alphabet
17
+ @return_alphabet = return_alphabet
16
18
 
17
19
  @paths = {}
18
20
 
@@ -28,6 +30,7 @@ module Lernen
28
30
  @root.edges[cex_out] = Leaf[cex]
29
31
  @paths[cex] = [cex_out]
30
32
  in :mealy
33
+ prefix = cex[0...-1]
31
34
  suffix = [cex.last]
32
35
  @root = Node[suffix, {}]
33
36
 
@@ -35,6 +38,16 @@ module Lernen
35
38
  @root.edges[suffix_out] = Leaf[[]]
36
39
  @paths[[]] = [suffix_out]
37
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
+
38
51
  cex_out = sul.query(cex).last
39
52
  @root.edges[cex_out] = Leaf[cex]
40
53
  @paths[cex] = [cex_out]
@@ -47,7 +60,15 @@ module Lernen
47
60
  path = []
48
61
 
49
62
  until node.is_a?(Leaf)
50
- inputs = word + node.suffix
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
+
51
72
  out = @sul.query(inputs).last
52
73
  path << out
53
74
 
@@ -65,12 +86,15 @@ module Lernen
65
86
  # Constructs a hypothesis automaton from this classification tree.
66
87
  def to_hypothesis
67
88
  transitions = {}
89
+ returns = {}
68
90
 
69
91
  queue = []
70
92
  prefix_to_state = {}
93
+ state_to_prefix = {}
71
94
 
72
95
  queue << []
73
96
  prefix_to_state[[]] = prefix_to_state.size
97
+ state_to_prefix[state_to_prefix.size] = []
74
98
 
75
99
  until queue.empty?
76
100
  prefix = queue.shift
@@ -80,22 +104,63 @@ module Lernen
80
104
  next_prefix = sift(word)
81
105
 
82
106
  unless prefix_to_state.include?(next_prefix)
83
- prefix_to_state[next_prefix] = prefix_to_state.size
84
107
  queue << next_prefix
108
+ prefix_to_state[next_prefix] = prefix_to_state.size
109
+ state_to_prefix[state_to_prefix.size] = next_prefix
85
110
  end
86
111
 
87
112
  next_state = prefix_to_state[next_prefix]
88
113
  case @automaton_type
89
- in :dfa | :moore
114
+ in :dfa | :moore | :vpa
90
115
  transitions[[state, input]] = next_state
91
116
  in :mealy
92
117
  output = @sul.query(word).last
93
118
  transitions[[state, input]] = [output, next_state]
94
119
  end
95
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
96
162
  end
97
163
 
98
- state_to_prefix = prefix_to_state.to_h { |q, i| [i, q] }
99
164
  automaton =
100
165
  case @automaton_type
101
166
  in :dfa
@@ -106,6 +171,11 @@ module Lernen
106
171
  Moore.new(0, outputs, transitions)
107
172
  in :mealy
108
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)
109
179
  end
110
180
 
111
181
  [automaton, state_to_prefix]
@@ -116,42 +186,72 @@ module Lernen
116
186
  old_prefix, new_input, new_suffix =
117
187
  CexProcessor.process(@sul, hypothesis, cex, state_to_prefix, cex_processing: @cex_processing)
118
188
 
119
- _, old_prefix_state = hypothesis.run(old_prefix)
120
- new_prefix = state_to_prefix[old_prefix_state] + [new_input]
121
- new_prefix_out = @sul.query(new_prefix + new_suffix).last
189
+ _, old_state = hypothesis.run(old_prefix)
190
+ _, replace_state = hypothesis.step(old_state, new_input)
122
191
 
123
- _, old_node_state = hypothesis.run(old_prefix + [new_input])
124
- old_node_prefix = state_to_prefix[old_node_state]
125
- old_node_out = @sul.query(old_node_prefix + new_suffix).last
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
126
216
 
127
- old_node_path = @paths[old_node_prefix]
128
- parent = @root
129
- old_node = @root.edges[old_node_path.first]
130
- old_node_path[1..].each do |out|
131
- parent = old_node
132
- old_node = old_node.edges[out]
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]
133
223
  end
134
224
 
135
225
  new_node = Node[new_suffix, {}]
136
- parent.edges[old_node_path.last] = new_node
226
+ replace_node_parent.edges[replace_node_path.last] = new_node
137
227
 
138
- new_node.edges[new_prefix_out] = Leaf[new_prefix]
139
- @paths[new_prefix] = old_node_path + [new_prefix_out]
228
+ new_node.edges[new_out] = Leaf[new_prefix]
229
+ @paths[new_prefix] = replace_node_path + [new_out]
140
230
 
141
- new_node.edges[old_node_out] = Leaf[old_node_prefix]
142
- @paths[old_node_prefix] = old_node_path + [old_node_out]
231
+ new_node.edges[replace_out] = Leaf[replace_prefix]
232
+ @paths[replace_prefix] = replace_node_path + [replace_out]
143
233
  end
144
234
  end
145
235
 
146
236
  # KearnsVazirani is an implementation of the Kearns-Vazirani automata learning algorithm.
147
237
  module KearnsVazirani
148
238
  # Runs the Kearns-Vazirani algoritghm and returns an inferred automaton.
149
- def self.learn(alphabet, sul, oracle, automaton_type:, cex_processing: :binary, max_learning_rounds: nil)
150
- hypothesis = construct_first_hypothesis(alphabet, sul, automaton_type)
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:)
151
250
  cex = oracle.find_cex(hypothesis)
152
251
  return hypothesis if cex.nil?
153
252
 
154
- classification_tree = ClassificationTree.new(alphabet, sul, cex:, automaton_type:, cex_processing:)
253
+ classification_tree =
254
+ ClassificationTree.new(alphabet, sul, cex:, automaton_type:, cex_processing:, call_alphabet:, return_alphabet:)
155
255
  learning_rounds = 0
156
256
 
157
257
  loop do
@@ -170,15 +270,15 @@ module Lernen
170
270
  end
171
271
 
172
272
  # Constructs the first hypothesis automaton.
173
- def self.construct_first_hypothesis(alphabet, sul, automaton_type)
273
+ def self.construct_first_hypothesis(alphabet, sul, automaton_type, call_alphabet:, return_alphabet:)
174
274
  transitions = {}
175
- alphabet.each do |a|
275
+ alphabet.each do |input|
176
276
  case automaton_type
177
- in :dfa | :moore
178
- transitions[[0, a]] = 0
277
+ in :dfa | :moore | :vpa
278
+ transitions[[0, input]] = 0
179
279
  in :mealy
180
- out = sul.query([a]).last
181
- transitions[[0, a]] = [out, 0]
280
+ out = sul.query([input]).last
281
+ transitions[[0, input]] = [out, 0]
182
282
  end
183
283
  end
184
284
 
@@ -191,6 +291,17 @@ module Lernen
191
291
  Moore.new(0, outputs, transitions)
192
292
  in :mealy
193
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)
194
305
  end
195
306
  end
196
307
 
data/lib/lernen/lsharp.rb CHANGED
@@ -85,15 +85,15 @@ module Lernen
85
85
 
86
86
  @basis = []
87
87
  @frontier = {}
88
+
89
+ @incomplete_basis = []
88
90
  end
89
91
 
90
92
  # Runs the L# algoritghm and returns an inferred automaton.
91
93
  def learn
92
- @basis << []
94
+ add_basis([])
93
95
 
94
96
  loop do
95
- update_frontier
96
-
97
97
  next if promotion || completion || identification
98
98
 
99
99
  hypothesis = check_hypothesis
@@ -140,6 +140,7 @@ module Lernen
140
140
 
141
141
  def add_basis(prefix)
142
142
  @basis << prefix
143
+ @incomplete_basis << prefix
143
144
  prefix_node = @observation_tree[prefix]
144
145
  @frontier.each do |border, eq_prefixes|
145
146
  border_node = @observation_tree[border]
@@ -158,7 +159,7 @@ module Lernen
158
159
  def update_frontier
159
160
  @frontier.each do |border, eq_prefixes|
160
161
  border_node = @observation_tree[border]
161
- @frontier[border] = eq_prefixes.filter do |prefix|
162
+ eq_prefixes.filter! do |prefix|
162
163
  prefix_node = @observation_tree[prefix]
163
164
  check_apartness(prefix_node, border_node).nil?
164
165
  end
@@ -213,7 +214,7 @@ module Lernen
213
214
 
214
215
  def check_consistency(hypothesis, state_to_prefix)
215
216
  queue = []
216
- queue << [[], hypothesis.initial_state, @observation_tree.root]
217
+ queue << [[], hypothesis.initial, @observation_tree.root]
217
218
 
218
219
  until queue.empty?
219
220
  prefix, state, node = queue.shift
@@ -232,46 +233,53 @@ module Lernen
232
233
  end
233
234
 
234
235
  def promotion
235
- isolated_borders = @frontier.to_a.filter { |(_, eq_prefixes)| eq_prefixes.empty? }.map { |(border, _)| border }
236
+ @frontier.each do |new_prefix, eq_prefixes|
237
+ next unless eq_prefixes.empty?
236
238
 
237
- return false if isolated_borders.empty?
239
+ @frontier.delete(new_prefix)
240
+ add_basis(new_prefix)
238
241
 
239
- new_prefix = isolated_borders.first
240
- @frontier.delete(new_prefix)
241
- add_basis(new_prefix)
242
+ return true
243
+ end
242
244
 
243
- true
245
+ false
244
246
  end
245
247
 
246
248
  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
249
+ updated = false
250
+
251
+ until @incomplete_basis.empty?
252
+ prefix = @incomplete_basis.pop
253
+ prefix_tree = @observation_tree[prefix]
253
254
 
254
- return false if incomplete_borders.empty?
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)
255
261
 
256
- incomplete_borders.each do |border|
257
- @observation_tree.query(border)
258
- add_frontier(border)
262
+ updated = true
263
+ end
259
264
  end
260
265
 
261
- true
266
+ updated
262
267
  end
263
268
 
264
269
  def identification
265
- unidentified_borders = @frontier.keys.filter { @frontier[_1].size >= 2 }
270
+ @frontier.each do |border, eq_prefixes|
271
+ next unless eq_prefixes.size >= 2
266
272
 
267
- return false if unidentified_borders.empty?
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
268
278
 
269
- border = unidentified_borders.first
270
- prefix1, prefix2 = @frontier[border][0...2]
271
- witness = compute_witness(prefix1, prefix2)
272
- @observation_tree.query(border + witness)
279
+ return true
280
+ end
273
281
 
274
- true
282
+ false
275
283
  end
276
284
 
277
285
  def check_hypothesis
@@ -300,6 +308,7 @@ module Lernen
300
308
  return hypothesis if cex.nil?
301
309
 
302
310
  process_cex(hypothesis, state_to_prefix, cex)
311
+ update_frontier
303
312
 
304
313
  nil
305
314
  end
data/lib/lernen/lstar.rb CHANGED
@@ -155,7 +155,8 @@ module Lernen
155
155
  else
156
156
  old_prefix, new_input, new_suffix =
157
157
  CexProcessor.process(sul, hypothesis, cex, state_to_prefix, cex_processing:)
158
- new_prefix = old_prefix + [new_input]
158
+ _, old_state = hypothesis.run(old_prefix)
159
+ new_prefix = state_to_prefix[old_state] + [new_input]
159
160
  observation_table.prefixes << new_prefix unless observation_table.prefixes.include?(new_prefix)
160
161
  observation_table.suffixes << new_suffix unless observation_table.suffixes.include?(new_suffix)
161
162
  end
data/lib/lernen/oracle.rb CHANGED
@@ -32,7 +32,7 @@ module Lernen
32
32
 
33
33
  # Resets the internal states of this oracle.
34
34
  def reset_internal(hypothesis)
35
- @current_state = hypothesis.initial_state
35
+ @current_state = hypothesis.initial
36
36
 
37
37
  @sul.shutdown
38
38
  @sul.setup
@@ -69,17 +69,19 @@ module Lernen
69
69
  end
70
70
  end
71
71
 
72
+ @sul.shutdown
72
73
  nil
73
74
  end
74
75
  end
75
76
 
76
77
  # This equivalence oracles uses random-walk exploration for equivalence checking.
77
78
  class RandomWalkOracle < Oracle
78
- def initialize(alphabet, sul, step_limit: 500, reset_prob: 0.09)
79
+ def initialize(alphabet, sul, step_limit: 3000, reset_prob: 0.09, random: Random)
79
80
  super(alphabet, sul)
80
81
 
81
82
  @step_limit = step_limit
82
83
  @reset_prob = reset_prob
84
+ @random = random
83
85
  end
84
86
 
85
87
  # Finds a conterexample against the given `hypothesis` automaton.
@@ -93,12 +95,12 @@ module Lernen
93
95
  while random_steps_done < @step_limit
94
96
  random_steps_done += 1
95
97
 
96
- if rand < @reset_prob
98
+ if @random.rand < @reset_prob
97
99
  inputs = []
98
100
  reset_internal(hypothesis)
99
101
  end
100
102
 
101
- inputs << @alphabet.sample
103
+ inputs << @alphabet.sample(random: @random)
102
104
 
103
105
  @num_steps += 1
104
106
  h_out, @current_state = hypothesis.step(@current_state, inputs.last)
@@ -110,6 +112,7 @@ module Lernen
110
112
  end
111
113
  end
112
114
 
115
+ @sul.shutdown
113
116
  nil
114
117
  end
115
118
  end
data/lib/lernen/sul.rb CHANGED
@@ -17,6 +17,17 @@ module Lernen
17
17
  # Creates a SUL from the given block as an implementation of a membership query.
18
18
  def self.from_block(cache: true, &) = BlockSUL.new(cache:, &)
19
19
 
20
+ # Creates a SUL from the given automaton as an implementation.
21
+ def self.from_automaton(automaton, cache: true) =
22
+ case automaton
23
+ when DFA
24
+ DFASimulatorSUL.new(automaton, cache:)
25
+ when Mealy
26
+ MealySimulatorSUL.new(automaton, cache:)
27
+ when Moore
28
+ MooreSimulatorSUL.new(automaton, cache:)
29
+ end
30
+
20
31
  def initialize(cache: true)
21
32
  @cache = cache ? {} : nil
22
33
  @num_cached_queries = 0
@@ -58,7 +69,7 @@ module Lernen
58
69
  @num_queries += 1
59
70
  @num_steps += inputs.size
60
71
 
61
- @cache[inputs] = outputs
72
+ @cache[inputs] = outputs if @cache
62
73
 
63
74
  outputs
64
75
  end
@@ -96,7 +107,7 @@ module Lernen
96
107
  super
97
108
  end
98
109
 
99
- # Runs a membership query with the given inputs.
110
+ # Runs a membership query with the empty input.
100
111
  #
101
112
  # This is *abstract*.
102
113
  def query_empty
@@ -104,6 +115,71 @@ module Lernen
104
115
  end
105
116
  end
106
117
 
118
+ # BaseSimulatorSUL is a base implementation of SUL on automaton simulators.
119
+ module BaseSimulatorSUL
120
+ # It is a setup procedure of this SUL.
121
+ def setup
122
+ @state = @automaton.initial
123
+ end
124
+
125
+ # It is a shutdown procedure of this SUL.
126
+ def shutdown
127
+ @state = nil
128
+ end
129
+
130
+ # Runs a membership query with the given inputs.
131
+ def step(input)
132
+ output, @state = @automaton.step(@state, input)
133
+ output
134
+ end
135
+ end
136
+
137
+ # DFASimulatorSUL is a SUL on a DFA simuator.
138
+ class DFASimulatorSUL < MooreSUL
139
+ include BaseSimulatorSUL
140
+
141
+ def initialize(automaton, cache: true)
142
+ super(cache:)
143
+
144
+ @automaton = automaton
145
+ @state = nil
146
+ end
147
+
148
+ # Runs a membership query with the empty input.
149
+ def query_empty
150
+ @automaton.accept_states.include?(@automaton.initial_state)
151
+ end
152
+ end
153
+
154
+ # MealySimulatorSUL is a SUL on a Mealy simuator.
155
+ class MealySimulatorSUL < SUL
156
+ include BaseSimulatorSUL
157
+
158
+ def initialize(automaton, cache: true)
159
+ super(cache:)
160
+
161
+ @automaton = automaton
162
+ @state = nil
163
+ end
164
+ end
165
+
166
+ # MooreSimulatorSUL is a SUL on a Moore simuator.
167
+ class MooreSimulatorSUL < MooreSUL
168
+ include BaseSimulatorSUL
169
+
170
+ def initialize(automaton, cache: true)
171
+ super(cache:)
172
+
173
+ @automaton = automaton
174
+ @state = nil
175
+ end
176
+
177
+ # Runs a membership query with the empty input.
178
+ def query_empty
179
+ @automaton.outputs[@automaton.initial_state]
180
+ end
181
+ end
182
+
107
183
  # BlockSUL is a System Under Learning (SUL) constructed from a block.
108
184
  #
109
185
  # A block is expected to behave like a membership query.
@@ -126,7 +202,7 @@ module Lernen
126
202
  @block.call(@inputs)
127
203
  end
128
204
 
129
- # Runs a membership query with the given inputs.
205
+ # Runs a membership query with the empty input.
130
206
  def query_empty
131
207
  @block.call([])
132
208
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Lernen
4
4
  # The version string.
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
data/lib/lernen.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "set"
4
4
 
5
- # Lernen is a simple automata learning library.
6
5
  module Lernen
7
6
  # Error is an error class for this library.
8
7
  class Error < StandardError
@@ -18,3 +17,63 @@ require_relative "lernen/version"
18
17
  require_relative "lernen/lstar"
19
18
  require_relative "lernen/kearns_vazirani"
20
19
  require_relative "lernen/lsharp"
20
+
21
+ # Lernen is a simple automata learning library.
22
+ module Lernen
23
+ # Learn an automaton.
24
+ def self.learn(
25
+ alphabet:,
26
+ call_alphabet: nil,
27
+ return_alphabet: nil,
28
+ sul: nil,
29
+ oracle: :random_walk,
30
+ oracle_params: {},
31
+ algorithm: :kearns_vazirani,
32
+ automaton_type: nil,
33
+ params: {},
34
+ random: Random,
35
+ &sul_block
36
+ )
37
+ automaton_type ||= call_alphabet ? :vpa : :dfa
38
+
39
+ case sul
40
+ when SUL
41
+ # Do nothing
42
+ when Automaton
43
+ automaton_type = sul.type
44
+ sul = SUL.from_automaton(sul)
45
+ when nil
46
+ sul = SUL.from_block(&sul_block)
47
+ else
48
+ raise ArgumentError, "Unsupported SUL: #{sul}"
49
+ end
50
+
51
+ full_alphabet =
52
+ case automaton_type
53
+ in :dfa | :moore | :mealy
54
+ alphabet
55
+ in :vpa
56
+ alphabet + call_alphabet + return_alphabet
57
+ end
58
+
59
+ case oracle
60
+ when Oracle
61
+ # Do nothing
62
+ when :breadth_first_exploration
63
+ oracle = BreadthFirstExplorationOracle.new(full_alphabet, sul, **oracle_params)
64
+ when :random_walk
65
+ oracle = RandomWalkOracle.new(full_alphabet, sul, random:, **oracle_params)
66
+ else
67
+ raise ArgumentError, "Unsupported oracle: #{oracle}"
68
+ end
69
+
70
+ case algorithm
71
+ in :lstar
72
+ LStar.learn(alphabet, sul, oracle, automaton_type:, **params)
73
+ in :kearns_vazirani
74
+ KearnsVazirani.learn(alphabet, sul, oracle, automaton_type:, call_alphabet:, return_alphabet:, **params)
75
+ in :lsharp
76
+ LSharp.learn(alphabet, sul, oracle, automaton_type:, **params)
77
+ end
78
+ end
79
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lernen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TSUYUSATO Kitsune
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-22 00:00:00.000000000 Z
11
+ date: 2024-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: set