stamina-core 0.5.4 → 0.6.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.
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: