stamina-core 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 0.6.0 / 2012-09-12
2
+
3
+ * Added Markable#marks and Markable#raw_data
4
+ * State#initial!, #accepting! and error! now accept an optional truth value parameter.
5
+ * Generalized the decoration algorithm to work backwards as well as forwards as well as
6
+ accepting an output Hash-like object, defaulting to true state decorations.
7
+ * Generalized the DFA equivalence algorithm under TransitionSystem::Equivalence.
8
+
1
9
  # O.5.4 / 2012-03-06
2
10
 
3
11
  * InputString and Sample have been moved from stamina-induction to stamina-core as ADL
@@ -1,5 +1,6 @@
1
+ require_relative 'automaton/state'
2
+ require_relative 'automaton/edge'
1
3
  module Stamina
2
-
3
4
  #
4
5
  # Automaton data-structure.
5
6
  #
@@ -133,358 +134,6 @@ module Stamina
133
134
  class Automaton
134
135
  include Stamina::Markable
135
136
 
136
- #
137
- # Automaton state.
138
- #
139
- class State
140
- include Stamina::Markable
141
- attr_reader :automaton, :index
142
-
143
- #
144
- # Creates a state.
145
- #
146
- # Arguments:
147
- # - automaton: parent automaton of the state.
148
- # - index: index of the state in the state list.
149
- # - data: user data attached to this state.
150
- #
151
- def initialize(automaton, index, data)
152
- @automaton = automaton
153
- @index = index
154
- @data = data.dup
155
- @out_edges = []
156
- @in_edges = []
157
- @epsilon_closure = nil
158
- end
159
-
160
- ### public read-only section ###############################################
161
- public
162
-
163
- # Returns true if this state is an initial state, false otherwise.
164
- def initial?
165
- !!@data[:initial]
166
- end
167
-
168
- # Sets this state as an initial state.
169
- def initial!
170
- @data[:initial] = true
171
- end
172
-
173
- # Returns true if this state is an accepting state, false otherwise.
174
- def accepting?
175
- !!@data[:accepting]
176
- end
177
-
178
- # Sets this state as an accepting state.
179
- def accepting!
180
- @data[:accepting] = true
181
- end
182
-
183
- # Returns true if this state is an error state, false otherwise.
184
- def error?
185
- !!@data[:error]
186
- end
187
-
188
- # Sets this state as an error state.
189
- def error!
190
- @data[:error] = true
191
- end
192
-
193
- # Returns true if this state is deterministic, false otherwise.
194
- def deterministic?
195
- outs = out_symbols
196
- (outs.size==@out_edges.size) and not(outs.include?(nil))
197
- end
198
-
199
- # Checks if this state is a sink state or not. Sink states are defined as
200
- # non accepting states having no outgoing transition or only loop
201
- # transitions.
202
- def sink?
203
- !accepting? && out_edges.all?{|e| e.target==self}
204
- end
205
-
206
- # Returns an array containing all incoming edges of the state. Edges are
207
- # sorted if _sorted_ is set to true. If two incoming edges have same symbol
208
- # no order is guaranteed between them.
209
- #
210
- # Returned array may be modified.
211
- def in_edges(sorted=false)
212
- sorted ? @in_edges.sort : @in_edges.dup
213
- end
214
-
215
- # Returns an array containing all outgoing edges of the state. Edges are
216
- # sorted if _sorted_ is set to true. If two outgoing edges have same symbol
217
- # no order is guaranteed between them.
218
- #
219
- # Returned array may be modified.
220
- def out_edges(sorted=false)
221
- sorted ? @out_edges.sort : @out_edges.dup
222
- end
223
-
224
- # Returns an array with the different symbols appearing on incoming edges.
225
- # Returned array does not contain duplicates. Symbols are sorted in the
226
- # array if _sorted_ is set to true.
227
- #
228
- # Returned array may be modified.
229
- def in_symbols(sorted=false)
230
- symbols = @in_edges.collect{|e| e.symbol}.uniq
231
- return sorted ? (symbols.sort &automaton.symbols_comparator) : symbols
232
- end
233
-
234
- # Returns an array with the different symbols appearing on outgoing edges.
235
- # Returned array does not contain duplicates. Symbols are sorted in the
236
- # array if _sorted_ is set to true.
237
- #
238
- # Returned array may be modified.
239
- def out_symbols(sorted=false)
240
- symbols = @out_edges.collect{|e| e.symbol}.uniq
241
- return sorted ? (symbols.sort &automaton.symbols_comparator) : symbols
242
- end
243
-
244
- # Returns an array with adjacent states (in or out edge).
245
- #
246
- # Returned array may be modified.
247
- def adjacent_states()
248
- (in_adjacent_states+out_adjacent_states).uniq
249
- end
250
-
251
- # Returns an array with adjacent states along an incoming edge (without
252
- # duplicates).
253
- #
254
- # Returned array may be modified.
255
- def in_adjacent_states()
256
- (@in_edges.collect {|e| e.source}).uniq
257
- end
258
-
259
- # Returns an array with adjacent states along an outgoing edge (whithout
260
- # duplicates).
261
- #
262
- # Returned array may be modified.
263
- def out_adjacent_states()
264
- (@out_edges.collect {|e| e.target}).uniq
265
- end
266
-
267
- # Returns reachable states from this one with an input _symbol_. Returned
268
- # array does not contain duplicates and may be modified. This method if not
269
- # epsilon symbol aware.
270
- def step(symbol)
271
- @out_edges.select{|e| e.symbol==symbol}.collect{|e| e.target}
272
- end
273
-
274
- # Returns the state reached from this one with an input _symbol_, or nil if
275
- # no such state. This method is not epsilon symbol aware. Moreover it is
276
- # expected to be used on deterministic states only. If the state is not
277
- # deterministic, the method returns one reachable state if such a state
278
- # exists; which one is returned must be considered non deterministic.
279
- def dfa_step(symbol)
280
- edge = @out_edges.find{|e| e.symbol==symbol}
281
- edge ? edge.target : nil
282
- end
283
-
284
- # Computes the epsilon closure of this state. Epsilon closure is the set of
285
- # all states reached from this one with a <tt>eps*</tt> input (sequence of
286
- # zero or more epsilon symbols). The current state is always contained in
287
- # the epsilon closure. Returns an unsorted array without duplicates; this
288
- # array may not be modified.
289
- def epsilon_closure()
290
- @epsilon_closure ||= compute_epsilon_closure(Set.new).to_a.freeze
291
- end
292
-
293
- # Internal implementation of epsilon_closure. _result_ is expected to be
294
- # a Set instance, is modified and is the returned value.
295
- def compute_epsilon_closure(result)
296
- result << self
297
- step(nil).each do |t|
298
- t.compute_epsilon_closure(result) unless result.include?(t)
299
- end
300
- raise if result.nil?
301
- return result
302
- end
303
-
304
- # Computes an array representing the set of states that can be reached from
305
- # this state with a given input _symbol_. Returned array does not contain
306
- # duplicates and may be modified. No particular ordering of states in the
307
- # array is guaranteed.
308
- #
309
- # This method is epsilon symbol aware (represented with nil) on non
310
- # deterministic automata, meaning that it actually computes the set of
311
- # reachable states through strings respecting the <tt>eps* symbol eps*</tt>
312
- # regular expression, where eps is the epsilon symbol.
313
- def delta(symbol)
314
- if automaton.deterministic?
315
- target = dfa_delta(symbol)
316
- target.nil? ? [] : [target]
317
- else
318
- # 1) first compute epsilon closure of self
319
- at_epsilon = epsilon_closure
320
-
321
- # 2) now, look where we can go from there
322
- at_espilon_then_symbol = at_epsilon.collect do |s|
323
- s.step(symbol)
324
- end.flatten.uniq
325
-
326
- # 3) look where we can go from there using epsilon
327
- result = at_espilon_then_symbol.collect do |s|
328
- s.epsilon_closure
329
- end.flatten.uniq
330
-
331
- # return result as an array
332
- result
333
- end
334
- end
335
-
336
- # Returns the target state that can be reached from this state with _symbol_
337
- # input. Returns nil if no such state exists.
338
- #
339
- # This method is expected to be used on deterministic automata. Unlike delta,
340
- # it returns a State instance (or nil), not an array of states. When used on
341
- # non deterministic automata, it returns a state immediately reachable from
342
- # this state with _symbol_ input, or nil if no such state exists. This
343
- # method is not epsilon aware.
344
- def dfa_delta(symbol)
345
- return nil if symbol.nil?
346
- edge = @out_edges.find{|e| e.symbol==symbol}
347
- edge.nil? ? nil : edge.target
348
- end
349
-
350
- # Provides comparator of states, based on the index in the automaton state
351
- # list. This method returns nil unless _o_ is a State from the same
352
- # automaton than self.
353
- def <=>(o)
354
- return nil unless State===o
355
- return nil unless automaton===o.automaton
356
- return index <=> o.index
357
- end
358
-
359
- # Returns a string representation
360
- def inspect
361
- 's' << @index.to_s
362
- end
363
-
364
- # Returns a string representation
365
- def to_s
366
- 's' << @index.to_s
367
- end
368
-
369
- ### protected write section ################################################
370
- protected
371
-
372
- # Changes the index of this state in the state list. This method is only
373
- # expected to be used by the automaton itself.
374
- def index=(i) @index=i end
375
-
376
- #
377
- # Fired by Loaded when a user data is changed. The message is forwarded to
378
- # the automaton.
379
- #
380
- def state_changed(what, description)
381
- @epsilon_closure = nil
382
- @automaton.send(:state_changed, what, description)
383
- end
384
-
385
- # Adds an incoming edge to the state.
386
- def add_incoming_edge(edge)
387
- @epsilon_closure = nil
388
- @in_edges << edge
389
- end
390
-
391
- # Adds an outgoing edge to the state.
392
- def add_outgoing_edge(edge)
393
- @epsilon_closure = nil
394
- @out_edges << edge
395
- end
396
-
397
- # Adds an incoming edge to the state.
398
- def drop_incoming_edge(edge)
399
- @epsilon_closure = nil
400
- @in_edges.delete(edge)
401
- end
402
-
403
- # Adds an outgoing edge to the state.
404
- def drop_outgoing_edge(edge)
405
- @epsilon_closure = nil
406
- @out_edges.delete(edge)
407
- end
408
-
409
- protected :compute_epsilon_closure
410
- end
411
-
412
- #
413
- # Automaton edge.
414
- #
415
- class Edge
416
- include Stamina::Markable
417
- attr_reader :automaton, :index, :from, :to
418
-
419
- #
420
- # Creates an edge.
421
- #
422
- # Arguments:
423
- # - automaton: parent automaton of the edge.
424
- # - index: index of the edge in the edge list.
425
- # - data: user data attached to this edge.
426
- # - from: source state of the edge.
427
- # - to: target state of the edge.
428
- #
429
- def initialize(automaton, index, data, from, to)
430
- @automaton, @index = automaton, index
431
- @data = data
432
- @from, @to = from, to
433
- end
434
-
435
- # Returns edge symbol.
436
- def symbol()
437
- @data[:symbol]
438
- end
439
-
440
- # Sets edge symbol.
441
- def symbol=(symbol)
442
- @data[:symbol] = symbol
443
- end
444
-
445
- alias :source :from
446
- alias :target :to
447
-
448
- #
449
- # Provides comparator of edges, based on the index in the automaton edge
450
- # list. This method returns nil unless _o_ is an Edge from the same
451
- # automaton than self.
452
- # Once again, this method has nothing to do with equality, it looks at an
453
- # index and ID only.
454
- #
455
- def <=>(o)
456
- return nil unless Edge===o
457
- return nil unless automaton===o.automaton
458
- return index <=> o.index
459
- end
460
-
461
- # Returns a string representation
462
- def inspect
463
- 'e' << @index.to_s
464
- end
465
-
466
- # Returns a string representation
467
- def to_s
468
- 'e' << @index.to_s
469
- end
470
-
471
- ### protected write section ################################################
472
- protected
473
-
474
- # Changes the index of this edge in the edge list. This method is only
475
- # expected to be used by the automaton itself.
476
- def index=(i) @index=i end
477
-
478
- #
479
- # Fired by Loaded when a user data is changed. The message if forwarded to
480
- # the automaton.
481
- #
482
- def state_changed(what, infos)
483
- @automaton.send(:state_changed, what, infos)
484
- end
485
-
486
- end
487
-
488
137
  ### Automaton class ##########################################################
489
138
  public
490
139
 
@@ -1114,9 +763,7 @@ module Stamina
1114
763
 
1115
764
  # Returns true if the automaton is deterministic, false otherwise
1116
765
  def deterministic?
1117
- if @deterministic.nil?
1118
- @deterministic = @states.all?{|s| s.deterministic?}
1119
- end
766
+ @deterministic = @states.all?{|s| s.deterministic?} if @deterministic.nil?
1120
767
  @deterministic
1121
768
  end
1122
769
 
@@ -0,0 +1,79 @@
1
+ module Stamina
2
+ class Automaton
3
+ #
4
+ # Automaton edge.
5
+ #
6
+ class Edge
7
+ include Stamina::Markable
8
+ attr_reader :automaton, :index, :from, :to
9
+
10
+ #
11
+ # Creates an edge.
12
+ #
13
+ # Arguments:
14
+ # - automaton: parent automaton of the edge.
15
+ # - index: index of the edge in the edge list.
16
+ # - data: user data attached to this edge.
17
+ # - from: source state of the edge.
18
+ # - to: target state of the edge.
19
+ #
20
+ def initialize(automaton, index, data, from, to)
21
+ @automaton, @index = automaton, index
22
+ @data = data
23
+ @from, @to = from, to
24
+ end
25
+
26
+ # Returns edge symbol.
27
+ def symbol()
28
+ @data[:symbol]
29
+ end
30
+
31
+ # Sets edge symbol.
32
+ def symbol=(symbol)
33
+ @data[:symbol] = symbol
34
+ end
35
+
36
+ alias :source :from
37
+ alias :target :to
38
+
39
+ #
40
+ # Provides comparator of edges, based on the index in the automaton edge
41
+ # list. This method returns nil unless _o_ is an Edge from the same
42
+ # automaton than self.
43
+ # Once again, this method has nothing to do with equality, it looks at an
44
+ # index and ID only.
45
+ #
46
+ def <=>(o)
47
+ return nil unless Edge===o
48
+ return nil unless automaton===o.automaton
49
+ return index <=> o.index
50
+ end
51
+
52
+ # Returns a string representation
53
+ def inspect
54
+ 'e' << @index.to_s
55
+ end
56
+
57
+ # Returns a string representation
58
+ def to_s
59
+ 'e' << @index.to_s
60
+ end
61
+
62
+ ### protected write section ################################################
63
+ protected
64
+
65
+ # Changes the index of this edge in the edge list. This method is only
66
+ # expected to be used by the automaton itself.
67
+ def index=(i) @index=i end
68
+
69
+ #
70
+ # Fired by Loaded when a user data is changed. The message if forwarded to
71
+ # the automaton.
72
+ #
73
+ def state_changed(what, infos)
74
+ @automaton.send(:state_changed, what, infos)
75
+ end
76
+
77
+ end # class Edge
78
+ end # class Automaton
79
+ end # module Stamina
@@ -1,55 +1,36 @@
1
1
  module Stamina
2
2
  class Automaton
3
3
 
4
+ # Implements the equivalence relation commonly used on canonical DFAs
5
+ class Equivalence < TransitionSystem::Equivalence
6
+
7
+ def equivalent_systems?(s, t)
8
+ (s.state_count == t.state_count) &&
9
+ (s.edge_count == t.edge_count) &&
10
+ (s.alphabet == t.alphabet) &&
11
+ (s.data == t.data)
12
+ end
13
+
14
+ def equivalent_states?(s, t)
15
+ (s.initial? == t.initial?) &&
16
+ (s.accepting? == t.accepting?) &&
17
+ (s.error? == t.error?)
18
+ end
19
+
20
+ def equivalent_edges?(e, f)
21
+ e.symbol == f.symbol
22
+ end
23
+
24
+ end # class Equivalence
25
+
4
26
  #
5
27
  # Checks if this automaton is equivalent to another one.
6
28
  #
7
29
  # Automata must be both minimal and complete to guarantee that this method
8
30
  # works.
9
31
  #
10
- def equivalent?(other, equiv = nil, key = :equiv_state)
11
- equiv ||= Proc.new{|s1,s2| (s1 && s2) &&
12
- (s1.accepting? == s2.accepting?) &&
13
- (s1.error? == s2.error?) &&
14
- (s1.initial? == s2.initial?) }
15
-
16
- # Both must already have basic attributes in common
17
- return false unless state_count==other.state_count
18
- return false unless edge_count==other.edge_count
19
- return false unless alphabet==other.alphabet
20
- return false unless equiv[initial_state, other.initial_state]
21
-
22
- # We instantiate the decoration algorithm for checking equivalence on this
23
- # automaton:
24
- # * decoration is the index of the equivalent state in other automaton
25
- # * d0 is thus 'other.initial_state.index'
26
- # * suppremum is identity and fails when the equivalent state is not unique
27
- # * propagation checks transition function delta
28
- #
29
- algo = Stamina::Utils::Decorate.new(key)
30
- algo.set_suppremum do |d0, d1|
31
- if (d0.nil? or d1.nil?)
32
- (d0 || d1)
33
- elsif d0==d1
34
- d0
35
- else
36
- raise Stamina::Abord
37
- end
38
- end
39
- algo.set_propagate do |d,e|
40
- reached = other.ith_state(d).dfa_step(e.symbol)
41
- raise Stamina::Abord if reached.nil?
42
- raise Stamina::Abord unless equiv[e.target, reached]
43
- reached.index
44
- end
45
-
46
- # Run the algorithm now
47
- begin
48
- algo.execute(self, nil, other.initial_state.index)
49
- return true
50
- rescue Stamina::Abord
51
- return false
52
- end
32
+ def equivalent?(other)
33
+ Equivalence.new.call(self, other)
53
34
  end
54
35
  alias :<=> :equivalent?
55
36
 
@@ -49,23 +49,17 @@ module Stamina
49
49
  # each state as a mark under _key_, which defaults to :depth.
50
50
  #
51
51
  def depth(key = :depth)
52
- algo = Stamina::Utils::Decorate.new(key)
52
+ algo = Stamina::Utils::Decorate.new
53
53
  algo.set_suppremum do |d0,d1|
54
- # Here, unreached state has the max value (i.e. nil is +INF)
55
- # and we look at the minimum depth for each state
56
- if d0.nil?
57
- d1
58
- elsif d1.nil?
59
- d0
60
- else
61
- (d0 <= d1 ? d0 : d1)
62
- end
54
+ # Unreached state is MAX (i.e. nil is +INF); we look at the min depth for each state
55
+ (d0.nil? or d1.nil?) ? (d0 || d1) : (d0 <= d1 ? d0 : d1)
63
56
  end
64
57
  algo.set_propagate{|d,e| d+1 }
65
- algo.execute(self, nil, 0)
58
+ algo.set_initiator{|s| s.initial? ? 0 : nil }
59
+ algo.set_start_predicate{|s| s.initial? }
60
+ algo.call(self, key)
66
61
  deepest = states.max do |s0,s1|
67
- # Here, we do not take unreachable states into account
68
- # so that -1 is taken when nil is encountered
62
+ # do not take unreachable states into account: -1 is taken if nil is encountered
69
63
  (s0[:depth] || -1) <=> (s1[:depth] || -1)
70
64
  end
71
65
  deepest[:depth]
@@ -107,13 +107,13 @@ module Stamina
107
107
  def main
108
108
  alph, states = @automaton.alphabet, @automaton.states
109
109
  old_nb_states = -1
110
- partition = states.collect{|s| s.accepting? ? 1 : 0}
110
+ partition = states.map{|s| s.accepting? ? 1 : 0}
111
111
  until (nb_states = partition.uniq.size) == old_nb_states
112
112
  old_nb_states = nb_states
113
113
  alph.each do |symbol|
114
- reached = states.collect{|s| partition[s.dfa_step(symbol).index]}
114
+ reached = states.map{|s| partition[s.dfa_step(symbol).index]}
115
115
  rehash = Hash.new{|h,k| h[k] = h.size}
116
- partition = partition.zip(reached).collect{|pair| rehash[pair]}
116
+ partition = partition.zip(reached).map{|pair| rehash[pair]}
117
117
  end
118
118
  end
119
119
  minimized_dfa(@automaton, nb_states, partition)
@@ -0,0 +1,267 @@
1
+ module Stamina
2
+ class Automaton
3
+ #
4
+ # Automaton state.
5
+ #
6
+ class State
7
+ include Stamina::Markable
8
+ attr_reader :automaton, :index
9
+
10
+ #
11
+ # Creates a state.
12
+ #
13
+ # Arguments:
14
+ # - automaton: parent automaton of the state.
15
+ # - index: index of the state in the state list.
16
+ # - data: user data attached to this state.
17
+ #
18
+ def initialize(automaton, index, data)
19
+ @automaton = automaton
20
+ @index = index
21
+ @data = data.dup
22
+ @out_edges = []
23
+ @in_edges = []
24
+ @epsilon_closure = nil
25
+ end
26
+
27
+ ### public read-only section ###############################################
28
+ public
29
+
30
+ # Returns true if this state is an initial state, false otherwise.
31
+ def initial?
32
+ !!@data[:initial]
33
+ end
34
+
35
+ # Sets this state as an initial state.
36
+ def initial!(value = true)
37
+ @data[:initial] = value
38
+ end
39
+
40
+ # Returns true if this state is an accepting state, false otherwise.
41
+ def accepting?
42
+ !!@data[:accepting]
43
+ end
44
+
45
+ # Sets this state as an accepting state.
46
+ def accepting!(value = true)
47
+ @data[:accepting] = value
48
+ end
49
+
50
+ # Returns true if this state is an error state, false otherwise.
51
+ def error?
52
+ !!@data[:error]
53
+ end
54
+
55
+ # Sets this state as an error state.
56
+ def error!(value = true)
57
+ @data[:error] = value
58
+ end
59
+
60
+ # Returns true if this state is deterministic, false otherwise.
61
+ def deterministic?
62
+ outs = out_symbols
63
+ (outs.size==@out_edges.size) and not(outs.include?(nil))
64
+ end
65
+
66
+ # Checks if this state is a sink state or not. Sink states are defined as
67
+ # non accepting states having no outgoing transition or only loop
68
+ # transitions.
69
+ def sink?
70
+ !accepting? && out_edges.all?{|e| e.target==self}
71
+ end
72
+
73
+ # Returns an array containing all incoming edges of the state. Edges are
74
+ # sorted if _sorted_ is set to true. If two incoming edges have same symbol
75
+ # no order is guaranteed between them.
76
+ #
77
+ # Returned array may be modified.
78
+ def in_edges(sorted=false)
79
+ sorted ? @in_edges.sort : @in_edges.dup
80
+ end
81
+
82
+ # Returns an array containing all outgoing edges of the state. Edges are
83
+ # sorted if _sorted_ is set to true. If two outgoing edges have same symbol
84
+ # no order is guaranteed between them.
85
+ #
86
+ # Returned array may be modified.
87
+ def out_edges(sorted=false)
88
+ sorted ? @out_edges.sort : @out_edges.dup
89
+ end
90
+
91
+ # Returns an array with the different symbols appearing on incoming edges.
92
+ # Returned array does not contain duplicates. Symbols are sorted in the
93
+ # array if _sorted_ is set to true.
94
+ #
95
+ # Returned array may be modified.
96
+ def in_symbols(sorted=false)
97
+ symbols = @in_edges.collect{|e| e.symbol}.uniq
98
+ return sorted ? (symbols.sort &automaton.symbols_comparator) : symbols
99
+ end
100
+
101
+ # Returns an array with the different symbols appearing on outgoing edges.
102
+ # Returned array does not contain duplicates. Symbols are sorted in the
103
+ # array if _sorted_ is set to true.
104
+ #
105
+ # Returned array may be modified.
106
+ def out_symbols(sorted=false)
107
+ symbols = @out_edges.collect{|e| e.symbol}.uniq
108
+ return sorted ? (symbols.sort &automaton.symbols_comparator) : symbols
109
+ end
110
+
111
+ # Returns an array with adjacent states (in or out edge).
112
+ #
113
+ # Returned array may be modified.
114
+ def adjacent_states()
115
+ (in_adjacent_states+out_adjacent_states).uniq
116
+ end
117
+
118
+ # Returns an array with adjacent states along an incoming edge (without
119
+ # duplicates).
120
+ #
121
+ # Returned array may be modified.
122
+ def in_adjacent_states()
123
+ (@in_edges.collect {|e| e.source}).uniq
124
+ end
125
+
126
+ # Returns an array with adjacent states along an outgoing edge (whithout
127
+ # duplicates).
128
+ #
129
+ # Returned array may be modified.
130
+ def out_adjacent_states()
131
+ (@out_edges.collect {|e| e.target}).uniq
132
+ end
133
+
134
+ # Returns reachable states from this one with an input _symbol_. Returned
135
+ # array does not contain duplicates and may be modified. This method if not
136
+ # epsilon symbol aware.
137
+ def step(symbol)
138
+ @out_edges.select{|e| e.symbol==symbol}.collect{|e| e.target}
139
+ end
140
+
141
+ # Returns the state reached from this one with an input _symbol_, or nil if
142
+ # no such state. This method is not epsilon symbol aware. Moreover it is
143
+ # expected to be used on deterministic states only. If the state is not
144
+ # deterministic, the method returns one reachable state if such a state
145
+ # exists; which one is returned must be considered non deterministic.
146
+ def dfa_step(symbol)
147
+ edge = @out_edges.find{|e| e.symbol==symbol}
148
+ edge ? edge.target : nil
149
+ end
150
+
151
+ # Computes the epsilon closure of this state. Epsilon closure is the set of
152
+ # all states reached from this one with a <tt>eps*</tt> input (sequence of
153
+ # zero or more epsilon symbols). The current state is always contained in
154
+ # the epsilon closure. Returns an unsorted array without duplicates; this
155
+ # array may not be modified.
156
+ def epsilon_closure()
157
+ @epsilon_closure ||= compute_epsilon_closure(Set.new).to_a.freeze
158
+ end
159
+
160
+ # Internal implementation of epsilon_closure. _result_ is expected to be
161
+ # a Set instance, is modified and is the returned value.
162
+ def compute_epsilon_closure(result)
163
+ result << self
164
+ step(nil).each do |t|
165
+ t.compute_epsilon_closure(result) unless result.include?(t)
166
+ end
167
+ raise if result.nil?
168
+ return result
169
+ end
170
+
171
+ # Computes an array representing the set of states that can be reached from
172
+ # this state with a given input _symbol_. Returned array does not contain
173
+ # duplicates and may be modified. No particular ordering of states in the
174
+ # array is guaranteed.
175
+ #
176
+ # This method is epsilon symbol aware (represented with nil) on non
177
+ # deterministic automata, meaning that it actually computes the set of
178
+ # reachable states through strings respecting the <tt>eps* symbol eps*</tt>
179
+ # regular expression, where eps is the epsilon symbol.
180
+ def delta(symbol)
181
+ if automaton.deterministic?
182
+ target = dfa_delta(symbol)
183
+ target.nil? ? [] : [target]
184
+ else
185
+ at_epsilon = epsilon_closure
186
+ at_espilon_then_symbol = at_epsilon.map{|s| s.step(symbol)}.flatten.uniq
187
+ at_espilon_then_symbol.map{|s| s.epsilon_closure}.flatten.uniq
188
+ end
189
+ end
190
+
191
+ # Returns the target state that can be reached from this state with _symbol_
192
+ # input. Returns nil if no such state exists.
193
+ #
194
+ # This method is expected to be used on deterministic automata. Unlike delta,
195
+ # it returns a State instance (or nil), not an array of states. When used on
196
+ # non deterministic automata, it returns a state immediately reachable from
197
+ # this state with _symbol_ input, or nil if no such state exists. This
198
+ # method is not epsilon aware.
199
+ def dfa_delta(symbol)
200
+ return nil if symbol.nil?
201
+ edge = @out_edges.find{|e| e.symbol==symbol}
202
+ edge.nil? ? nil : edge.target
203
+ end
204
+
205
+ # Provides comparator of states, based on the index in the automaton state
206
+ # list. This method returns nil unless _o_ is a State from the same
207
+ # automaton than self.
208
+ def <=>(o)
209
+ return nil unless State===o
210
+ return nil unless automaton===o.automaton
211
+ return index <=> o.index
212
+ end
213
+
214
+ # Returns a string representation
215
+ def inspect
216
+ 's' << @index.to_s
217
+ end
218
+
219
+ # Returns a string representation
220
+ def to_s
221
+ 's' << @index.to_s
222
+ end
223
+
224
+ ### protected write section ################################################
225
+ protected
226
+
227
+ # Changes the index of this state in the state list. This method is only
228
+ # expected to be used by the automaton itself.
229
+ def index=(i) @index=i end
230
+
231
+ #
232
+ # Fired by Loaded when a user data is changed. The message is forwarded to
233
+ # the automaton.
234
+ #
235
+ def state_changed(what, description)
236
+ @epsilon_closure = nil
237
+ @automaton.send(:state_changed, what, description)
238
+ end
239
+
240
+ # Adds an incoming edge to the state.
241
+ def add_incoming_edge(edge)
242
+ @epsilon_closure = nil
243
+ @in_edges << edge
244
+ end
245
+
246
+ # Adds an outgoing edge to the state.
247
+ def add_outgoing_edge(edge)
248
+ @epsilon_closure = nil
249
+ @out_edges << edge
250
+ end
251
+
252
+ # Adds an incoming edge to the state.
253
+ def drop_incoming_edge(edge)
254
+ @epsilon_closure = nil
255
+ @in_edges.delete(edge)
256
+ end
257
+
258
+ # Adds an outgoing edge to the state.
259
+ def drop_outgoing_edge(edge)
260
+ @epsilon_closure = nil
261
+ @out_edges.delete(edge)
262
+ end
263
+
264
+ protected :compute_epsilon_closure
265
+ end # class State
266
+ end # class Automaton
267
+ end # module Stamina
@@ -135,7 +135,7 @@ module Stamina
135
135
  #
136
136
  def step(from, symbol)
137
137
  from = walking_to_from(from)
138
- from.collect{|s| s.step(symbol)}.flatten.uniq
138
+ from.map{|s| s.step(symbol)}.flatten.uniq
139
139
  end
140
140
 
141
141
  #
@@ -144,7 +144,7 @@ module Stamina
144
144
  # reached with the given symbol.
145
145
  #
146
146
  def dfa_step(from, symbol)
147
- step = walking_to_from(from).collect{|s| s.dfa_step(symbol)}.flatten.uniq
147
+ step = walking_to_from(from).map{|s| s.dfa_step(symbol)}.flatten.uniq
148
148
  walking_to_dfa_result(step, from)
149
149
  end
150
150
 
@@ -158,7 +158,7 @@ module Stamina
158
158
  # regular expression, where eps is the epsilon symbol.
159
159
  #
160
160
  def delta(from, symbol)
161
- walking_to_from(from).collect{|s| s.delta(symbol)}.flatten.uniq
161
+ walking_to_from(from).map{|s| s.delta(symbol)}.flatten.uniq
162
162
  end
163
163
 
164
164
  #
@@ -171,7 +171,7 @@ module Stamina
171
171
  if from.is_a?(Automaton::State)
172
172
  from.dfa_delta(symbol)
173
173
  else
174
- delta = walking_to_from(from).collect{|s| s.dfa_delta(symbol)}.flatten.uniq
174
+ delta = walking_to_from(from).map{|s| s.dfa_delta(symbol)}.flatten.uniq
175
175
  walking_to_dfa_result(delta, from)
176
176
  end
177
177
  end
@@ -344,7 +344,7 @@ module Stamina
344
344
  # Implements _from_ conventions.
345
345
  def walking_to_from(from)
346
346
  return initial_states if from.nil?
347
- Array===from ? from.collect{|s| to_state(s)} : [to_state(from)]
347
+ Array===from ? from.map{|s| to_state(s)} : [to_state(from)]
348
348
  end
349
349
 
350
350
  # Implements _return_ conventions of dfa_xxx methods.
@@ -2,9 +2,10 @@ require_relative 'version'
2
2
  require_relative 'errors'
3
3
  require_relative 'ext/math'
4
4
  require_relative 'markable'
5
+ require_relative 'utils'
6
+ require_relative 'transition_system'
5
7
  require_relative 'adl'
6
8
  require_relative 'automaton'
7
- require_relative 'utils'
8
9
  require_relative 'input_string'
9
10
  require_relative 'sample'
10
11
  require_relative 'dsl'
@@ -14,7 +14,9 @@ module Stamina
14
14
  #
15
15
  # Returns user-value associated to _key_, nil if no such key in user-data.
16
16
  #
17
- def [](key) @data[key] end
17
+ def [](key)
18
+ @data[key]
19
+ end
18
20
 
19
21
  #
20
22
  # Associates _value_ to _key_ in user-data. Overrides previous value if
@@ -33,6 +35,17 @@ module Stamina
33
35
  state_changed(:loaded_pair, [key,oldvalue,nil]) if self.respond_to? :state_changed
34
36
  end
35
37
 
38
+ # Returns the values mapped to `keys` as an array
39
+ def marks(*keys)
40
+ raw_data.values_at(*keys)
41
+ end
42
+
43
+ # Returns RAW data, that is without duplicating it. Returns result should not be
44
+ # changed.
45
+ def raw_data
46
+ @data ||= {}
47
+ end
48
+
36
49
  # Extracts the copy of attributes which can subsequently be modified.
37
50
  def data
38
51
  @data.nil? ? {} : @data.dup
@@ -0,0 +1 @@
1
+ require_relative 'transition_system/equivalence'
@@ -0,0 +1,107 @@
1
+ module Stamina
2
+ class TransitionSystem
3
+ class Equivalence
4
+
5
+ # Returns true if `s` and `t` must be considered equivalent, false otherwise.
6
+ def equivalent_systems?(s, t)
7
+ (s.state_count == t.state_count) &&
8
+ (s.edge_count == t.edge_count) &&
9
+ (s.raw_data == t.raw_data)
10
+ end
11
+
12
+ def equivalent_systems!(s, t)
13
+ fail "Non equivalent systems `#{s}` and `#{t}`" unless equivalent_systems?(s,t)
14
+ true
15
+ end
16
+
17
+ # Returns true if `s` and `t` must be considered equivalent, false otherwise.
18
+ def equivalent_states?(s, t)
19
+ s.raw_data == t.raw_data
20
+ end
21
+
22
+ def equivalent_states!(s, t)
23
+ fail "Non equivalent states `#{s}` and `#{t}`" unless equivalent_states?(s,t)
24
+ true
25
+ end
26
+
27
+ # Returns true if `e` and `f` must be considered equivalent, false otherwise.
28
+ def equivalent_edges?(e, f)
29
+ e.raw_data == f.raw_data
30
+ end
31
+
32
+ def equivalent_edges!(s, t)
33
+ fail "Non equivalent edges `#{s}` and `#{t}`" unless equivalent_edges?(s,t)
34
+ true
35
+ end
36
+
37
+ # Finds the edge counterpart of `operand_edge` as an outgoing edge of
38
+ # `reference_state`. The default implementation takes an edge that shares the same
39
+ # symbol as operand_edge.
40
+ def find_edge_counterpart(reference_state, operand_edge)
41
+ symbol = operand_edge.symbol
42
+ reference_state.out_edges.find{|e| e.symbol==symbol}
43
+ end
44
+
45
+ # Computes equivalence pairs through decoration
46
+ class EquivThroughDeco < Utils::Decorate
47
+
48
+ def initialize(reference, algo)
49
+ @reference = reference
50
+ @algo = algo
51
+ end
52
+ attr_reader :reference, :algo
53
+
54
+ def suppremum(d0, d1)
55
+ return (d0 || d1) if (d0.nil? or d1.nil?)
56
+ unless d0==d1
57
+ algo.fail("Different states found through same path: #{d0} & #{d1}")
58
+ end
59
+ d0
60
+ end
61
+
62
+ def propagate(deco, edge)
63
+ symbol = edge.symbol
64
+ source = reference.ith_state(deco)
65
+ eq_edge = algo.find_edge_counterpart(source, edge)
66
+ algo.fail("No such transition `#{symbol}` from #{source}") unless eq_edge
67
+ algo.equivalent_edges!(edge, eq_edge)
68
+ algo.equivalent_states!(edge.target, eq_edge.target)
69
+ eq_edge.target.index
70
+ end
71
+
72
+ def init_deco(s)
73
+ s.initial? ? reference.initial_state.index : nil
74
+ end
75
+
76
+ def take_at_start?(s)
77
+ s.initial?
78
+ end
79
+
80
+ end # EquivThroughDeco
81
+
82
+ # Executes the equivalence algorithm on two transition systems `ts1` and `ts2`.
83
+ # Returns true if they are considered equivalent, false otherwise.
84
+ def call(ts1, ts2, &explain)
85
+ @explain = explain
86
+ catch(:fail) do
87
+ equivalent_systems!(ts1, ts2)
88
+ i1, i2 = ts1.initial_state, ts2.initial_state
89
+ fail "No initial state on ts1" unless i1
90
+ fail "No initial state on ts2" unless i2
91
+ equivalent_states!(i1, i2)
92
+ EquivThroughDeco.new(ts2, self).call(ts1, {})
93
+ return true
94
+ end
95
+ return false
96
+ ensure
97
+ @explain = nil
98
+ end
99
+
100
+ def fail(message)
101
+ @explain.call(message) if @explain
102
+ throw :fail
103
+ end
104
+
105
+ end # class Equivalence
106
+ end # class TransitionSystem
107
+ end # module Stamina
@@ -1,22 +1,35 @@
1
1
  module Stamina
2
2
  module Utils
3
3
  #
4
- # Decorates states of an automaton by applying a propagation rule
5
- # until a fix point is reached.
4
+ # Decorates states of an automaton by applying a propagation rule until a fix point is
5
+ # reached.
6
6
  #
7
7
  class Decorate
8
8
 
9
- # The key to use to maintain the decoration on states (:invariant
10
- # is used by default)
11
- attr_writer :decoration_key
12
-
13
9
  # Creates a decoration algorithm instance
14
- def initialize(decoration_key = :invariant)
15
- @decoration_key = decoration_key
10
+ def initialize(output = :invariant)
11
+ @output = Decorate.state_output(output) if output
16
12
  @suppremum = nil
17
13
  @propagate = nil
14
+ @initiator = nil
15
+ @backward = false
16
+ @start_predicate = nil
17
+ end
18
+
19
+ # Builds an output hash that keeps decoration in states
20
+ def self.state_output(decoration_key)
21
+ Object.new.extend Module.new{
22
+ define_method :[] do |state|
23
+ state[decoration_key]
24
+ end
25
+ define_method :[]= do |state,deco|
26
+ state[decoration_key] = deco
27
+ end
28
+ }
18
29
  end
19
30
 
31
+ ### CONFIGURATION ##################################################################
32
+
20
33
  # Installs a suppremum function through a block.
21
34
  def set_suppremum(&block)
22
35
  raise ArgumentError, 'Suppremum expected through a block' if block.nil?
@@ -24,6 +37,11 @@ module Stamina
24
37
  @suppremum = block
25
38
  end
26
39
 
40
+ # Same as #set_suppremum, but with an explicit proc
41
+ def suppremum=(proc)
42
+ set_suppremum(&proc)
43
+ end
44
+
27
45
  # Installs a propagate function through a block.
28
46
  def set_propagate(&block)
29
47
  raise ArgumentError, 'Propagate expected through a block' if block.nil?
@@ -31,6 +49,43 @@ module Stamina
31
49
  @propagate = block
32
50
  end
33
51
 
52
+ # Same as #set_propagate, but with an explicit proc
53
+ def propagate=(proc)
54
+ set_propagate(&proc)
55
+ end
56
+
57
+ # Set an initiator methods, responsible of computing the initial decoration of
58
+ # each state
59
+ def set_initiator(&block)
60
+ raise ArgumentError, 'Initiator expected through a block' if block.nil?
61
+ raise ArgumentError, 'Block of arity 1 expected' unless block.arity==1
62
+ @initiator = block
63
+ end
64
+
65
+ # Same as #set_initiator but with an explicit proc
66
+ def initiator=(proc)
67
+ set_initiator(&proc)
68
+ end
69
+
70
+ # Sets the start predicate to use
71
+ def set_start_predicate(&block)
72
+ raise ArgumentError, 'Start predicate expected through a block' if block.nil?
73
+ raise ArgumentError, 'Block of arity 1 expected' unless block.arity==1
74
+ @start_predicate = block
75
+ end
76
+
77
+ # Same as #set_start_predicate but with an explicit proc
78
+ def start_predicate=(proc)
79
+ set_start_predicate(&proc)
80
+ end
81
+
82
+ # Sets if the algorithms works backward
83
+ def backward=(val)
84
+ @backward = val
85
+ end
86
+
87
+ ### SUBCLASS HOOKS #################################################################
88
+
34
89
  # Computes the suppremum between two decorations. By default, this method
35
90
  # looks for a suppremum function installed with set_suppremum. If not found,
36
91
  # it tries calling a suppremum method on d0. If not found it raises an error.
@@ -51,31 +106,72 @@ module Stamina
51
106
  raise "No propagate function installed or implemented by decorations"
52
107
  end
53
108
 
54
- # Executes the propagation algorithm on a given automaton.
55
- def execute(fa, bottom, d0)
56
- to_explore = []
109
+ # Returns the initial decoration of state `s`
110
+ def init_deco(s)
111
+ return @initiator.call(s) if @initiator
112
+ raise "No initiator function installed"
113
+ end
57
114
 
58
- # install initial decoration
59
- fa.states.each do |s|
60
- s[@decoration_key] = (s.initial? ? d0 : bottom)
61
- to_explore << s if s.initial?
62
- end
115
+ # Returns the start predicate
116
+ def take_at_start?(s)
117
+ return @start_predicate.call(s) if @start_predicate
118
+ raise "No start predicate function installed"
119
+ end
120
+
121
+ # Work backward?
122
+ def backward?
123
+ @backward
124
+ end
63
125
 
64
- # fix-point loop starting with initial states
65
- until to_explore.empty?
66
- source = to_explore.pop
67
- source.out_edges.each do |edge|
68
- target = edge.target
69
- p_decor = propagate(source[@decoration_key], edge)
70
- p_decor = suppremum(target[@decoration_key], p_decor)
71
- unless p_decor == target[@decoration_key]
72
- target[@decoration_key] = p_decor
73
- to_explore << target unless to_explore.include?(target)
126
+ ### MAIN ###########################################################################
127
+
128
+ # Executes the propagation algorithm on a given automaton.
129
+ def call(fa, out = nil)
130
+ with_output(out) do |output|
131
+ fa.states.each{|s| output[s] = init_deco(s) } # Init decoration on each state
132
+ to_explore = fa.states.select{|s| take_at_start?(s)} # Init to_explore (start predicate)
133
+ until to_explore.empty? # empty to_explore now
134
+ source = to_explore.pop
135
+ each_edge_and_target(source) do |edge, target|
136
+ p_decor = propagate(output[source], edge)
137
+ p_decor = suppremum(output[target], p_decor)
138
+ unless p_decor == output[target]
139
+ output[target] = p_decor
140
+ to_explore << target unless to_explore.include?(target)
141
+ end
74
142
  end
75
143
  end
144
+ fa
76
145
  end
146
+ end
147
+
148
+ # Executes the propagation algorithm on a given automaton.
149
+ def execute(fa, bottom, d0)
150
+ warn "Decorate#execute is deprecated, use Decorate#call (#{caller[0]})"
151
+ self.initiator = lambda{|s| (s.initial? ? d0 : bottom)}
152
+ self.start_predicate = lambda{|s| s.initial? }
153
+ call(fa)
154
+ end
77
155
 
78
- fa
156
+ private
157
+
158
+ def with_output(out)
159
+ if out
160
+ yield out.is_a?(Symbol) ? Decorate.state_output(out) : out
161
+ elsif @output
162
+ warn "Decorate.new(:decokey) is deprecated, use Decorate#call(fa, :decokey) (#{caller[1]})"
163
+ yield @output
164
+ else
165
+ raise ArgumentError, "Output may not be nil", caller
166
+ end
167
+ end
168
+
169
+ def each_edge_and_target(source)
170
+ edges = backward? ? source.in_edges : source.out_edges
171
+ edges.each do |edge|
172
+ target = target = backward? ? edge.source : edge.target
173
+ yield edge, target
174
+ end
79
175
  end
80
176
 
81
177
  end # class Decorate
@@ -2,8 +2,8 @@ module Stamina
2
2
  module Version
3
3
 
4
4
  MAJOR = 0
5
- MINOR = 5
6
- TINY = 4
5
+ MINOR = 6
6
+ TINY = 0
7
7
 
8
8
  def self.to_s
9
9
  [ MAJOR, MINOR, TINY ].join('.')
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stamina-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.4
4
+ version: 0.6.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-03-06 00:00:00.000000000Z
12
+ date: 2012-09-12 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: quickl
16
- requirement: &70340660206260 !ruby/object:Gem::Requirement
16
+ requirement: &70185698728600 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
@@ -21,7 +21,7 @@ dependencies:
21
21
  version: 0.4.3
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70340660206260
24
+ version_requirements: *70185698728600
25
25
  description: ! "Stamina is an automaton and regular inference toolkit initially developped
26
26
  for the \nbaseline of the Stamina Competition (stamina.chefbe.net)."
27
27
  email:
@@ -37,12 +37,14 @@ files:
37
37
  - lib/stamina-core/stamina/automaton/complete.rb
38
38
  - lib/stamina-core/stamina/automaton/compose.rb
39
39
  - lib/stamina-core/stamina/automaton/determinize.rb
40
+ - lib/stamina-core/stamina/automaton/edge.rb
40
41
  - lib/stamina-core/stamina/automaton/equivalence.rb
41
42
  - lib/stamina-core/stamina/automaton/hide.rb
42
43
  - lib/stamina-core/stamina/automaton/metrics.rb
43
44
  - lib/stamina-core/stamina/automaton/minimize/hopcroft.rb
44
45
  - lib/stamina-core/stamina/automaton/minimize/pitchies.rb
45
46
  - lib/stamina-core/stamina/automaton/minimize.rb
47
+ - lib/stamina-core/stamina/automaton/state.rb
46
48
  - lib/stamina-core/stamina/automaton/strip.rb
47
49
  - lib/stamina-core/stamina/automaton/walking.rb
48
50
  - lib/stamina-core/stamina/automaton.rb
@@ -62,6 +64,8 @@ files:
62
64
  - lib/stamina-core/stamina/input_string.rb
63
65
  - lib/stamina-core/stamina/markable.rb
64
66
  - lib/stamina-core/stamina/sample.rb
67
+ - lib/stamina-core/stamina/transition_system/equivalence.rb
68
+ - lib/stamina-core/stamina/transition_system.rb
65
69
  - lib/stamina-core/stamina/utils/decorate.rb
66
70
  - lib/stamina-core/stamina/utils.rb
67
71
  - lib/stamina-core/stamina/version.rb
@@ -78,6 +82,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
78
82
  - - ! '>='
79
83
  - !ruby/object:Gem::Version
80
84
  version: '0'
85
+ segments:
86
+ - 0
87
+ hash: -57027985138010278
81
88
  required_rubygems_version: !ruby/object:Gem::Requirement
82
89
  none: false
83
90
  requirements:
@@ -86,9 +93,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
93
  version: '0'
87
94
  requirements: []
88
95
  rubyforge_project:
89
- rubygems_version: 1.8.10
96
+ rubygems_version: 1.8.15
90
97
  signing_key:
91
98
  specification_version: 3
92
99
  summary: Automaton and Regular Inference Toolkit
93
100
  test_files: []
94
- has_rdoc: