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 +4 -4
- data/.rubocop.yml +3 -0
- data/README.md +42 -25
- data/Rakefile +5 -2
- data/lib/lernen/automaton.rb +402 -1
- data/lib/lernen/cex_processor.rb +40 -9
- data/lib/lernen/kearns_vazirani.rb +142 -31
- data/lib/lernen/lsharp.rb +38 -29
- data/lib/lernen/lstar.rb +2 -1
- data/lib/lernen/oracle.rb +7 -4
- data/lib/lernen/sul.rb +79 -3
- data/lib/lernen/version.rb +1 -1
- data/lib/lernen.rb +60 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f98907d83d61890848e0425254b717d3c080ea4a9b596fc6d015bc03b6de71fa
|
4
|
+
data.tar.gz: a6231a390522b67f4da4290ce385b043043e172d7045f13ede5398f8dff6131c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d6fbeb6eb3f8e05d34d229201e02304418d182e3804ac33269d00d8c515497906ca7f815711a0efce29a1aaae673b3d80ef4d018e9b7abcb60e44c25d66f366
|
7
|
+
data.tar.gz: 8f92777c0fce29e40b1cca5158c7b7bf911eca481ad00084fe1e22b6615c51492f5edd66dfdfb1a823d5ff8a79898d50246ced7dacfb177b688ad2ca77aed56c
|
data/.rubocop.yml
CHANGED
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
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
# =>
|
16
|
-
# 0
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
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
|
-
|
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"]
|
data/lib/lernen/automaton.rb
CHANGED
@@ -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 =
|
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
|
data/lib/lernen/cex_processor.rb
CHANGED
@@ -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.
|
21
|
-
cex.each_with_index do |
|
22
|
-
_, next_state = hypothesis.step(current_state,
|
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
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
_,
|
120
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
226
|
+
replace_node_parent.edges[replace_node_path.last] = new_node
|
137
227
|
|
138
|
-
new_node.edges[
|
139
|
-
@paths[new_prefix] =
|
228
|
+
new_node.edges[new_out] = Leaf[new_prefix]
|
229
|
+
@paths[new_prefix] = replace_node_path + [new_out]
|
140
230
|
|
141
|
-
new_node.edges[
|
142
|
-
@paths[
|
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(
|
150
|
-
|
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 =
|
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 |
|
275
|
+
alphabet.each do |input|
|
176
276
|
case automaton_type
|
177
|
-
in :dfa | :moore
|
178
|
-
transitions[[0,
|
277
|
+
in :dfa | :moore | :vpa
|
278
|
+
transitions[[0, input]] = 0
|
179
279
|
in :mealy
|
180
|
-
out = sul.query([
|
181
|
-
transitions[[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
|
-
|
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
|
-
|
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.
|
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
|
-
|
236
|
+
@frontier.each do |new_prefix, eq_prefixes|
|
237
|
+
next unless eq_prefixes.empty?
|
236
238
|
|
237
|
-
|
239
|
+
@frontier.delete(new_prefix)
|
240
|
+
add_basis(new_prefix)
|
238
241
|
|
239
|
-
|
240
|
-
|
241
|
-
add_basis(new_prefix)
|
242
|
+
return true
|
243
|
+
end
|
242
244
|
|
243
|
-
|
245
|
+
false
|
244
246
|
end
|
245
247
|
|
246
248
|
def completion
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
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
|
-
|
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
|
-
|
257
|
-
|
258
|
-
add_frontier(border)
|
262
|
+
updated = true
|
263
|
+
end
|
259
264
|
end
|
260
265
|
|
261
|
-
|
266
|
+
updated
|
262
267
|
end
|
263
268
|
|
264
269
|
def identification
|
265
|
-
|
270
|
+
@frontier.each do |border, eq_prefixes|
|
271
|
+
next unless eq_prefixes.size >= 2
|
266
272
|
|
267
|
-
|
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
|
-
|
270
|
-
|
271
|
-
witness = compute_witness(prefix1, prefix2)
|
272
|
-
@observation_tree.query(border + witness)
|
279
|
+
return true
|
280
|
+
end
|
273
281
|
|
274
|
-
|
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
|
-
|
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.
|
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:
|
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
|
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
|
205
|
+
# Runs a membership query with the empty input.
|
130
206
|
def query_empty
|
131
207
|
@block.call([])
|
132
208
|
end
|
data/lib/lernen/version.rb
CHANGED
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.
|
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-
|
11
|
+
date: 2024-08-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: set
|